Skip to content
openapi.yaml

API v1.1.0 · REST and JSON

Nori API

This is the same API the mobile app uses. Read catalogs, create orders and invoices, record payments and work offline, with the same rules and permissions as the web app. If a user can't do something on screen, they can't do it through the API either.

Base URL Each company has its own domain and database.

https://{company}.nori.software/api/v1

Format
JSON; amounts as decimal strings
Authentication
Bearer, one token per device
Writes
Idempotency-Key required
Pagination
Cursor based, up to 200 per page
Limit
240 requests per minute

Building an integration? Write to us and we'll review it with you.

curl -X POST https://demo.nori.software/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "ana",
    "password": "secreto",
    "device_name": "Pixel 8 de Ana",
    "tenant": "demo"
  }'
const response = await fetch('https://demo.nori.software/api/v1/auth/login', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "username": "ana",
    "password": "secreto",
    "device_name": "Pixel 8 de Ana",
    "tenant": "demo"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('auth/login', [
    'json' => [
        'username' => 'ana',
        'password' => 'secreto',
        'device_name' => 'Pixel 8 de Ana',
        'tenant' => 'demo',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 201
{
  "data": {
    "token": "12|Xk2m...",
    "token_type": "Bearer",
    "expires_at": "2026-11-03T10:15:00-06:00",
    "tenant": {
      "id": "demo",
      "name": "Demo",
      "domain": "demo.nori.software"
    },
    "signed_out_devices": [
      "Pixel 8 de Ana"
    ],
    "user": {
      "id": 42,
      "username": "ana",
      "name": "Ana López",
      "email": "ana@example.com",
      "mobile_phone": "+528112345678",
      "locale": "es_MX",
      "is_admin": false,
      "roles": [
        "Vendedor"
      ],
      "role_labels": [
        "Sales rep"
      ]
    },
    "license_type": "cajero",
    "permissions": [
      "quotations.create",
      "sales_orders.create",
      "incoming_payments.create"
    ],
    "company": {
      "name": "Demo, S.A. de C.V.",
      "accent_color": "#2563eb",
      "country": "MX",
      "logo": "…"
    },
    "working_context": {
      "branch": {},
      "sales_employee": {},
      "default_partner": {},
      "warehouse_id": 42,
      "must_choose_warehouse": false,
      "warehouses": [
        {}
      ],
      "allowed_price_list_ids": [
        42
      ],
      "minimum_price_list_id": 42,
      "max_discount_percent": 1.5,
      "allowed_warehouse_ids": [
        42
      ],
      "tender_types": [
        {}
      ]
    }
  }
}

Guide

Authentication

Each device signs in with the user's web app username and password and gets its own token. Signing in again on the same device (device_name) replaces the previous token.

Send the token on every call as Authorization: Bearer <token>. It expires after 30 days. Renew it with POST /auth/refresh a few days before expires_at; the old token stops working as soon as you do.

Login allows five attempts per minute per user and IP. A user can sign out a lost phone with GET /auth/devices and DELETE /auth/devices/{token}.

GET /me
curl https://demo.nori.software/api/v1/me \
  -H "Authorization: Bearer $NORI_TOKEN"
curl -X POST https://demo.nori.software/api/v1/auth/refresh \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/auth/refresh', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('auth/refresh', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);

Guide

Company

Each company has its own domain and database. The simplest option is to call the company's domain, as the web app does: https://your-company.nori.software/api/v1.

From the central domain (api.nori.software) send the company code in the X-Tenant header; at login the tenant field works too. Login returns tenant.domain, so you only need to ask for the code once.

A token exists only in its company's database and doesn't work for any other company.

HTTPcodeDescription
400tenant_requiredX-Tenant missing on the central domain
404tenant_not_foundThe company code does not exist
403account_suspendedThe company is suspended
cURL
# Company domain
curl https://your-company.nori.software/api/v1/items \
  -H "Authorization: Bearer $NORI_TOKEN"

# Central domain
curl https://api.nori.software/api/v1/items \
  -H "X-Tenant: your-company" \
  -H "Authorization: Bearer $NORI_TOKEN"

Guide

Format and pagination

Successful responses come as {"data": …} and some add meta. Route sales endpoints (/route/*) respond without data, in the same shape the web app's route screen uses.

Amounts and quantities are decimal strings with the stored precision ("232.000000"). Don't convert them to double for money math. Dates are YYYY-MM-DD and timestamps are ISO 8601 with time zone.

Lists use cursor pagination. Request ?per_page=50 (up to 200) and continue with cursor=meta.next_cursor until it's null.

Documents and payments carry pdf_url, a signed link that lets you open or share the PDF without a token for 30 minutes.

GET /items 200
{
  "data": [
    {
      "id": 5,
      "code": "ROSA",
      "name": "Red rose",
      "barcode": "7501055363057",
      "uom": "Piece",
      "is_active": true
    }
  ],
  "links": {
    "next": "https://demo.nori.software/api/v1/items?cursor=eyJpZCI6NX0",
    "prev": null
  },
  "meta": {
    "next_cursor": "eyJpZCI6NX0",
    "prev_cursor": null,
    "per_page": 50
  }
}

Guide

Errors

Every error uses the same envelope: a message ready to show and an error object with a stable code, the field to fix when it applies and a request_id for support.

authorization_required includes the same signed request the web screen gets. Don't modify it. Pass it to the authorization endpoints to request approval or to apply one that was already granted.

HTTPcodeDescription
422validationInvalid data; fields has the messages per field
422business_ruleA business rule failed (credit, stock…); field says what to fix
422authorization_requiredNeeds supervisor approval; authorization contains the request
401unauthenticatedNo token, or an expired or revoked one
403forbiddenThe user lacks the permission or is out of scope
404not_foundThe resource does not exist
428idempotency_key_requiredA write without Idempotency-Key
422idempotency_key_reusedThe key was already used with another request
429rate_limitedToo many requests; wait for the time in Retry-After
500server_errorUnexpected error; show the request_id to the user
Response 422
{
  "message": "The payment exceeds the balance of Invoice A-120 (232.00).",
  "error": {
    "code": "business_rule",
    "message": "The payment exceeds the balance of Invoice A-120 (232.00).",
    "field": "applications",
    "request_id": "K3J9QX2A"
  }
}

Guide

Idempotency

Every write (POST, PUT, PATCH and DELETE) carries Idempotency-Key with a UUID v4. Generate the key when the user takes the action, store it with the operation and reuse it on every retry.

  1. 01The first request runs and its successful response is stored.
  2. 02The same key with the same request returns the stored response with Idempotent-Replayed: true, and nothing runs again.
  3. 03The same key with a different request returns 422 idempotency_key_reused.
  4. 04Failed operations aren't stored, so you can fix the request and retry with the same key.

Login, refresh, logout, revoking a device, the /pricing/* calculations and /sync/push (each operation carries its own key) don't take a key. On route sales endpoints the key is optional.

curl -X POST https://demo.nori.software/api/v1/customers \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Abarrotes Don Pepe",
    "phone": "3312345678",
    "latitude": 20.67,
    "longitude": -103.35
  }'
const response = await fetch('https://demo.nori.software/api/v1/customers', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "Abarrotes Don Pepe",
    "phone": "3312345678",
    "latitude": 20.67,
    "longitude": -103.35
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('customers', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'name' => 'Abarrotes Don Pepe',
        'phone' => '3312345678',
        'latitude' => 20.67,
        'longitude' => -103.35,
    ],
]);
$data = json_decode($response->getBody(), true);
Response 201
HTTP/1.1 201 Created
Idempotent-Replayed: true
Content-Type: application/json

Guide

Limits

Over the limit, the API returns 429 rate_limited with a Retry-After header in seconds.

Requests per user and company240 per minute
Sign in5 per minute per user and IP
Records per list pageUp to 200
Rows per sync page500 by default, up to 2,000
Operations per queue push100
Response 429
HTTP/1.1 429 Too Many Requests
Retry-After: 12

{
  "message": "Too many requests.",
  "error": {
    "code": "rate_limited",
    "message": "Too many requests.",
    "request_id": "K3J9QX2A"
  }
}

Guide

Offline work

GET /sync lists the catalogs the user can keep on the phone, based on their permissions: items, prices, stock, customers, price lists, tender types, warehouses, reason codes and visit reasons.

Each catalog downloads as incremental changes. With no token, or a very old one, you get meta.reset: true and must clear the local table. Delete what comes in deleted, upsert data by id and store meta.sync_token only after the last page.

Anything captured offline is sent in order with POST /sync/push, up to 100 operations per push. One failed operation doesn't stop the rest. A "$ref:<key>:<path>" value takes data from an earlier operation of the same push, such as the id of a customer created offline.

sync
token = saved[entity]        # null the first time
cursor = null
loop:
  r = GET /sync/{entity}?updated_since={token}&cursor={cursor}
  if r.meta.reset and cursor == null: empty the local table
  delete r.deleted             # delete first
  upsert r.data by id          # then insert or update
  if not r.meta.has_more: break
  cursor = r.meta.next_cursor
saved[entity] = r.meta.sync_token
curl -X POST https://demo.nori.software/api/v1/sync/push \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "operations": [
      {
        "key": "6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001",
        "method": "POST",
        "path": "/api/v1/customers",
        "body": {
          "name": "Tienda nueva"
        }
      },
      {
        "key": "6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0002",
        "method": "POST",
        "path": "/api/v1/documents",
        "body": {
          "type": "sales_order",
          "business_partner_id": "$ref:6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001:data.id",
          "lines": [
            {
              "item_id": 5,
              "quantity": 1
            }
          ]
        }
      }
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/sync/push', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "operations": [
      {
        "key": "6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001",
        "method": "POST",
        "path": "/api/v1/customers",
        "body": {
          "name": "Tienda nueva"
        }
      },
      {
        "key": "6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0002",
        "method": "POST",
        "path": "/api/v1/documents",
        "body": {
          "type": "sales_order",
          "business_partner_id": "$ref:6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001:data.id",
          "lines": [
            {
              "item_id": 5,
              "quantity": 1
            }
          ]
        }
      }
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('sync/push', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
    'json' => [
        'operations' => [
            [
                'key' => '6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001',
                'method' => 'POST',
                'path' => '/api/v1/customers',
                'body' => [
                    'name' => 'Tienda nueva',
                ],
            ],
            [
                'key' => '6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0002',
                'method' => 'POST',
                'path' => '/api/v1/documents',
                'body' => [
                    'type' => 'sales_order',
                    'business_partner_id' => '$ref:6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001:data.id',
                    'lines' => [
                        [
                            'item_id' => 5,
                            'quantity' => 1,
                        ],
                    ],
                ],
            ],
        ],
    ],
]);
$data = json_decode($response->getBody(), true);

Guide

Versioning

The /api/v1 prefix is the major version. Within v1 we only ship backward-compatible changes (new endpoints, fields or values), so ignore fields you don't recognize.

An incompatible change opens /api/v2. The previous version stays available for at least six months and announces it with the Deprecation, Sunset and Link headers.

HTTP
Deprecation: true
Sunset: Wed, 30 Jun 2027 23:59:59 GMT
Link: <https://demo.nori.software/api/v2>; rel="successor-version"

Reference

Authentication

Sign in a device, renew or revoke its token and manage the devices with an open session.

Sign in a device

POST /auth/login No session

Headers

Name Type Req. Description
X-Tenant string no Company code, only on a central domain.

Request body

Name Type Req. Description
username string yes
password string yes
device_name string yes One token per device: signing in again replaces it.
tenant string no Company code, only needed on a central domain.

Responses

  • 201 Token and session
  • 401 Error envelope
  • 422 Error envelope
  • 429 Error envelope
curl -X POST https://demo.nori.software/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "ana",
    "password": "secreto",
    "device_name": "Pixel 8 de Ana",
    "tenant": "demo"
  }'
const response = await fetch('https://demo.nori.software/api/v1/auth/login', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "username": "ana",
    "password": "secreto",
    "device_name": "Pixel 8 de Ana",
    "tenant": "demo"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('auth/login', [
    'json' => [
        'username' => 'ana',
        'password' => 'secreto',
        'device_name' => 'Pixel 8 de Ana',
        'tenant' => 'demo',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 201
{
  "data": {
    "token": "12|Xk2m...",
    "token_type": "Bearer",
    "expires_at": "2026-11-03T10:15:00-06:00",
    "tenant": {
      "id": "demo",
      "name": "Demo",
      "domain": "demo.nori.software"
    },
    "signed_out_devices": [
      "Pixel 8 de Ana"
    ],
    "user": {
      "id": 42,
      "username": "ana",
      "name": "Ana López",
      "email": "ana@example.com",
      "mobile_phone": "+528112345678",
      "locale": "es_MX",
      "is_admin": false,
      "roles": [
        "Vendedor"
      ],
      "role_labels": [
        "Sales rep"
      ]
    },
    "license_type": "cajero",
    "permissions": [
      "quotations.create",
      "sales_orders.create",
      "incoming_payments.create"
    ],
    "company": {
      "name": "Demo, S.A. de C.V.",
      "accent_color": "#2563eb",
      "country": "MX",
      "logo": "…"
    },
    "working_context": {
      "branch": {},
      "sales_employee": {},
      "default_partner": {},
      "warehouse_id": 42,
      "must_choose_warehouse": false,
      "warehouses": [
        {}
      ],
      "allowed_price_list_ids": [
        42
      ],
      "minimum_price_list_id": 42,
      "max_discount_percent": 1.5,
      "allowed_warehouse_ids": [
        42
      ],
      "tender_types": [
        {}
      ]
    }
  }
}

Rotate this device's token (the old one stops working)

POST /auth/refresh

Responses

  • 200 New token and session
  • 401 Error envelope
curl -X POST https://demo.nori.software/api/v1/auth/refresh \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/auth/refresh', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('auth/refresh', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "token": "12|Xk2m...",
    "token_type": "Bearer",
    "expires_at": "2026-11-03T10:15:00-06:00",
    "tenant": {
      "id": "demo",
      "name": "Demo",
      "domain": "demo.nori.software"
    },
    "signed_out_devices": [
      "Pixel 8 de Ana"
    ],
    "user": {
      "id": 42,
      "username": "ana",
      "name": "Ana López",
      "email": "ana@example.com",
      "mobile_phone": "+528112345678",
      "locale": "es_MX",
      "is_admin": false,
      "roles": [
        "Vendedor"
      ],
      "role_labels": [
        "Sales rep"
      ]
    },
    "license_type": "cajero",
    "permissions": [
      "quotations.create",
      "sales_orders.create",
      "incoming_payments.create"
    ],
    "company": {
      "name": "Demo, S.A. de C.V.",
      "accent_color": "#2563eb",
      "country": "MX",
      "logo": "…"
    },
    "working_context": {
      "branch": {},
      "sales_employee": {},
      "default_partner": {},
      "warehouse_id": 42,
      "must_choose_warehouse": false,
      "warehouses": [
        {}
      ],
      "allowed_price_list_ids": [
        42
      ],
      "minimum_price_list_id": 42,
      "max_discount_percent": 1.5,
      "allowed_warehouse_ids": [
        42
      ],
      "tender_types": [
        {}
      ]
    }
  }
}

Revoke this device's token

POST /auth/logout

Responses

  • 200 Done
  • 401 Error envelope
curl -X POST https://demo.nori.software/api/v1/auth/logout \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/auth/logout', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('auth/logout', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "message": "Listo."
  }
}

The user's signed-in devices

GET /auth/devices

Responses

  • 200 Devices
curl https://demo.nori.software/api/v1/auth/devices \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/auth/devices', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('auth/devices', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "id": 42,
      "device_name": "Pixel 8 de Ana",
      "last_used_at": "2026-11-03T10:15:00-06:00",
      "expires_at": "2026-11-03T10:15:00-06:00",
      "created_at": "2026-11-03T10:15:00-06:00",
      "is_current": false
    }
  ]
}

Sign a device out (lost phone)

DELETE /auth/devices/{token}

Path parameters

Name Type Req. Description
token integer yes

Responses

  • 200 Done
  • 404 Error envelope
curl -X DELETE https://demo.nori.software/api/v1/auth/devices/18 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/auth/devices/18', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->delete('auth/devices/18', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "message": "Listo."
  }
}

User, permissions and working context of this device

GET /me

Responses

  • 200 Session
curl https://demo.nori.software/api/v1/me \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/me', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('me', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "user": {
      "id": 42,
      "username": "ana",
      "name": "Ana López",
      "email": "ana@example.com",
      "mobile_phone": "+528112345678",
      "locale": "es_MX",
      "is_admin": false,
      "roles": [
        "Vendedor"
      ],
      "role_labels": [
        "Sales rep"
      ]
    },
    "license_type": "cajero",
    "permissions": [
      "quotations.create",
      "sales_orders.create",
      "incoming_payments.create"
    ],
    "company": {
      "name": "Demo, S.A. de C.V.",
      "accent_color": "#2563eb",
      "country": "MX",
      "logo": "…"
    },
    "working_context": {
      "branch": {},
      "sales_employee": {},
      "default_partner": {},
      "warehouse_id": 42,
      "must_choose_warehouse": false,
      "warehouses": [
        {}
      ],
      "allowed_price_list_ids": [
        42
      ],
      "minimum_price_list_id": 42,
      "max_discount_percent": 1.5,
      "allowed_warehouse_ids": [
        42
      ],
      "tender_types": [
        {}
      ]
    }
  }
}

Choose the warehouse this device works in

PUT /me/working-warehouse Idempotency-Key

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
warehouse_id integer yes

Responses

  • 200 Session with the new working warehouse
  • 422 Error envelope
curl -X PUT https://demo.nori.software/api/v1/me/working-warehouse \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "warehouse_id": 3
  }'
const response = await fetch('https://demo.nori.software/api/v1/me/working-warehouse', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "warehouse_id": 3
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->put('me/working-warehouse', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'warehouse_id' => 3,
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "user": {
      "id": 42,
      "username": "ana",
      "name": "Ana López",
      "email": "ana@example.com",
      "mobile_phone": "+528112345678",
      "locale": "es_MX",
      "is_admin": false,
      "roles": [
        "Vendedor"
      ],
      "role_labels": [
        "Sales rep"
      ]
    },
    "license_type": "cajero",
    "permissions": [
      "quotations.create",
      "sales_orders.create",
      "incoming_payments.create"
    ],
    "company": {
      "name": "Demo, S.A. de C.V.",
      "accent_color": "#2563eb",
      "country": "MX",
      "logo": "…"
    },
    "working_context": {
      "branch": {},
      "sales_employee": {},
      "default_partner": {},
      "warehouse_id": 42,
      "must_choose_warehouse": false,
      "warehouses": [
        {}
      ],
      "allowed_price_list_ids": [
        42
      ],
      "minimum_price_list_id": 42,
      "max_discount_percent": 1.5,
      "allowed_warehouse_ids": [
        42
      ],
      "tender_types": [
        {}
      ]
    }
  }
}

Ask Nori to delete the signed-in user's account

POST /me/deletion-request Idempotency-Key

The signed-in user asks Nori to delete their account and personal data (name, e-mail, phone, device tokens, sessions and the GPS points of their route visits) within 30 days. The company's documents and fiscal records are kept, with the user shown as "Deleted user #id". The request is verified by the session; Nori and the company's administrators are notified. If the user already has an open request, it is returned with 200 instead of creating another one. The app should then sign out and clear its local data.

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
reason string | null no Optional reason

Responses

  • 201 Request received
  • 200 The user's open request, already received
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/me/deletion-request \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Ya no trabajo en la empresa"
  }'
const response = await fetch('https://demo.nori.software/api/v1/me/deletion-request', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "reason": "Ya no trabajo en la empresa"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('me/deletion-request', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'reason' => 'Ya no trabajo en la empresa',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 201
{
  "data": {
    "id": 12,
    "status": "verified",
    "created_at": "2026-10-23T18:00:00-06:00",
    "message": "Recibimos tu solicitud. Borraremos tu usuario y tus datos personales en un máximo de 30 días y te avisaremos por correo."
  }
}

Reference

Meta

Read the user's session, permissions, working warehouse, server time and reference catalogs.

Server time and API version (no sign-in)

GET /meta/server-time No session

Headers

Name Type Req. Description
X-Tenant string no Company code, only on a central domain.

Responses

  • 200 Server time
curl https://demo.nori.software/api/v1/meta/server-time
const response = await fetch('https://demo.nori.software/api/v1/meta/server-time', {
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('meta/server-time');
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "server_time": "2026-11-03T10:15:00-06:00",
    "timezone": "America/Mexico_City",
    "api_version": "1.0.0",
    "tenant": "demo"
  }
}

Small reference catalogs (currencies, units, payment terms, sales employees, branches, item groups, SAT payment methods)

GET /catalogs

Responses

  • 200 Catalogs (always whole)
curl https://demo.nori.software/api/v1/catalogs \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/catalogs', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('catalogs', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "currencies": [
      {
        "id": 1,
        "code": "MXN",
        "name": "Peso",
        "symbol": "$",
        "decimals": 2,
        "is_local": true
      }
    ],
    "units_of_measure": [
      {
        "id": 1,
        "code": "PZA",
        "name": "Pieza"
      }
    ]
  }
}

Active user-defined fields per entity (documents, document_lines, business_partners, items, payments)

GET /user-fields

The company's own fields (SAP B1 UDF style,). Each record of those entities carries an udf object with the values by field code; send the same object to write them (documents, their lines and payments). Fields with editable: false come from the ERP and are ignored when sent.

Responses

  • 200 Definitions by entity
curl https://demo.nori.software/api/v1/user-fields \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/user-fields', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('user-fields', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "documents": [
      {
        "code": "U_RUTA",
        "label": "Ruta",
        "type": "list",
        "required": false,
        "default": null,
        "editable": true,
        "values": [
          {
            "value": "N",
            "label": "Norte"
          }
        ]
      }
    ],
    "document_lines": [],
    "business_partners": [],
    "items": [],
    "payments": []
  }
}

Reference

Customers

Search and create customers, read their account (balance, credit, aging) and the open documents to collect.

Search customers

GET /customers

Query parameters

Name Type Req. Description
search string no Code, name, trade name, RFC or phone
mine boolean no Only the user's sales employee's customers
route_id integer no
include_inactive boolean no
per_page integer no Default: 50 Maximum: 200
cursor string no meta.next_cursor of the previous page

Responses

  • 200 Customers
  • 403 Error envelope
curl https://demo.nori.software/api/v1/customers?search=manzana&per_page=50 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/customers?search=manzana&per_page=50', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('customers', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
    'query' => [
        'search' => 'manzana',
        'per_page' => 50,
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "links": {},
  "meta": {
    "path": "…",
    "per_page": 1,
    "next_cursor": "…",
    "prev_cursor": "…"
  },
  "data": [
    {
      "id": 12,
      "code": "C000012",
      "type": "customer",
      "name": "FLORERÍA LUPITA",
      "credit_limit": "1000.000000",
      "balance": "0.000000",
      "is_active": true
    }
  ]
}

Quick customer (the seller becomes its sales employee)

POST /customers Idempotency-Key

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
name string yes
trade_name string no
phone string no
mobile string no
email string<email> no
rfc string no
notes string no
latitude number no
longitude number no

Responses

  • 201 Created
  • 403 Error envelope
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/customers \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Abarrotes Don Pepe",
    "phone": "3312345678",
    "latitude": 20.67,
    "longitude": -103.35
  }'
const response = await fetch('https://demo.nori.software/api/v1/customers', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "Abarrotes Don Pepe",
    "phone": "3312345678",
    "latitude": 20.67,
    "longitude": -103.35
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('customers', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'name' => 'Abarrotes Don Pepe',
        'phone' => '3312345678',
        'latitude' => 20.67,
        'longitude' => -103.35,
    ],
]);
$data = json_decode($response->getBody(), true);
Response 201
{
  "data": {
    "id": 12,
    "code": "C000012",
    "type": "customer",
    "name": "FLORERÍA LUPITA",
    "credit_limit": "1000.000000",
    "balance": "0.000000",
    "is_active": true
  }
}

Customer with addresses, contacts, account (balance, credit, aging) and order defaults

GET /customers/{customer}

Path parameters

Name Type Req. Description
customer integer yes

Responses

  • 200 Customer
  • 404 Error envelope
curl https://demo.nori.software/api/v1/customers/42 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/customers/42', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('customers/42', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "id": 42,
    "code": "ROSA",
    "type": "…",
    "name": "Rosa roja",
    "udf": {
      "U_RUTA": "N",
      "U_PESO": 12.5,
      "U_REVISADO": true
    },
    "trade_name": "…",
    "rfc": "XAXX010101000",
    "email": "ana@example.com",
    "phone": "3312345678",
    "mobile": "3312345678",
    "partner_group_id": 42,
    "price_list_id": 42,
    "currency_id": 42,
    "payment_terms_id": 42,
    "sales_employee_id": 42,
    "delivery_route_id": 42,
    "discount_percent": "232.000000",
    "credit_limit": "232.000000",
    "balance": "232.000000",
    "cfdi_use": "…",
    "tax_regime": "…",
    "billing_address_id": 42,
    "shipping_address_id": 42,
    "contact_id": 42,
    "latitude": 1.5,
    "longitude": 1.5,
    "image_url": "https://demo.nori.software/…",
    "is_active": true,
    "addresses": [
      {}
    ],
    "contacts": [
      {}
    ],
    "updated_at": "2026-11-03T10:15:00-06:00",
    "account": {
      "balance": "232.00",
      "credit_limit": "1000.00",
      "available_credit": "768.00",
      "over_limit": false,
      "aging": {
        "0_30": "232.00",
        "31_60": "0.00",
        "61_90": "0.00",
        "90_plus": "0.00"
      }
    },
    "defaults": {}
  }
}

Open documents to collect (oldest due first; credit memos negative)

GET /customers/{customer}/open-documents

Path parameters

Name Type Req. Description
customer integer yes

Responses

  • 200 Open documents
curl https://demo.nori.software/api/v1/customers/42/open-documents \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/customers/42/open-documents', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('customers/42/open-documents', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "id": 42,
      "type": "quotation",
      "folio": "A-1043",
      "document_date": "2026-11-03",
      "due_date": "2026-11-03",
      "days_overdue": 1,
      "bucket": "…",
      "currency": "MXN",
      "total": "232.000000",
      "balance": "232.000000",
      "balance_local": "…"
    }
  ],
  "meta": {
    "balance": "232.000000",
    "aging": {
      "030": "…",
      "3160": "…",
      "6190": "…",
      "90_plus": "…"
    }
  }
}

Reference

Items

Search items by text or barcode and read stock, batches, serial numbers, bins, images and prices.

Search items by text or barcode

GET /items

Query parameters

Name Type Req. Description
search string no
barcode string no Exact barcode or code
item_group_id integer no
sales boolean no
inventory boolean no
per_page integer no Default: 50 Maximum: 200
cursor string no meta.next_cursor of the previous page

Responses

  • 200 Items
curl https://demo.nori.software/api/v1/items?search=manzana&barcode=7501055363057&per_page=50 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/items?search=manzana&barcode=7501055363057&per_page=50', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('items', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
    'query' => [
        'search' => 'manzana',
        'barcode' => '7501055363057',
        'per_page' => 50,
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "links": {},
  "meta": {
    "path": "…",
    "per_page": 1,
    "next_cursor": "…",
    "prev_cursor": "…"
  },
  "data": [
    {
      "id": 5,
      "code": "ROSA",
      "name": "Rosa roja",
      "is_inventory_item": true,
      "is_sales_item": true,
      "is_active": true,
      "barcodes": [
        {
          "barcode": "7500000000017",
          "uom_id": null,
          "is_default": true
        }
      ]
    }
  ]
}

The items this user sold most lately (quick capture without a route)

GET /items/frequent

Orders, deliveries and invoices the user created or that carry their sales employee; most frequent first.

Query parameters

Name Type Req. Description
days integer no Default: 30 Maximum: 365
limit integer no Default: 30 Maximum: 100

Responses

  • 200 Items with the quantity sold and in how many documents
  • 403 Error envelope
curl https://demo.nori.software/api/v1/items/frequent \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/items/frequent', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('items/frequent', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "item_id": 5,
      "code": "ROSA",
      "name": "Rosa roja",
      "uom": "Pieza",
      "quantity": "24",
      "times": 6
    }
  ]
}

Item with images, stock and kit/package definition

GET /items/{item}

Path parameters

Name Type Req. Description
item integer yes

Responses

  • 200 Item
  • 404 Error envelope
curl https://demo.nori.software/api/v1/items/42 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/items/42', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('items/42', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "id": 42,
    "code": "ROSA",
    "name": "Rosa roja",
    "udf": {
      "U_RUTA": "N",
      "U_PESO": 12.5,
      "U_REVISADO": true
    },
    "description": "…",
    "alternate_code": "…",
    "item_group_id": 42,
    "manufacturer_id": 42,
    "is_inventory_item": false,
    "is_sales_item": false,
    "is_purchase_item": false,
    "is_weighted": false,
    "is_active": true,
    "inventory_uom_id": 42,
    "sales_uom_id": 42,
    "uom_group_id": 42,
    "sales_quantity_per_unit": "232.000000",
    "is_taxable": false,
    "tax_code_id": 42,
    "sales_tax_code_id": 42,
    "default_discount_percent": "232.000000",
    "image_url": "https://demo.nori.software/…",
    "barcodes": [
      {
        "barcode": "7501055363057",
        "uom_id": 42,
        "is_default": false
      }
    ],
    "is_package": false,
    "tracking": "none",
    "updated_at": "2026-11-03T10:15:00-06:00",
    "images": [
      {
        "id": 42,
        "url": "https://demo.nori.software/…",
        "is_main": false,
        "width": 1,
        "height": 1
      }
    ],
    "stock": [
      {
        "id": 42,
        "item_id": 42,
        "warehouse_id": 42,
        "on_hand": "232.000000",
        "committed": "232.000000",
        "ordered": "232.000000",
        "available": "232.000000",
        "is_active": true,
        "updated_at": "2026-11-03T10:15:00-06:00"
      }
    ],
    "package": {
      "item_id": 42,
      "name": "Rosa roja",
      "code": "ROSA",
      "issues_components": false,
      "components": [
        {
          "item_id": null,
          "name": null,
          "code": null,
          "quantity": null
        }
      ],
      "groups": [
        {
          "id": null,
          "name": null,
          "min": null,
          "max": null,
          "options": null
        }
      ]
    }
  }
}

Stock per warehouse (the ones the user may see)

GET /items/{item}/stock

Path parameters

Name Type Req. Description
item integer yes

Query parameters

Name Type Req. Description
warehouse_ids[] array<integer> no

Responses

  • 200 Stock
curl https://demo.nori.software/api/v1/items/42/stock \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/items/42/stock', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('items/42/stock', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "id": 42,
      "item_id": 42,
      "warehouse_id": 42,
      "on_hand": "232.000000",
      "committed": "232.000000",
      "ordered": "232.000000",
      "available": "232.000000",
      "is_active": true,
      "updated_at": "2026-11-03T10:15:00-06:00"
    }
  ]
}

The engine's price for a customer or price list (discounts, promotions, taxes)

GET /items/{item}/price

Path parameters

Name Type Req. Description
item integer yes

Query parameters

Name Type Req. Description
partner_id integer no
price_list_id integer no
quantity number no Default: 1
uom_id integer no
warehouse_id integer no
document_type string no Values: quotationsales_orderdeliveryreturndown_paymentinvoicecredit_memodebit_memotransfer_requestinventory_transfergoods_receiptgoods_issue

Responses

  • 200 Quoted line
curl https://demo.nori.software/api/v1/items/42/price \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/items/42/price', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('items/42/price', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "item_id": 42,
    "item_code": "ROSA",
    "description": "…",
    "quantity": "232.000000",
    "uom_id": 42,
    "warehouse_id": 42,
    "price": "232.000000",
    "discount_percent": "…",
    "discount_source": "…",
    "promotion_id": 42,
    "tax_percent": "…",
    "unit_price": "…",
    "subtotal": "232.000000",
    "tax_total": "…",
    "total": "232.000000"
  }
}

Item gallery

GET /items/{item}/images

Path parameters

Name Type Req. Description
item integer yes

Responses

  • 200 Images
curl https://demo.nori.software/api/v1/items/42/images \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/items/42/images', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('items/42/images', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "id": 42,
      "url": "https://demo.nori.software/…",
      "is_main": false,
      "width": 1,
      "height": 1
    }
  ]
}

Kit components and package choices (null when neither)

GET /items/{item}/package

Path parameters

Name Type Req. Description
item integer yes

Query parameters

Name Type Req. Description
warehouse_id integer no

Responses

  • 200 Package
curl https://demo.nori.software/api/v1/items/42/package \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/items/42/package', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('items/42/package', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "item_id": 42,
    "name": "Rosa roja",
    "code": "ROSA",
    "issues_components": false,
    "components": [
      {
        "item_id": 42,
        "name": "Rosa roja",
        "code": "ROSA",
        "quantity": 1.5
      }
    ],
    "groups": [
      {
        "id": 42,
        "name": "Rosa roja",
        "min": 1,
        "max": 1,
        "options": [
          null
        ]
      }
    ]
  }
}

Price checker in one call (gallery, units, prices per list and unit with taxes, stock per warehouse)

GET /items/{item}/price-check

Prices of the price lists the user may use (all for administrators), per unit (uom_id null = sales unit), with price_with_tax from the item's sales tax. Stock of the warehouses the user may see, the working warehouse first; where the warehouse uses bin locations, bin_location is the item's usual bin there and bins its stock per bin. Items managed by batches list their batches with stock.

Path parameters

Name Type Req. Description
item integer yes

Query parameters

Name Type Req. Description
warehouse_ids[] array<integer> no

Responses

  • 200 Item for the price checker
  • 404 Error envelope
curl https://demo.nori.software/api/v1/items/42/price-check \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/items/42/price-check', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('items/42/price-check', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "id": 5,
    "code": "ROSA",
    "name": "Rosa roja",
    "tax_percent": "16",
    "prices": [
      {
        "price_list_id": 1,
        "price_list": "Menudeo",
        "uom_id": null,
        "uom": "Pieza",
        "currency": "MXN",
        "price": "100.000000",
        "price_with_tax": "116.00"
      }
    ],
    "stock": [
      {
        "warehouse_id": 3,
        "warehouse": {
          "id": 3,
          "code": "MAT",
          "name": "Matriz"
        },
        "on_hand": "12.0000",
        "committed": "0.0000",
        "available": "12.0000",
        "is_working": true,
        "uses_bin_locations": true,
        "bin_location": {
          "id": 7,
          "code": "A-01-03",
          "description": null
        }
      }
    ]
  }
}

Reference

Pricing

Price one line or a whole cart with discounts, promotions and taxes without saving anything.

One line from text ("3*ROSA", barcode) or an item id. Saves nothing.

POST /pricing/quote

Request body

Name Type Req. Description
document_type string yes Values: quotationsales_orderdeliveryreturndown_paymentinvoicecredit_memodebit_memotransfer_requestinventory_transfergoods_receiptgoods_issue
partner_id integer no
price_list_id integer no Default: the customer's, else the user's
currency_id integer no
exchange_rate number no
warehouse_id integer no
text string no
item_id integer no
quantity number no
uom_id integer no

Responses

  • 200 Quoted line
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/pricing/quote \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "document_type": "sales_order",
    "partner_id": 12,
    "text": "3*ROSA"
  }'
const response = await fetch('https://demo.nori.software/api/v1/pricing/quote', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "document_type": "sales_order",
    "partner_id": 12,
    "text": "3*ROSA"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('pricing/quote', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
    'json' => [
        'document_type' => 'sales_order',
        'partner_id' => 12,
        'text' => '3*ROSA',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "item_id": 42,
    "item_code": "ROSA",
    "description": "…",
    "quantity": "232.000000",
    "uom_id": 42,
    "warehouse_id": 42,
    "price": "232.000000",
    "discount_percent": "…",
    "discount_source": "…",
    "promotion_id": 42,
    "tax_percent": "…",
    "unit_price": "…",
    "subtotal": "232.000000",
    "tax_total": "…",
    "total": "232.000000",
    "is_package": false,
    "has_choices": false
  }
}

Every line of a cart plus its totals (promotions included). Saves nothing.

POST /pricing/recalculate

Request body

Name Type Req. Description
document_type string yes Values: quotationsales_orderdeliveryreturndown_paymentinvoicecredit_memodebit_memotransfer_requestinventory_transfergoods_receiptgoods_issue
partner_id integer no
price_list_id integer no Default: the customer's, else the user's
currency_id integer no
exchange_rate number no
warehouse_id integer no
discount_percent number no
lines array<object> yes
lines[].item_id integer yes
lines[].quantity number yes
lines[].uom_id integer no
lines[].price number no
lines[].price_is_manual boolean no
lines[].discount_percent number no
lines[].discount_source string no

Responses

  • 200 Lines, totals and errors per line
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/pricing/recalculate \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "document_type": "sales_order",
    "partner_id": 12,
    "lines": [
      {
        "item_id": 5,
        "quantity": 2
      }
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/pricing/recalculate', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "document_type": "sales_order",
    "partner_id": 12,
    "lines": [
      {
        "item_id": 5,
        "quantity": 2
      }
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('pricing/recalculate', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
    'json' => [
        'document_type' => 'sales_order',
        'partner_id' => 12,
        'lines' => [
            [
                'item_id' => 5,
                'quantity' => 2,
            ],
        ],
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "lines": [
      {
        "item_id": 5,
        "quantity": "2.0000",
        "price": "100.000000",
        "total": "232.00"
      }
    ],
    "totals": {
      "subtotal": "200.00",
      "tax_total": "32.00",
      "total": "232.00"
    },
    "errors": []
  }
}

Reference

Documents

Create, edit and read sales documents, download their PDF or the printed CFDI and email them.

Sales documents (mine, by type, status, partner, dates)

GET /documents

Query parameters

Name Type Req. Description
type string no Values: quotationsales_orderdeliveryreturndown_paymentinvoicecredit_memodebit_memotransfer_requestinventory_transfergoods_receiptgoods_issue
mine boolean no Created by me or of my sales employee
status string no Values: draftopenclosedcancelled
partner_id integer no
from string<date> no
to string<date> no
search string no
per_page integer no Default: 50 Maximum: 200
cursor string no meta.next_cursor of the previous page

Responses

  • 200 Documents (lines not included)
  • 403 Error envelope
curl https://demo.nori.software/api/v1/documents?search=manzana&per_page=50 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/documents?search=manzana&per_page=50', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('documents', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
    'query' => [
        'search' => 'manzana',
        'per_page' => 50,
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "links": {},
  "meta": {
    "path": "…",
    "per_page": 1,
    "next_cursor": "…",
    "prev_cursor": "…"
  },
  "data": [
    {
      "id": 81,
      "type": "sales_order",
      "status": "open",
      "folio": "A-120",
      "total": "232.000000",
      "balance": "232.000000",
      "lines": [
        {
          "id": 301,
          "item_id": 5,
          "quantity": "2.0000",
          "price": "100.000000",
          "total": "232.000000"
        }
      ]
    }
  ]
}

Create (and post) a quotation, sales order, delivery, invoice, return or credit memo

POST /documents Idempotency-Key

Saved by the document engine: prices, discounts, taxes and totals are recalculated, the user's view applies (locked fields keep their defaults). draft: true keeps it unposted. Deliveries and invoices with tenders (or on_credit: true) are posted and collected in one transaction. With crm_deal_id (a quotation made from a CRM deal) the document joins the deal.

Sales rules applied on every save, the same as the web and the POS: - Credit. Posting an order, delivery or invoice whose payment terms give days to pay answers authorization_required (concept credit_exceeded) when its total exceeds the customer's available credit or the customer has overdue invoices. The counter customer and cash terms are never checked. Once approved, saving the same document again (or copying it to the next one) does not ask again unless its total grows. - Payment required. A delivery or invoice whose payment terms require payment is only posted with tenders that cover its whole balance; otherwise 422 business_rule. Orders and quotations are not affected. - Minimum price. A user with a minimum price list (minimum_price_list_id in /me) cannot send a line below that list's price: 422 business_rule, no authorization possible. - Maximum discount. A user with a maximum discount (max_discount_percent in /me) cannot send a line whose discount, the typed line discount and the header discount together (1 − (1 − line) × (1 − header)), is over it: 422 business_rule, no authorization possible, checked before the discount authorizations. Promotions, the customer's own discount and lines copied unchanged from another document do not count. - Minimum margin. With the company's minimum profit margin set, a line whose margin over its net price ((net − cost) ÷ net) is below it answers 422 business_rule, or authorization_required (concept low_margin) when the company asks for approval instead. Lines without cost are not checked. - Salesperson. Without sales_employee_id the document takes the salesperson of the documents its lines were copied from, else the company's rule (the customer's, the user's or the POS terminal's).

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
type string no Required on create Values: quotationsales_orderdeliveryreturndown_paymentinvoicecredit_memodebit_memo
draft boolean no
on_credit boolean no Deliveries and invoices: post and leave on credit (credit policy applies)
tenders array<object> no
tenders[].tender_type_id integer yes
tenders[].amount number yes
tenders[].reference string no
tenders[].credit_memo_id integer no
document_series_id integer no
business_partner_id integer no
contact_id integer no
billing_address_id integer no
shipping_address_id integer no
document_date string<date> no
due_date string<date> no
delivery_date string<date> no
currency_id integer no
exchange_rate number no
price_list_id integer no
payment_terms_id integer no
sales_employee_id integer no
warehouse_id integer no
reference string no
notes string no
discount_percent number no
route_id integer no
cfdi_use string no
payment_method_id integer no
is_reserve boolean no Invoices, on create: reserve invoice (stamped and receivable now; stock moves with the deliveries copied from it)
shipping object no Deliveries and invoices: home delivery. enabled false takes a pending one off the logistics board
shipping.enabled boolean no
shipping.shipping_address_id integer no Defaults to the document shipping address; must be the customer's
shipping.references string no
shipping.requested_date string<date> no
shipping.window_start string no
shipping.window_end string no
shipping.cash_on_delivery boolean no
shipping.latitude number no
shipping.longitude number no
shipping.notes string no
crm_deal_id integer no Create only: the CRM deal this document belongs to (same customer; needs crm_deals.update). It is linked and moves the deal forward
udf object no User-defined fields: value by field code (U_…). Text, integer, number, date (YYYY-MM-DD), boolean, a list value or a user table row code. See GET /user-fields.
lines array<object> no
lines[].id integer no Existing line (edit)
lines[].item_id integer no
lines[].quantity number no
lines[].uom_id integer no
lines[].warehouse_id integer no
lines[].price number no
lines[].price_is_manual boolean no
lines[].discount_percent number no
lines[].base_line_id integer no Copied from this line of a base document
lines[].notes string no
lines[].udf object no User-defined fields: value by field code (U_…). Text, integer, number, date (YYYY-MM-DD), boolean, a list value or a user table row code. See GET /user-fields.
lines[].choices object no Package choices: {group id: {item id: how many}} per unit

Responses

  • 201 Created
  • 403 Error envelope
  • 422 Error envelope
  • 428 Error envelope
curl -X POST https://demo.nori.software/api/v1/documents \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "sales_order",
    "business_partner_id": 12,
    "delivery_date": "2026-11-05",
    "lines": [
      {
        "item_id": 5,
        "quantity": 2
      }
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/documents', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "type": "sales_order",
    "business_partner_id": 12,
    "delivery_date": "2026-11-05",
    "lines": [
      {
        "item_id": 5,
        "quantity": 2
      }
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('documents', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'type' => 'sales_order',
        'business_partner_id' => 12,
        'delivery_date' => '2026-11-05',
        'lines' => [
            [
                'item_id' => 5,
                'quantity' => 2,
            ],
        ],
    ],
]);
$data = json_decode($response->getBody(), true);
Response 201
{
  "data": {
    "id": 42,
    "type": "quotation",
    "status": "draft",
    "is_reserve": false,
    "shipment": {
      "id": 42,
      "status": "pending",
      "requested_date": "2026-11-03",
      "window": "…",
      "cash_on_delivery": false,
      "scheduled_date": "2026-11-03",
      "delivered_at": "2026-11-03T10:15:00-06:00",
      "received_by": "…",
      "failure_reason": "…"
    },
    "folio": "A-1043",
    "series": "…",
    "number": 1,
    "business_partner_id": 42,
    "partner": {},
    "partner_name": "…",
    "document_date": "2026-11-03",
    "due_date": "2026-11-03",
    "delivery_date": "2026-11-03",
    "currency_id": 42,
    "exchange_rate": "…",
    "price_list_id": 42,
    "sales_employee_id": 42,
    "warehouse_id": 42,
    "to_warehouse_id": 42,
    "causality_id": 42,
    "reference": "A-1043",
    "notes": "…",
    "subtotal": "232.000000",
    "discount_total": "…",
    "tax_total": "…",
    "total": "232.000000",
    "paid_total": "…",
    "balance": "232.000000",
    "udf": {
      "U_RUTA": "N",
      "U_PESO": 12.5,
      "U_REVISADO": true
    },
    "lines": [
      {
        "id": 42,
        "line_number": 1,
        "item_id": 42,
        "item_code": "ROSA",
        "description": "…",
        "quantity": "232.000000",
        "open_quantity": "…",
        "uom_id": 42,
        "warehouse_id": 42,
        "price": "232.000000",
        "discount_percent": "…",
        "tax_percent": "…",
        "subtotal": "232.000000",
        "tax_total": "…",
        "total": "232.000000",
        "base_line_id": 42,
        "is_closed": false,
        "udf": {
          "U_RUTA": "N",
          "U_PESO": 12.5,
          "U_REVISADO": true
        },
        "components": [
          null
        ]
      }
    ],
    "pdf_url": "https://demo.nori.software/…",
    "posted_at": "2026-11-03T10:15:00-06:00",
    "created_at": "2026-11-03T10:15:00-06:00",
    "payment": {
      "id": 40,
      "type": "incoming",
      "folio": "P-15",
      "total": "232.000000",
      "change_total": "0.000000",
      "applications": [
        {
          "document_id": 81,
          "folio": "A-120",
          "type": "invoice",
          "amount": "232.000000"
        }
      ]
    }
  }
}

Document with its lines

GET /documents/{document}

Path parameters

Name Type Req. Description
document integer yes

Responses

  • 200 Document
  • 404 Error envelope
curl https://demo.nori.software/api/v1/documents/42 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/documents/42', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('documents/42', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "id": 81,
    "type": "sales_order",
    "status": "open",
    "folio": "A-120",
    "total": "232.000000",
    "balance": "232.000000",
    "lines": [
      {
        "id": 301,
        "item_id": 5,
        "quantity": "2.0000",
        "price": "100.000000",
        "total": "232.000000"
      }
    ]
  }
}

Edit a draft (and post it with post=true) or the header data of a posted document

PATCH /documents/{document} Idempotency-Key

Path parameters

Name Type Req. Description
document integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
type string no Required on create Values: quotationsales_orderdeliveryreturndown_paymentinvoicecredit_memodebit_memo
draft boolean no
on_credit boolean no Deliveries and invoices: post and leave on credit (credit policy applies)
tenders array<object> no
tenders[].tender_type_id integer yes
tenders[].amount number yes
tenders[].reference string no
tenders[].credit_memo_id integer no
document_series_id integer no
business_partner_id integer no
contact_id integer no
billing_address_id integer no
shipping_address_id integer no
document_date string<date> no
due_date string<date> no
delivery_date string<date> no
currency_id integer no
exchange_rate number no
price_list_id integer no
payment_terms_id integer no
sales_employee_id integer no
warehouse_id integer no
reference string no
notes string no
discount_percent number no
route_id integer no
cfdi_use string no
payment_method_id integer no
is_reserve boolean no Invoices, on create: reserve invoice (stamped and receivable now; stock moves with the deliveries copied from it)
shipping object no Deliveries and invoices: home delivery. enabled false takes a pending one off the logistics board
shipping.enabled boolean no
shipping.shipping_address_id integer no Defaults to the document shipping address; must be the customer's
shipping.references string no
shipping.requested_date string<date> no
shipping.window_start string no
shipping.window_end string no
shipping.cash_on_delivery boolean no
shipping.latitude number no
shipping.longitude number no
shipping.notes string no
crm_deal_id integer no Create only: the CRM deal this document belongs to (same customer; needs crm_deals.update). It is linked and moves the deal forward
udf object no User-defined fields: value by field code (U_…). Text, integer, number, date (YYYY-MM-DD), boolean, a list value or a user table row code. See GET /user-fields.
lines array<object> no
lines[].id integer no Existing line (edit)
lines[].item_id integer no
lines[].quantity number no
lines[].uom_id integer no
lines[].warehouse_id integer no
lines[].price number no
lines[].price_is_manual boolean no
lines[].discount_percent number no
lines[].base_line_id integer no Copied from this line of a base document
lines[].notes string no
lines[].udf object no User-defined fields: value by field code (U_…). Text, integer, number, date (YYYY-MM-DD), boolean, a list value or a user table row code. See GET /user-fields.
lines[].choices object no Package choices: {group id: {item id: how many}} per unit
post boolean no

Responses

  • 200 Updated
  • 422 Error envelope
curl -X PATCH https://demo.nori.software/api/v1/documents/42 \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "reference": "OC-9",
    "post": true
  }'
const response = await fetch('https://demo.nori.software/api/v1/documents/42', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "reference": "OC-9",
    "post": true
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->patch('documents/42', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'reference' => 'OC-9',
        'post' => true,
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "id": 81,
    "type": "sales_order",
    "status": "open",
    "folio": "A-120",
    "total": "232.000000",
    "balance": "232.000000",
    "lines": [
      {
        "id": 301,
        "item_id": 5,
        "quantity": "2.0000",
        "price": "100.000000",
        "total": "232.000000"
      }
    ]
  }
}

PDF of the document (its layout)

GET /documents/{document}/pdf

Path parameters

Name Type Req. Description
document integer yes

Query parameters

Name Type Req. Description
template integer no

Responses

  • 200 PDF
  • 503 Error envelope
curl https://demo.nori.software/api/v1/documents/42/pdf \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/documents/42/pdf', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('documents/42/pdf', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
The response is a file (application/pdf).

Printed representation of the document's CFDI 4.0 (Anexo 20), for the app's tickets

GET /documents/{document}/cfdi

Read from the stamped XML: voucher, issuer, receiver, concepts with their taxes, taxes by tax and rate, totals with the amount in words, the stamp (UUID, certificates, seals, original string of the SAT certification complement) and the SAT verification URL to print as a QR. Invoices, credit and debit memos and down payments ("I"/"E"); deliveries and transfers with Carta Porte ("T", carta_porte). A document not stamped yet answers its preview with status: unstamped (stamp and verification_url null; print "SIN TIMBRAR · No válido como comprobante fiscal" when fiscal is true). A cancelled CFDI answers status: cancelled with cancelled_at.

Path parameters

Name Type Req. Description
document integer yes

Responses

  • 200 CFDI print data
  • 404 Error envelope
curl https://demo.nori.software/api/v1/documents/42/cfdi \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/documents/42/cfdi', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('documents/42/cfdi', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "status": "stamped",
    "stamped": false,
    "fiscal": false,
    "cancelled": false,
    "cancelled_at": "2026-11-03T10:15:00-06:00",
    "title": "Factura",
    "version": "4.0",
    "series": "A",
    "folio": "120",
    "date": "2026-10-22T10:15:00",
    "place_of_issue": "42501",
    "voucher_type": {
      "code": "601",
      "name": "General de Ley Personas Morales"
    },
    "export": {
      "code": "601",
      "name": "General de Ley Personas Morales"
    },
    "payment_form": {
      "code": "601",
      "name": "General de Ley Personas Morales"
    },
    "payment_method": {
      "code": "601",
      "name": "General de Ley Personas Morales"
    },
    "currency": "MXN",
    "exchange_rate": "…",
    "payment_terms": "…",
    "issuer": {
      "rfc": "XAXX010101000",
      "name": "Rosa roja",
      "tax_regime": {
        "code": "601",
        "name": "General de Ley Personas Morales"
      }
    },
    "receiver": {
      "rfc": "XAXX010101000",
      "name": "Rosa roja",
      "tax_address": "…",
      "tax_regime": {
        "code": "601",
        "name": "General de Ley Personas Morales"
      },
      "cfdi_use": {
        "code": "601",
        "name": "General de Ley Personas Morales"
      },
      "foreign_tax_id": "…",
      "tax_residence": "…"
    },
    "global_information": {
      "periodicity": {
        "code": "601",
        "name": "General de Ley Personas Morales"
      },
      "months": {
        "code": "601",
        "name": "General de Ley Personas Morales"
      },
      "year": "…"
    },
    "related": [
      {
        "relation_type": {
          "code": "601",
          "name": "General de Ley Personas Morales"
        },
        "uuid": "…"
      }
    ],
    "concepts": [
      {
        "product_code": "…",
        "identification": "…",
        "quantity": "232.000000",
        "unit_code": "…",
        "unit": "Pieza",
        "description": "…",
        "unit_price": "…",
        "amount": "232.000000",
        "discount": "…",
        "tax_object": {
          "code": "601",
          "name": "General de Ley Personas Morales"
        },
        "taxes": [
          null
        ]
      }
    ],
    "subtotal": "232.000000",
    "discount": "…",
    "taxes": [
      {
        "kind": "transfer",
        "tax": {
          "code": "601",
          "name": "General de Ley Personas Morales"
        },
        "factor": "Tasa",
        "rate": "0.160000",
        "base": "300.00",
        "amount": "48.00",
        "label": "IVA trasladado 16 %"
      }
    ],
    "total_transferred": "…",
    "total_withheld": "…",
    "total": "232.000000",
    "amount_in_words": "(TRESCIENTOS CUARENTA Y OCHO PESOS 00/100 M.N.)",
    "stamp": {
      "uuid": "…",
      "stamped_at": "2026-11-03T10:15:00-06:00",
      "pac_rfc": "…",
      "sat_certificate": "…",
      "issuer_certificate": "…",
      "cfd_seal": "…",
      "sat_seal": "…",
      "original_string": "…"
    },
    "issuer_certificate": "…",
    "verification_url": "https://verificacfdi.facturaelectronica.sat.gob.mx/default.aspx?id=…&re=…&rr=…&tt=348.0&fe=…",
    "legend": "Este documento es una representación impresa de un CFDI",
    "payments": [
      {
        "date": "2026-11-03",
        "form": {
          "code": "601",
          "name": "General de Ley Personas Morales"
        },
        "currency": "MXN",
        "exchange_rate": "…",
        "amount": "232.000000",
        "operation_number": "…",
        "taxes": [
          null
        ],
        "documents": [
          null
        ]
      }
    ],
    "payment_totals": {
      "total": "232.000000",
      "lines": [
        {
          "name": null,
          "label": null,
          "amount": null
        }
      ]
    },
    "carta_porte": {},
    "notes": "…"
  }
}

PDF from the signed `pdf_url` of the resource (valid 30 minutes, no token)

GET /documents/{document}/pdf/signed No session

Path parameters

Name Type Req. Description
document integer yes

Query parameters

Name Type Req. Description
expires integer yes
signature string yes

Responses

  • 200 PDF
  • 403 Error envelope
curl https://demo.nori.software/api/v1/documents/42/pdf/signed?expires=valor&signature=valor
const response = await fetch('https://demo.nori.software/api/v1/documents/42/pdf/signed?expires=valor&signature=valor', {
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('documents/42/pdf/signed', [
    'query' => [
        'expires' => 'valor',
        'signature' => 'valor',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
The response is a file (application/pdf).

Email the document with its PDF (and the XML of a stamped CFDI)

POST /documents/{document}/email Idempotency-Key

Path parameters

Name Type Req. Description
document integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
to array<string<email>> no Default: the partner and the document contact
subject string no
message string no
copy_me boolean no

Responses

  • 202 Sent or queued
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/documents/42/email \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "to": [
      "compras@cliente.mx"
    ],
    "message": "Le enviamos su pedido."
  }'
const response = await fetch('https://demo.nori.software/api/v1/documents/42/email', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "to": [
      "compras@cliente.mx"
    ],
    "message": "Le enviamos su pedido."
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('documents/42/email', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'to' => [
            'compras@cliente.mx',
        ],
        'message' => 'Le enviamos su pedido.',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 202
{
  "data": {
    "id": 42,
    "status": "…",
    "to": [
      "…"
    ]
  }
}

Reference

Payments

Record payments against open documents or on account and get the receipt or payment complement.

Collections (payments received)

GET /payments

Query parameters

Name Type Req. Description
mine boolean no
partner_id integer no
from string<date> no
to string<date> no
per_page integer no Default: 50 Maximum: 200
cursor string no meta.next_cursor of the previous page

Responses

  • 200 Payments
curl https://demo.nori.software/api/v1/payments?per_page=50 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/payments?per_page=50', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('payments', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
    'query' => [
        'per_page' => 50,
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "links": {},
  "meta": {
    "path": "…",
    "per_page": 1,
    "next_cursor": "…",
    "prev_cursor": "…"
  },
  "data": [
    {
      "id": 40,
      "type": "incoming",
      "folio": "P-15",
      "total": "232.000000",
      "change_total": "0.000000",
      "applications": [
        {
          "document_id": 81,
          "folio": "A-120",
          "type": "invoice",
          "amount": "232.000000"
        }
      ]
    }
  ]
}

Collect a payment against open documents

POST /payments Idempotency-Key

Recorded by the payments engine: tenders (cash gives change, a reference when the tender asks for it, credit memos as a tender), applications per document (amount: null = what it still owes), the user's cash register must be open for money in the drawer. With on_account: true it needs no documents and what is left over stays as the customer's unapplied balance (unapplied_total).

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
business_partner_id integer yes
on_account boolean no Payment on account: no documents needed; the rest stays as unapplied balance
payment_date string<date> no
reference string no
notes string no
udf object no User-defined fields: value by field code (U_…). Text, integer, number, date (YYYY-MM-DD), boolean, a list value or a user table row code. See GET /user-fields.
applications array<object> no
applications[].document_id integer yes
applications[].amount number | null no null = what the document still owes
tenders array<object> no
tenders[].tender_type_id integer yes
tenders[].amount number yes
tenders[].reference string no
tenders[].credit_memo_id integer no

Responses

  • 201 Recorded
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/payments \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "business_partner_id": 12,
    "applications": [
      {
        "document_id": 81,
        "amount": null
      }
    ],
    "tenders": [
      {
        "tender_type_id": 1,
        "amount": 232
      }
    ],
    "reference": "Cobro ruta"
  }'
const response = await fetch('https://demo.nori.software/api/v1/payments', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "business_partner_id": 12,
    "applications": [
      {
        "document_id": 81,
        "amount": null
      }
    ],
    "tenders": [
      {
        "tender_type_id": 1,
        "amount": 232
      }
    ],
    "reference": "Cobro ruta"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('payments', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'business_partner_id' => 12,
        'applications' => [
            [
                'document_id' => 81,
                'amount' => null,
            ],
        ],
        'tenders' => [
            [
                'tender_type_id' => 1,
                'amount' => 232,
            ],
        ],
        'reference' => 'Cobro ruta',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 201
{
  "data": {
    "id": 40,
    "type": "incoming",
    "folio": "P-15",
    "total": "232.000000",
    "change_total": "0.000000",
    "applications": [
      {
        "document_id": 81,
        "folio": "A-120",
        "type": "invoice",
        "amount": "232.000000"
      }
    ]
  }
}

Payment with tenders and applications

GET /payments/{payment}

Path parameters

Name Type Req. Description
payment integer yes

Responses

  • 200 Payment
  • 404 Error envelope
curl https://demo.nori.software/api/v1/payments/42 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/payments/42', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('payments/42', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "id": 40,
    "type": "incoming",
    "folio": "P-15",
    "total": "232.000000",
    "change_total": "0.000000",
    "applications": [
      {
        "document_id": 81,
        "folio": "A-120",
        "type": "invoice",
        "amount": "232.000000"
      }
    ]
  }
}

PDF of the payment receipt

GET /payments/{payment}/pdf

Path parameters

Name Type Req. Description
payment integer yes

Query parameters

Name Type Req. Description
template integer no

Responses

  • 200 PDF
  • 503 Error envelope
curl https://demo.nori.software/api/v1/payments/42/pdf \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/payments/42/pdf', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('payments/42/pdf', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
The response is a file (application/pdf).

Printed representation of the payment complement (CFDI "P", Pagos 2.0), for the app's tickets

GET /payments/{payment}/cfdi

Same contract as /documents/{document}/cfdi; payments carries each payment with its related documents (UUID, series-folio, installment, previous balance, paid, outstanding, tax object) and payment_totals the totals of the complement. A payment that needs no complement (PUE, cash sale) answers fiscal: false. Outgoing payments answer 404.

Path parameters

Name Type Req. Description
payment integer yes

Responses

  • 200 CFDI print data
  • 404 Error envelope
curl https://demo.nori.software/api/v1/payments/42/cfdi \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/payments/42/cfdi', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('payments/42/cfdi', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "status": "stamped",
    "stamped": false,
    "fiscal": false,
    "cancelled": false,
    "cancelled_at": "2026-11-03T10:15:00-06:00",
    "title": "Factura",
    "version": "4.0",
    "series": "A",
    "folio": "120",
    "date": "2026-10-22T10:15:00",
    "place_of_issue": "42501",
    "voucher_type": {
      "code": "601",
      "name": "General de Ley Personas Morales"
    },
    "export": {
      "code": "601",
      "name": "General de Ley Personas Morales"
    },
    "payment_form": {
      "code": "601",
      "name": "General de Ley Personas Morales"
    },
    "payment_method": {
      "code": "601",
      "name": "General de Ley Personas Morales"
    },
    "currency": "MXN",
    "exchange_rate": "…",
    "payment_terms": "…",
    "issuer": {
      "rfc": "XAXX010101000",
      "name": "Rosa roja",
      "tax_regime": {
        "code": "601",
        "name": "General de Ley Personas Morales"
      }
    },
    "receiver": {
      "rfc": "XAXX010101000",
      "name": "Rosa roja",
      "tax_address": "…",
      "tax_regime": {
        "code": "601",
        "name": "General de Ley Personas Morales"
      },
      "cfdi_use": {
        "code": "601",
        "name": "General de Ley Personas Morales"
      },
      "foreign_tax_id": "…",
      "tax_residence": "…"
    },
    "global_information": {
      "periodicity": {
        "code": "601",
        "name": "General de Ley Personas Morales"
      },
      "months": {
        "code": "601",
        "name": "General de Ley Personas Morales"
      },
      "year": "…"
    },
    "related": [
      {
        "relation_type": {
          "code": "601",
          "name": "General de Ley Personas Morales"
        },
        "uuid": "…"
      }
    ],
    "concepts": [
      {
        "product_code": "…",
        "identification": "…",
        "quantity": "232.000000",
        "unit_code": "…",
        "unit": "Pieza",
        "description": "…",
        "unit_price": "…",
        "amount": "232.000000",
        "discount": "…",
        "tax_object": {
          "code": "601",
          "name": "General de Ley Personas Morales"
        },
        "taxes": [
          null
        ]
      }
    ],
    "subtotal": "232.000000",
    "discount": "…",
    "taxes": [
      {
        "kind": "transfer",
        "tax": {
          "code": "601",
          "name": "General de Ley Personas Morales"
        },
        "factor": "Tasa",
        "rate": "0.160000",
        "base": "300.00",
        "amount": "48.00",
        "label": "IVA trasladado 16 %"
      }
    ],
    "total_transferred": "…",
    "total_withheld": "…",
    "total": "232.000000",
    "amount_in_words": "(TRESCIENTOS CUARENTA Y OCHO PESOS 00/100 M.N.)",
    "stamp": {
      "uuid": "…",
      "stamped_at": "2026-11-03T10:15:00-06:00",
      "pac_rfc": "…",
      "sat_certificate": "…",
      "issuer_certificate": "…",
      "cfd_seal": "…",
      "sat_seal": "…",
      "original_string": "…"
    },
    "issuer_certificate": "…",
    "verification_url": "https://verificacfdi.facturaelectronica.sat.gob.mx/default.aspx?id=…&re=…&rr=…&tt=348.0&fe=…",
    "legend": "Este documento es una representación impresa de un CFDI",
    "payments": [
      {
        "date": "2026-11-03",
        "form": {
          "code": "601",
          "name": "General de Ley Personas Morales"
        },
        "currency": "MXN",
        "exchange_rate": "…",
        "amount": "232.000000",
        "operation_number": "…",
        "taxes": [
          null
        ],
        "documents": [
          null
        ]
      }
    ],
    "payment_totals": {
      "total": "232.000000",
      "lines": [
        {
          "name": null,
          "label": null,
          "amount": null
        }
      ]
    },
    "carta_porte": {},
    "notes": "…"
  }
}

PDF from the signed `pdf_url` (no token)

GET /payments/{payment}/pdf/signed No session

Path parameters

Name Type Req. Description
payment integer yes

Query parameters

Name Type Req. Description
expires integer yes
signature string yes

Responses

  • 200 PDF
  • 403 Error envelope
curl https://demo.nori.software/api/v1/payments/42/pdf/signed?expires=valor&signature=valor
const response = await fetch('https://demo.nori.software/api/v1/payments/42/pdf/signed?expires=valor&signature=valor', {
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('payments/42/pdf/signed', [
    'query' => [
        'expires' => 'valor',
        'signature' => 'valor',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
The response is a file (application/pdf).

Email the receipt

POST /payments/{payment}/email Idempotency-Key

Path parameters

Name Type Req. Description
payment integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
to array<string<email>> no Default: the partner and the document contact
subject string no
message string no
copy_me boolean no

Responses

  • 202 Sent or queued
curl -X POST https://demo.nori.software/api/v1/payments/42/email \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "to": [
      "ana@example.com"
    ],
    "subject": "…",
    "message": "Listo.",
    "copy_me": false
  }'
const response = await fetch('https://demo.nori.software/api/v1/payments/42/email', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "to": [
      "ana@example.com"
    ],
    "subject": "…",
    "message": "Listo.",
    "copy_me": false
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('payments/42/email', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'to' => [
            'ana@example.com',
        ],
        'subject' => '…',
        'message' => 'Listo.',
        'copy_me' => false,
    ],
]);
$data = json_decode($response->getBody(), true);
Response 202
{
  "data": {
    "id": 42,
    "status": "…",
    "to": [
      "…"
    ]
  }
}

Reference

Inventory

Look up stock by scanning, review expiring batches and record transfer requests, transfers, goods receipts and goods issues.

Batches with stock, first to expire first (one row per bin where bins are used)

GET /items/{item}/batches

Path parameters

Name Type Req. Description
item integer yes

Query parameters

Name Type Req. Description
warehouse_ids[] array<integer> no

Responses

  • 200 Batches
curl https://demo.nori.software/api/v1/items/42/batches \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/items/42/batches', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('items/42/batches', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "batch_id": 42,
      "number": "…",
      "supplier_batch": "…",
      "internal_number": "…",
      "availability": "released",
      "manufactured_on": "2026-11-03",
      "expires_on": "2026-11-03",
      "received_on": "2026-11-03",
      "is_expired": false,
      "warehouse_id": 42,
      "warehouse": "…",
      "bin_location_id": 42,
      "bin_code": "…",
      "on_hand": "232.000000"
    }
  ]
}

Serial numbers of the item (in stock unless status says otherwise)

GET /items/{item}/serial-numbers

Path parameters

Name Type Req. Description
item integer yes

Query parameters

Name Type Req. Description
warehouse_ids[] array<integer> no
status string no Values: in_stockissuedall Default: in_stock
search string no

Responses

  • 200 Serial numbers (up to 500)
curl https://demo.nori.software/api/v1/items/42/serial-numbers?search=manzana \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/items/42/serial-numbers?search=manzana', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('items/42/serial-numbers', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
    'query' => [
        'search' => 'manzana',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "serial_number_id": 42,
      "number": "…",
      "status": "in_stock",
      "warehouse_id": 42,
      "warehouse": "…",
      "bin_location_id": 42,
      "bin_code": "…",
      "batch_number": "…",
      "expires_on": "2026-11-03",
      "manufacturer_serial": "…",
      "lot_number": "…",
      "warranty_ends_on": "2026-11-03",
      "availability": "released"
    }
  ]
}

Stock of the item per bin location

GET /items/{item}/bin-stock

Path parameters

Name Type Req. Description
item integer yes

Query parameters

Name Type Req. Description
warehouse_ids[] array<integer> no

Responses

  • 200 Stock per bin
curl https://demo.nori.software/api/v1/items/42/bin-stock \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/items/42/bin-stock', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('items/42/bin-stock', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "warehouse_id": 42,
      "warehouse": "…",
      "bin_location_id": 42,
      "code": "ROSA",
      "description": "…",
      "on_hand": "232.000000"
    }
  ]
}

Active bin locations of a warehouse

GET /warehouses/{warehouse}/bins

Bins as the ERP keeps them (sublevels, bar code, attributes, restriction, min/max). With the ERP integration on they are inherited from it.

Path parameters

Name Type Req. Description
warehouse integer yes

Query parameters

Name Type Req. Description
code string no Only the bin with this code or bar code (scanning the shelf label)

Responses

  • 200 Bins; meta.uses_bin_locations says whether the warehouse keeps stock per bin
  • 403 Error envelope
curl https://demo.nori.software/api/v1/warehouses/42/bins \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/warehouses/42/bins', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('warehouses/42/bins', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "id": 42,
      "code": "ROSA",
      "description": "…",
      "is_default": false,
      "sublevels": [
        "…"
      ],
      "barcode": "7501055363057",
      "attributes": {},
      "restriction": "none",
      "is_receiving": false,
      "minimum_quantity": "…",
      "maximum_quantity": "…"
    }
  ],
  "meta": {
    "uses_bin_locations": false,
    "enforce_default_bin": false
  }
}

What a bin holds (items and batches)

GET /bins/{bin}/stock

Path parameters

Name Type Req. Description
bin integer yes

Responses

  • 200 Contents of the bin
  • 403 Error envelope
curl https://demo.nori.software/api/v1/bins/42/stock \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/bins/42/stock', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('bins/42/stock', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "item_id": 42,
      "code": "ROSA",
      "name": "Rosa roja",
      "batch_id": 42,
      "batch_number": "…",
      "expires_on": "2026-11-03",
      "on_hand": "232.000000"
    }
  ],
  "meta": {}
}

Scan or type an item ("3*CODE", barcode, scale label) with its stock

GET /inventory/lookup

Query parameters

Name Type Req. Description
text string no
item_id integer no
warehouse_ids[] array<integer> no

Responses

  • 200 Item, quantity and balances per warehouse
  • 422 Error envelope
curl https://demo.nori.software/api/v1/inventory/lookup \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/inventory/lookup', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('inventory/lookup', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "item": {
      "id": 5,
      "code": "ROSA",
      "name": "Rosa roja",
      "uom_factor": "1"
    },
    "quantity": "2.0000",
    "balances": {
      "3": {
        "on_hand": "15.0000",
        "committed": "0.0000",
        "ordered": "0.0000",
        "available": "15.0000"
      }
    }
  }
}

Batches with stock that expired or expire within the given days (company default when omitted)

GET /inventory/expiring

Query parameters

Name Type Req. Description
days integer no
warehouse_ids[] array<integer> no

Responses

  • 200 Expiring batches, soonest first
curl https://demo.nori.software/api/v1/inventory/expiring \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/inventory/expiring', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('inventory/expiring', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "item_id": 42,
      "code": "ROSA",
      "name": "Rosa roja",
      "batch_id": 42,
      "number": "…",
      "expires_on": "2026-11-03",
      "days_left": 1,
      "is_expired": false,
      "warehouse_id": 42,
      "warehouse": "…",
      "bin_code": "…",
      "on_hand": "232.000000"
    }
  ],
  "meta": {
    "days": 1
  }
}

Transfer requests with something pending

GET /inventory/transfer-requests/pending

Query parameters

Name Type Req. Description
warehouse_id integer no
limit integer no Default: 50

Responses

  • 200 Requests
curl https://demo.nori.software/api/v1/inventory/transfer-requests/pending \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/inventory/transfer-requests/pending', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('inventory/transfer-requests/pending', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "id": 42,
      "folio": "A-1043",
      "date": "2026-11-03",
      "days": 1,
      "from": "…",
      "to": "…",
      "reference": "A-1043",
      "lines": 1,
      "requested": "…",
      "transferred": "…",
      "pending": "…",
      "progress": 1
    }
  ]
}

Open lines of a request, ready to transfer (base_line_id)

GET /inventory/transfer-requests/{document}/open-lines

Path parameters

Name Type Req. Description
document integer yes

Responses

  • 200 Lines and balances
  • 422 Error envelope
curl https://demo.nori.software/api/v1/inventory/transfer-requests/42/open-lines \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/inventory/transfer-requests/42/open-lines', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('inventory/transfer-requests/42/open-lines', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {}
}

Transfer request, transfer, goods receipt or goods issue

POST /inventory/{movement} Idempotency-Key

Goods receipts and issues need a causality of their direction; receipts need the cost per unit. A transfer from a request sends base_line_id per line.

Path parameters

Name Type Req. Description
movement string yes Values: transfer-requeststransfersgoods-receiptsgoods-issues

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
draft boolean no
document_series_id integer no
document_date string<date> no
warehouse_id integer yes
to_warehouse_id integer no
causality_id integer no
reference string no
notes string no
udf object no User-defined fields: value by field code (U_…). Text, integer, number, date (YYYY-MM-DD), boolean, a list value or a user table row code. See GET /user-fields.
lines array<object> yes
lines[].item_id integer yes
lines[].quantity number yes
lines[].uom_id integer no
lines[].warehouse_id integer no
lines[].to_warehouse_id integer no
lines[].cost number no Goods receipts: cost per unit (required)
lines[].base_line_id integer no Transfers: line of the request
lines[].udf object no User-defined fields: value by field code (U_…). Text, integer, number, date (YYYY-MM-DD), boolean, a list value or a user table row code. See GET /user-fields.
lines[].allocations array<object> no Batches, serial numbers and bins of the line
lines[].allocations[].item_id integer no A component of a kit; the line item when omitted
lines[].allocations[].batch_id integer no
lines[].allocations[].batch_number string no Receipts: a new batch is created with it
lines[].allocations[].serial_number_id integer no
lines[].allocations[].serial_number string no
lines[].allocations[].bin_location_id integer no Bin it goes into (receipts) or comes from (issues, transfers)
lines[].allocations[].to_bin_location_id integer no Transfers: destination bin (same warehouse = bin to bin)
lines[].allocations[].supplier_batch string no
lines[].allocations[].manufactured_on string<date> no
lines[].allocations[].expires_on string<date> no
lines[].allocations[].quantity number no

Responses

  • 201 Saved document
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/inventory/42 \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "warehouse_id": 3,
    "causality_id": 2,
    "lines": [
      {
        "item_id": 5,
        "quantity": 2
      }
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/inventory/42', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "warehouse_id": 3,
    "causality_id": 2,
    "lines": [
      {
        "item_id": 5,
        "quantity": 2
      }
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('inventory/42', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'warehouse_id' => 3,
        'causality_id' => 2,
        'lines' => [
            [
                'item_id' => 5,
                'quantity' => 2,
            ],
        ],
    ],
]);
$data = json_decode($response->getBody(), true);
Response 201
{
  "data": {
    "id": 81,
    "type": "sales_order",
    "status": "open",
    "folio": "A-120",
    "total": "232.000000",
    "balance": "232.000000",
    "lines": [
      {
        "id": 301,
        "item_id": 5,
        "quantity": "2.0000",
        "price": "100.000000",
        "total": "232.000000"
      }
    ]
  }
}

Reference

Inventory counts

Join an inventory count, record what you find by item, batch or bin and follow the progress.

Counts (in progress by default) with their progress

GET /inventory/counts

Query parameters

Name Type Req. Description
status string no Values: countingpostedcancelled
warehouse_id integer no

Responses

  • 200 Counts
curl https://demo.nori.software/api/v1/inventory/counts \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/inventory/counts', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('inventory/counts', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "id": 42,
      "number": 1,
      "title": "…",
      "name": "Rosa roja",
      "warehouse": {},
      "status": "counting",
      "is_blind": false,
      "freezes_stock": false,
      "snapshot_at": "2026-11-03T10:15:00-06:00",
      "notes": "…",
      "progress": {
        "lines": 1,
        "counted": 1,
        "progress": 1
      }
    }
  ]
}

Join a count

GET /inventory/counts/{count}

Path parameters

Name Type Req. Description
count integer yes

Responses

  • 200 Count
curl https://demo.nori.software/api/v1/inventory/counts/42 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/inventory/counts/42', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('inventory/counts/42', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "id": 42,
    "number": 1,
    "title": "…",
    "name": "Rosa roja",
    "warehouse": {},
    "status": "counting",
    "is_blind": false,
    "freezes_stock": false,
    "snapshot_at": "2026-11-03T10:15:00-06:00",
    "notes": "…",
    "progress": {
      "lines": 1,
      "counted": 1,
      "progress": 1
    }
  }
}

Lines and counted lines

GET /inventory/counts/{count}/progress

Path parameters

Name Type Req. Description
count integer yes

Responses

  • 200 Progress
curl https://demo.nori.software/api/v1/inventory/counts/42/progress \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/inventory/counts/42/progress', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('inventory/counts/42/progress', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "lines": 1,
    "counted": 1,
    "progress": 1
  }
}

Lines of the count (system quantity hidden in a blind count)

GET /inventory/counts/{count}/lines

Path parameters

Name Type Req. Description
count integer yes

Query parameters

Name Type Req. Description
counted boolean no
per_page integer no Default: 50 Maximum: 200
cursor string no meta.next_cursor of the previous page

Responses

  • 200 Lines
curl https://demo.nori.software/api/v1/inventory/counts/42/lines?per_page=50 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/inventory/counts/42/lines?per_page=50', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('inventory/counts/42/lines', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
    'query' => [
        'per_page' => 50,
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "id": 42,
      "item_id": 42,
      "code": "ROSA",
      "name": "Rosa roja",
      "uom": "Pieza",
      "system": "…",
      "counted": "…",
      "counted_at": "2026-11-03T10:15:00-06:00",
      "is_extra": false
    }
  ],
  "meta": {
    "next_cursor": "…",
    "per_page": 1
  }
}

Latest captures (mine with mine=1)

GET /inventory/counts/{count}/entries

Path parameters

Name Type Req. Description
count integer yes

Query parameters

Name Type Req. Description
mine boolean no
limit integer no Default: 100

Responses

  • 200 Captures
curl https://demo.nori.software/api/v1/inventory/counts/42/entries \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/inventory/counts/42/entries', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('inventory/counts/42/entries', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "id": 42,
      "line_id": 42,
      "item_id": 42,
      "code": "ROSA",
      "name": "Rosa roja",
      "quantity": "232.000000",
      "counted_after": "…",
      "user_id": 42,
      "user": "…",
      "created_at": "2026-11-03T10:15:00-06:00"
    }
  ]
}

Record what was found (scan adds; mode=set replaces)

POST /inventory/counts/{count}/entries Idempotency-Key

Path parameters

Name Type Req. Description
count integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
text string no Scan or "3*CODE"; a box barcode counts its units
item_id integer no
quantity number no
mode string no Values: addset Default: add
bin_location_id integer no Bin counted (warehouses with bins)
batch_id integer no Batch counted (batch items)
batch_number string no Batch counted by number; created when it does not exist
expires_on string<date> no Expiry of a batch found while counting

Responses

  • 201 Line and progress
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/inventory/counts/42/entries \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "3*ROSA"
  }'
const response = await fetch('https://demo.nori.software/api/v1/inventory/counts/42/entries', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "text": "3*ROSA"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('inventory/counts/42/entries', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'text' => '3*ROSA',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 201
{
  "data": {
    "line": {
      "id": 42,
      "item_id": 42,
      "code": "ROSA",
      "name": "Rosa roja",
      "uom": "Pieza",
      "system": "…",
      "counted": "…",
      "counted_at": "2026-11-03T10:15:00-06:00",
      "is_extra": false
    },
    "progress": {
      "lines": 1,
      "counted": 1,
      "progress": 1
    }
  }
}

Take back the last capture of a line

DELETE /inventory/counts/{count}/lines/{line}/last-entry Idempotency-Key

Path parameters

Name Type Req. Description
count integer yes
line integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Responses

  • 200 Line and progress
curl -X DELETE https://demo.nori.software/api/v1/inventory/counts/42/lines/3/last-entry \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
const response = await fetch('https://demo.nori.software/api/v1/inventory/counts/42/lines/3/last-entry', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->delete('inventory/counts/42/lines/3/last-entry', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "line": {
      "id": 42,
      "item_id": 42,
      "code": "ROSA",
      "name": "Rosa roja",
      "uom": "Pieza",
      "system": "…",
      "counted": "…",
      "counted_at": "2026-11-03T10:15:00-06:00",
      "is_extra": false
    },
    "progress": {
      "lines": 1,
      "counted": 1,
      "progress": 1
    }
  }
}

Reference

Route sales

Run the route day: start and finish, visits, sales, orders, deliveries, collections, pickups, asset service and photos.

The user's routes of the day with stops, reasons and tenders

GET /route/today

Query parameters

Name Type Req. Description
date string<date> no

Responses

  • 200 Plans and catalogs (not wrapped in data)
  • 403 Error envelope
curl https://demo.nori.software/api/v1/route/today \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/route/today', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('route/today', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "date": "2026-11-03",
  "plans": [
    {}
  ],
  "reasons": {},
  "tenders": [
    {}
  ],
  "server_time": "2026-11-03T10:15:00-06:00"
}

One day of a route

GET /route/plans/{plan}

Path parameters

Name Type Req. Description
plan integer yes

Responses

  • 200 Plan (not wrapped)
curl https://demo.nori.software/api/v1/route/plans/42 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/route/plans/42', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('route/plans/42', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "plan": {},
  "replayed": false
}

What the truck carries

GET /route/plans/{plan}/stock

Path parameters

Name Type Req. Description
plan integer yes

Responses

  • 200 Items on the truck
curl https://demo.nori.software/api/v1/route/plans/42/stock \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/route/plans/42/stock', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('route/plans/42/stock', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "items": [
    {}
  ]
}

Start the day (opens the seller's cash register with the fund)

POST /route/plans/{plan}/start Idempotency-Key

Path parameters

Name Type Req. Description
plan integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Request body

Name Type Req. Description
fund number no

Responses

  • 200 Plan (not wrapped)
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/route/plans/42/start \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "fund": 200
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/plans/42/start', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "fund": 200
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/plans/42/start', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'fund' => 200,
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "plan": {},
  "replayed": false
}

Back to the depot

POST /route/plans/{plan}/finish Idempotency-Key

Path parameters

Name Type Req. Description
plan integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Responses

  • 200 Plan (not wrapped)
curl -X POST https://demo.nori.software/api/v1/route/plans/42/finish \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
const response = await fetch('https://demo.nori.software/api/v1/route/plans/42/finish', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/plans/42/finish', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "plan": {},
  "replayed": false
}

Unplanned stop

POST /route/plans/{plan}/visits Idempotency-Key

Path parameters

Name Type Req. Description
plan integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Request body

Name Type Req. Description
business_partner_id integer yes
reason_ids array<integer> no

Responses

  • 200 Visit (not wrapped)
curl -X POST https://demo.nori.software/api/v1/route/plans/42/visits \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "business_partner_id": 42,
    "reason_ids": [
      42
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/plans/42/visits', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "business_partner_id": 42,
    "reason_ids": [
      42
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/plans/42/visits', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'business_partner_id' => 42,
        'reason_ids' => [
            42,
        ],
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "visit": {},
  "replayed": false
}

Customers for an unplanned stop

GET /route/customers

Query parameters

Name Type Req. Description
search string no

Responses

  • 200 Customers
curl https://demo.nori.software/api/v1/route/customers?search=manzana \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/route/customers?search=manzana', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('route/customers', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
    'query' => [
        'search' => 'manzana',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
[
  {
    "id": 42,
    "label": "…",
    "sublabel": "…"
  }
]

A stop with orders to deliver, balance, assets, suggestion and what was done

GET /route/visits/{visit}

Path parameters

Name Type Req. Description
visit integer yes

Responses

  • 200 Visit (not wrapped)
  • 403 Error envelope
curl https://demo.nori.software/api/v1/route/visits/42 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('route/visits/42', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "visit": {},
  "replayed": false
}

Arrived (GPS and device time)

POST /route/visits/{visit}/check-in Idempotency-Key

Path parameters

Name Type Req. Description
visit integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Request body

Name Type Req. Description
lat number no
lng number no
at string<date-time> no Device time

Responses

  • 200 Visit (not wrapped)
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/route/visits/42/check-in \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "lat": 20.6736,
    "lng": -103.344,
    "at": "2026-11-03T09:12:00-06:00"
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/check-in', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lat": 20.6736,
    "lng": -103.344,
    "at": "2026-11-03T09:12:00-06:00"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/visits/42/check-in', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'lat' => 20.6736,
        'lng' => -103.344,
        'at' => '2026-11-03T09:12:00-06:00',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "visit": {},
  "replayed": false
}

Leaving (a sale stop without a sale needs a no-sale reason first)

POST /route/visits/{visit}/check-out Idempotency-Key

Path parameters

Name Type Req. Description
visit integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Request body

Name Type Req. Description
lat number no
lng number no
at string<date-time> no Device time
notes string no

Responses

  • 200 Visit (not wrapped)
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/route/visits/42/check-out \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "lat": 1.5,
    "lng": 1.5,
    "at": "2026-11-03T10:15:00-06:00",
    "notes": "…"
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/check-out', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lat": 1.5,
    "lng": 1.5,
    "at": "2026-11-03T10:15:00-06:00",
    "notes": "…"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/visits/42/check-out', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'lat' => 1.5,
        'lng' => 1.5,
        'at' => '2026-11-03T10:15:00-06:00',
        'notes' => '…',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "visit": {},
  "replayed": false
}

Not visited (reason)

POST /route/visits/{visit}/skip Idempotency-Key

Path parameters

Name Type Req. Description
visit integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Request body

Name Type Req. Description
reason_id integer yes
notes string no

Responses

  • 200 Visit (not wrapped)
curl -X POST https://demo.nori.software/api/v1/route/visits/42/skip \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "reason_id": 42,
    "notes": "…"
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/skip', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "reason_id": 42,
    "notes": "…"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/visits/42/skip', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'reason_id' => 42,
        'notes' => '…',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "visit": {},
  "replayed": false
}

Visited without a sale (reason)

POST /route/visits/{visit}/no-sale Idempotency-Key

Path parameters

Name Type Req. Description
visit integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Request body

Name Type Req. Description
reason_id integer yes
notes string no

Responses

  • 200 Visit (not wrapped)
curl -X POST https://demo.nori.software/api/v1/route/visits/42/no-sale \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "reason_id": 42,
    "notes": "…"
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/no-sale', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "reason_id": 42,
    "notes": "…"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/visits/42/no-sale', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'reason_id' => 42,
        'notes' => '…',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "visit": {},
  "replayed": false
}

Engine prices and totals of a cart before selling

POST /route/visits/{visit}/quote

Path parameters

Name Type Req. Description
visit integer yes

Request body

Name Type Req. Description
kind string no Values: saleorderpickup
lines array<object> no
lines[].item_id integer yes
lines[].quantity number yes
lines[].uom_id integer no

Responses

  • 200 Lines and totals (not wrapped)
curl -X POST https://demo.nori.software/api/v1/route/visits/42/quote \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "sale",
    "lines": [
      {
        "item_id": 42,
        "quantity": 1.5,
        "uom_id": 42
      }
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/quote', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "kind": "sale",
    "lines": [
      {
        "item_id": 42,
        "quantity": 1.5,
        "uom_id": 42
      }
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/visits/42/quote', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
    'json' => [
        'kind' => 'sale',
        'lines' => [
            [
                'item_id' => 42,
                'quantity' => 1.5,
                'uom_id' => 42,
            ],
        ],
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "lines": [
    {}
  ],
  "totals": {
    "subtotal": "232.000000",
    "discount_total": "…",
    "tax_total": "…",
    "total": "232.000000"
  },
  "errors": {}
}

Sale from the truck (remission or invoice), collected or on credit

POST /route/visits/{visit}/sale Idempotency-Key

Path parameters

Name Type Req. Description
visit integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Request body

Name Type Req. Description
lines array<object> yes
lines[].item_id integer yes
lines[].quantity number yes
lines[].uom_id integer no
tenders array<object> no
tenders[].tender_type_id integer yes
tenders[].amount number yes
tenders[].reference string no
tenders[].credit_memo_id integer no

Responses

  • 200 Documents and payment made at the stop (not wrapped)
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/route/visits/42/sale \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "lines": [
      {
        "item_id": 5,
        "quantity": 2
      }
    ],
    "tenders": [
      {
        "tender_type_id": 1,
        "amount": 46.4
      }
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/sale', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lines": [
      {
        "item_id": 5,
        "quantity": 2
      }
    ],
    "tenders": [
      {
        "tender_type_id": 1,
        "amount": 46.4
      }
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/visits/42/sale', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'lines' => [
            [
                'item_id' => 5,
                'quantity' => 2,
            ],
        ],
        'tenders' => [
            [
                'tender_type_id' => 1,
                'amount' => 46.4,
            ],
        ],
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "message": "Listo.",
  "documents": [
    {
      "id": 42,
      "type": "…",
      "folio": "A-1043",
      "total": "232.000000",
      "balance": "232.000000",
      "print_url": "https://demo.nori.software/…"
    }
  ],
  "payment": {},
  "visit": {},
  "replayed": false
}

Presale order delivered on the next visit

POST /route/visits/{visit}/order Idempotency-Key

Path parameters

Name Type Req. Description
visit integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Request body

Name Type Req. Description
lines array<object> yes
lines[].item_id integer yes
lines[].quantity number yes
lines[].uom_id integer no
delivery_date string<date> no

Responses

  • 200 Documents and payment made at the stop (not wrapped)
curl -X POST https://demo.nori.software/api/v1/route/visits/42/order \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "lines": [
      {
        "item_id": 42,
        "quantity": 1.5,
        "uom_id": 42
      }
    ],
    "delivery_date": "2026-11-03"
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/order', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lines": [
      {
        "item_id": 42,
        "quantity": 1.5,
        "uom_id": 42
      }
    ],
    "delivery_date": "2026-11-03"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/visits/42/order', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'lines' => [
            [
                'item_id' => 42,
                'quantity' => 1.5,
                'uom_id' => 42,
            ],
        ],
        'delivery_date' => '2026-11-03',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "message": "Listo.",
  "documents": [
    {
      "id": 42,
      "type": "…",
      "folio": "A-1043",
      "total": "232.000000",
      "balance": "232.000000",
      "print_url": "https://demo.nori.software/…"
    }
  ],
  "payment": {},
  "visit": {},
  "replayed": false
}

Deliver an order from the truck (partial allowed), optionally collecting

POST /route/visits/{visit}/delivery Idempotency-Key

Path parameters

Name Type Req. Description
visit integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Request body

Name Type Req. Description
lines array<object> yes
lines[].base_line_id integer no
lines[].quantity number no
tenders array<object> no
tenders[].tender_type_id integer yes
tenders[].amount number yes
tenders[].reference string no
tenders[].credit_memo_id integer no

Responses

  • 200 Documents and payment made at the stop (not wrapped)
curl -X POST https://demo.nori.software/api/v1/route/visits/42/delivery \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "lines": [
      {
        "base_line_id": 42,
        "quantity": 1.5
      }
    ],
    "tenders": [
      {
        "tender_type_id": 42,
        "amount": 1.5,
        "reference": "A-1043",
        "credit_memo_id": 42
      }
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/delivery', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lines": [
      {
        "base_line_id": 42,
        "quantity": 1.5
      }
    ],
    "tenders": [
      {
        "tender_type_id": 42,
        "amount": 1.5,
        "reference": "A-1043",
        "credit_memo_id": 42
      }
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/visits/42/delivery', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'lines' => [
            [
                'base_line_id' => 42,
                'quantity' => 1.5,
            ],
        ],
        'tenders' => [
            [
                'tender_type_id' => 42,
                'amount' => 1.5,
                'reference' => 'A-1043',
                'credit_memo_id' => 42,
            ],
        ],
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "message": "Listo.",
  "documents": [
    {
      "id": 42,
      "type": "…",
      "folio": "A-1043",
      "total": "232.000000",
      "balance": "232.000000",
      "print_url": "https://demo.nori.software/…"
    }
  ],
  "payment": {},
  "visit": {},
  "replayed": false
}

Pick up products (expired, quality) into the returns compartment

POST /route/visits/{visit}/pickup Idempotency-Key

Path parameters

Name Type Req. Description
visit integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Request body

Name Type Req. Description
lines array<object> yes
lines[].item_id integer no
lines[].quantity number no
lines[].reason_id integer no

Responses

  • 200 Documents and payment made at the stop (not wrapped)
curl -X POST https://demo.nori.software/api/v1/route/visits/42/pickup \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "lines": [
      {
        "item_id": 42,
        "quantity": 1.5,
        "reason_id": 42
      }
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/pickup', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lines": [
      {
        "item_id": 42,
        "quantity": 1.5,
        "reason_id": 42
      }
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/visits/42/pickup', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'lines' => [
            [
                'item_id' => 42,
                'quantity' => 1.5,
                'reason_id' => 42,
            ],
        ],
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "message": "Listo.",
  "documents": [
    {
      "id": 42,
      "type": "…",
      "folio": "A-1043",
      "total": "232.000000",
      "balance": "232.000000",
      "print_url": "https://demo.nori.software/…"
    }
  ],
  "payment": {},
  "visit": {},
  "replayed": false
}

Collect (chosen documents or the oldest)

POST /route/visits/{visit}/collection Idempotency-Key

Path parameters

Name Type Req. Description
visit integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Request body

Name Type Req. Description
tenders array<object> yes
tenders[].tender_type_id integer yes
tenders[].amount number yes
tenders[].reference string no
tenders[].credit_memo_id integer no
document_ids array<integer> no

Responses

  • 200 Documents and payment made at the stop (not wrapped)
curl -X POST https://demo.nori.software/api/v1/route/visits/42/collection \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "tenders": [
      {
        "tender_type_id": 42,
        "amount": 1.5,
        "reference": "A-1043",
        "credit_memo_id": 42
      }
    ],
    "document_ids": [
      42
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/collection', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "tenders": [
      {
        "tender_type_id": 42,
        "amount": 1.5,
        "reference": "A-1043",
        "credit_memo_id": 42
      }
    ],
    "document_ids": [
      42
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/visits/42/collection', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'tenders' => [
            [
                'tender_type_id' => 42,
                'amount' => 1.5,
                'reference' => 'A-1043',
                'credit_memo_id' => 42,
            ],
        ],
        'document_ids' => [
            42,
        ],
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "message": "Listo.",
  "documents": [
    {
      "id": 42,
      "type": "…",
      "folio": "A-1043",
      "total": "232.000000",
      "balance": "232.000000",
      "print_url": "https://demo.nori.software/…"
    }
  ],
  "payment": {},
  "visit": {},
  "replayed": false
}

Hand over a home delivery scheduled on the stop (JSON, or multipart with the photo)

POST /route/visits/{visit}/shipments/{shipment}/delivered Idempotency-Key

Cash on delivery with a balance needs tenders; they are collected through the route's collection first. A reserve invoice shipped whole is delivered (stock moves) at this point.

Path parameters

Name Type Req. Description
visit integer yes
shipment integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Request body · multipart/form-data

Name Type Req. Description
received_by string yes
notes string no
lat number no
lng number no
photo string<binary> no
tenders array<object> no
tenders[].tender_type_id integer yes
tenders[].amount number yes
tenders[].reference string no
tenders[].credit_memo_id integer no

Responses

  • 200 Visit (not wrapped)
curl -X POST https://demo.nori.software/api/v1/route/visits/42/shipments/42/delivered \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "received_by=…" \
  -F "notes=…" \
  -F "lat=…" \
  -F "lng=…" \
  -F "photo=@foto.jpg" \
  -F "tenders=…" \
  -F "tenders[].tender_type_id=…" \
  -F "tenders[].amount=…" \
  -F "tenders[].reference=…" \
  -F "tenders[].credit_memo_id=…"
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/shipments/42/delivered', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: formData,
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/visits/42/shipments/42/delivered', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'multipart' => [/* … */],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "visit": {},
  "replayed": false
}

A home delivery that could not be handed over, and why (back to the logistics board)

POST /route/visits/{visit}/shipments/{shipment}/failed Idempotency-Key

Path parameters

Name Type Req. Description
visit integer yes
shipment integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Request body

Name Type Req. Description
reason string yes
notes string no

Responses

  • 200 Visit (not wrapped)
curl -X POST https://demo.nori.software/api/v1/route/visits/42/shipments/42/failed \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "…",
    "notes": "…"
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/shipments/42/failed', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "reason": "…",
    "notes": "…"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/visits/42/shipments/42/failed', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'reason' => '…',
        'notes' => '…',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "visit": {},
  "replayed": false
}

Cooler/display service with photos (multipart)

POST /route/visits/{visit}/asset-service Idempotency-Key

Path parameters

Name Type Req. Description
visit integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Request body · multipart/form-data

Name Type Req. Description
customer_asset_id integer yes
kind string yes Values: preventivecorrectiveinstallationremovalinspection
condition string yes Values: workingneeds_repairout_of_service
temperature number no
notes string no
performed_at string<date-time> no
photos[] array<string<binary>> no

Responses

  • 200 Visit (not wrapped)
curl -X POST https://demo.nori.software/api/v1/route/visits/42/asset-service \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "customer_asset_id=…" \
  -F "kind=…" \
  -F "condition=…" \
  -F "temperature=…" \
  -F "notes=…" \
  -F "performed_at=…" \
  -F "photos[]=@foto.jpg"
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/asset-service', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: formData,
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/visits/42/asset-service', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'multipart' => [/* … */],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "visit": {},
  "replayed": false
}

Photo of the visit (multipart)

POST /route/visits/{visit}/photos Idempotency-Key

Path parameters

Name Type Req. Description
visit integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> no Optional on route endpoints; also accepted as the "key" field.

Request body · multipart/form-data

Name Type Req. Description
photo string<binary> yes
kind string no
caption string no
customer_asset_id integer no

Responses

  • 200 Visit (not wrapped)
curl -X POST https://demo.nori.software/api/v1/route/visits/42/photos \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "photo=@foto.jpg" \
  -F "kind=…" \
  -F "caption=…" \
  -F "customer_asset_id=…"
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/photos', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: formData,
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/visits/42/photos', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'multipart' => [/* … */],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "visit": {},
  "replayed": false
}

Route offline queue (actions by name; /sync/push is the general one)

POST /route/sync

Request body

Name Type Req. Description
operations array<object> yes
operations[].key string<uuid> yes
operations[].action string yes Values: startfinishcheck_incheck_outskipno_salesaleorderdeliverypickupcollectionshipment_deliveredshipment_failed
operations[].visit_id integer no
operations[].shipment_id integer no shipment_delivered / shipment_failed (without photo)
operations[].plan_id integer no
operations[].data object no

Responses

  • 200 Results
curl -X POST https://demo.nori.software/api/v1/route/sync \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "operations": [
      {
        "key": "3f2b8c1e-7a4d-4f0e-9b8a-1c2d3e4f5a6b",
        "action": "start",
        "visit_id": 42,
        "shipment_id": 42,
        "plan_id": 42,
        "data": {}
      }
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/sync', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "operations": [
      {
        "key": "3f2b8c1e-7a4d-4f0e-9b8a-1c2d3e4f5a6b",
        "action": "start",
        "visit_id": 42,
        "shipment_id": 42,
        "plan_id": 42,
        "data": {}
      }
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('route/sync', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
    'json' => [
        'operations' => [
            [
                'key' => '3f2b8c1e-7a4d-4f0e-9b8a-1c2d3e4f5a6b',
                'action' => 'start',
                'visit_id' => 42,
                'shipment_id' => 42,
                'plan_id' => 42,
                'data' => [],
            ],
        ],
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "results": [
    {}
  ]
}

Reference

Supervisor

Sales, collections and receivables indicators for the day and the month, by sales rep and branch.

Sales today and month vs previous, collections, receivables, overdue, aging, top items, per seller and branch

GET /kpis

Sections the user may not see come as null. A seller without approvals.grant only sees their own sales.

Query parameters

Name Type Req. Description
branch_id integer no
sales_employee_id integer no
top integer no Default: 10

Responses

  • 200 KPIs
curl https://demo.nori.software/api/v1/kpis \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/kpis', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('kpis', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "sales_today": {
      "value": 500,
      "previous": 320,
      "change_percent": 56.3,
      "format": "money"
    },
    "top_items": [
      {
        "label": "ROSA · Rosa roja",
        "value": 500
      }
    ]
  },
  "meta": {
    "generated_at": "2026-11-03T10:15:00-06:00"
  }
}

Reference

Sync

Download catalogs for offline work as incremental changes and send the queue of pending operations.

Entities the user may keep offline

GET /sync

Responses

  • 200 Manifest
curl https://demo.nori.software/api/v1/sync \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/sync', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('sync', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "entities": [
      "items"
    ],
    "page_size": 1,
    "tombstone_days": 1,
    "server_time": "2026-11-03T10:15:00-06:00"
  }
}

A page of changes of a catalog since a sync token

GET /sync/{entity}

Without updated_since (or with one older than the tombstones kept) meta.reset is true: clear the local table and take everything. Follow meta.next_cursor while meta.has_more; then store meta.sync_token for the next delta. Apply deleted (ids) before data (upserts).

Path parameters

Name Type Req. Description
entity string yes Values: itemsitem_pricesstockcustomersprice_liststender_typeswarehousescausalitiesvisit_reasons

Query parameters

Name Type Req. Description
updated_since string<date-time> no The sync_token of the last complete sync
cursor string no
limit integer no Default: 500 Maximum: 2000
scope string no customers: only mine Values: allmine
warehouse_ids[] array<integer> no stock: these warehouses

Responses

  • 200 Changes
  • 403 Error envelope
  • 404 Error envelope
curl https://demo.nori.software/api/v1/sync/items \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/sync/items', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('sync/items', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "id": 5,
      "code": "ROSA",
      "name": "Rosa roja",
      "is_active": true,
      "updated_at": "2026-11-03T09:58:12-06:00"
    }
  ],
  "deleted": [
    17
  ],
  "meta": {
    "entity": "items",
    "reset": false,
    "has_more": false,
    "next_cursor": null,
    "sync_token": "2026-11-03T10:00:00-06:00",
    "server_time": "2026-11-03T10:00:00-06:00"
  }
}

Send the offline queue (writes of this API, in order)

POST /sync/push

Each operation runs through the router with its key as Idempotency-Key. A failing one does not stop the rest. A string "$ref:<key>:<path>" in a body takes that value from an earlier answer.

Request body

Name Type Req. Description
operations array<object> yes
operations[].key string<uuid> yes
operations[].method string yes Values: POSTPUTPATCHDELETE
operations[].path string yes
operations[].body object no

Responses

  • 200 Result per operation
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/sync/push \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "operations": [
      {
        "key": "6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001",
        "method": "POST",
        "path": "/api/v1/customers",
        "body": {
          "name": "Tienda nueva"
        }
      },
      {
        "key": "6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0002",
        "method": "POST",
        "path": "/api/v1/documents",
        "body": {
          "type": "sales_order",
          "business_partner_id": "$ref:6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001:data.id",
          "lines": [
            {
              "item_id": 5,
              "quantity": 1
            }
          ]
        }
      }
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/sync/push', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "operations": [
      {
        "key": "6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001",
        "method": "POST",
        "path": "/api/v1/customers",
        "body": {
          "name": "Tienda nueva"
        }
      },
      {
        "key": "6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0002",
        "method": "POST",
        "path": "/api/v1/documents",
        "body": {
          "type": "sales_order",
          "business_partner_id": "$ref:6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001:data.id",
          "lines": [
            {
              "item_id": 5,
              "quantity": 1
            }
          ]
        }
      }
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('sync/push', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
    'json' => [
        'operations' => [
            [
                'key' => '6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001',
                'method' => 'POST',
                'path' => '/api/v1/customers',
                'body' => [
                    'name' => 'Tienda nueva',
                ],
            ],
            [
                'key' => '6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0002',
                'method' => 'POST',
                'path' => '/api/v1/documents',
                'body' => [
                    'type' => 'sales_order',
                    'business_partner_id' => '$ref:6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001:data.id',
                    'lines' => [
                        [
                            'item_id' => 5,
                            'quantity' => 1,
                        ],
                    ],
                ],
            ],
        ],
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "key": "3f2b8c1e-7a4d-4f0e-9b8a-1c2d3e4f5a6b",
      "status": 1,
      "ok": true,
      "replayed": false,
      "body": {}
    }
  ],
  "meta": {
    "ok": 1,
    "failed": 1,
    "server_time": "…"
  }
}

Reference

Assistant

Ask the AI assistant and read your conversations and suggested questions.

Ask the AI assistant (a new conversation without conversation_id)

POST /assistant/messages Idempotency-Key

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
conversation_id integer no
message string yes

Responses

  • 201 Answer
  • 403 Error envelope
curl -X POST https://demo.nori.software/api/v1/assistant/messages \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "¿Cuánto vendí este mes?"
  }'
const response = await fetch('https://demo.nori.software/api/v1/assistant/messages', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "message": "¿Cuánto vendí este mes?"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('assistant/messages', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'message' => '¿Cuánto vendí este mes?',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 201
{
  "data": {
    "conversation_id": 42,
    "message": {
      "id": 42,
      "text": "Listo.",
      "html": "…"
    }
  }
}

My last 50 conversations

GET /assistant/conversations

Responses

  • 200 Conversations
curl https://demo.nori.software/api/v1/assistant/conversations \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/assistant/conversations', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('assistant/conversations', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {}
  ]
}

Messages of a conversation

GET /assistant/conversations/{conversation}/messages

Path parameters

Name Type Req. Description
conversation integer yes

Responses

  • 200 Messages
curl https://demo.nori.software/api/v1/assistant/conversations/42/messages \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/assistant/conversations/42/messages', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('assistant/conversations/42/messages', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {}
  ]
}

Suggested questions

GET /assistant/templates

Responses

  • 200 Templates
curl https://demo.nori.software/api/v1/assistant/templates \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/assistant/templates', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('assistant/templates', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {}
  ]
}

Reference

Authorizations

Request a supervisor's approval, approve on site or decide pending requests.

Requests the user can decide

GET /authorizations/pending

Responses

  • 200 OK
  • 401 Error envelope
curl https://demo.nori.software/api/v1/authorizations/pending \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/authorizations/pending', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('authorizations/pending', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{}

The user's own requests

GET /authorizations/mine

Responses

  • 200 OK
  • 401 Error envelope
curl https://demo.nori.software/api/v1/authorizations/mine \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/authorizations/mine', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('authorizations/mine', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{}

Ask a supervisor, with the token of an authorization_required error

POST /authorizations Idempotency-Key

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Responses

  • 200 OK
  • 401 Error envelope
curl -X POST https://demo.nori.software/api/v1/authorizations \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
const response = await fetch('https://demo.nori.software/api/v1/authorizations', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('authorizations', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{}

Approve on site with supervisor credentials or PIN

POST /authorizations/on-site Idempotency-Key

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Responses

  • 200 OK
  • 401 Error envelope
curl -X POST https://demo.nori.software/api/v1/authorizations/on-site \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
const response = await fetch('https://demo.nori.software/api/v1/authorizations/on-site', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('authorizations/on-site', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{}

Status of a request

GET /authorizations/{authorization}

Path parameters

Name Type Req. Description
authorization integer yes

Responses

  • 200 OK
  • 401 Error envelope
curl https://demo.nori.software/api/v1/authorizations/42 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/authorizations/42', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('authorizations/42', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{}

Approve or reject a request

POST /authorizations/{authorization}/decide Idempotency-Key

Path parameters

Name Type Req. Description
authorization integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Responses

  • 200 OK
  • 401 Error envelope
curl -X POST https://demo.nori.software/api/v1/authorizations/42/decide \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
const response = await fetch('https://demo.nori.software/api/v1/authorizations/42/decide', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('authorizations/42/decide', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{}

Reference

CRM

Work your deals: pipelines and stages with their totals, create and edit, move between stages, win or lose with a reason, activities, follow-ups, your agenda and linked documents.

Pipelines with their stages

GET /crm/pipelines

Active pipelines, the default one first, each with its stages in board order. outcome says whether a stage is open, won or lost.

Responses

  • 200 Pipelines
  • 403 Error envelope
curl https://demo.nori.software/api/v1/crm/pipelines \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/crm/pipelines', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('crm/pipelines', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "id": 1,
      "name": "Ventas",
      "is_default": true,
      "idle_days": 14,
      "stages": [
        {
          "id": 1,
          "name": "Prospecto",
          "color": "#64748b",
          "probability": "10.00",
          "outcome": "open",
          "sort_order": 0,
          "auto_on": null
        },
        {
          "id": 4,
          "name": "Cotización enviada",
          "color": "#f59e0b",
          "probability": "50.00",
          "outcome": "open",
          "sort_order": 3,
          "auto_on": "quotation"
        },
        {
          "id": 6,
          "name": "Ganado",
          "color": "#16a34a",
          "probability": "100.00",
          "outcome": "won",
          "sort_order": 5,
          "auto_on": null
        },
        {
          "id": 7,
          "name": "Perdido",
          "color": "#dc2626",
          "probability": "0.00",
          "outcome": "lost",
          "sort_order": 6,
          "auto_on": null
        }
      ]
    }
  ]
}

Count, amount and weighted amount per stage

GET /crm/pipelines/{pipeline}/totals

Same totals and KPIs as the web board, with its filters and the user's visibility. Open stages count open deals; the won and lost stages count what closed this month.

Path parameters

Name Type Req. Description
pipeline integer yes

Query parameters

Name Type Req. Description
scope string no mine: only the deals the user owns; all needs crm_deals.view_all Values: mineall
search string no
tag_id integer no
partner_id integer no
overdue boolean no Only deals with an overdue follow-up

Responses

  • 200 Totals per stage and KPIs
  • 403 Error envelope
  • 404 Error envelope
curl https://demo.nori.software/api/v1/crm/pipelines/42/totals?search=manzana \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/crm/pipelines/42/totals?search=manzana', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('crm/pipelines/42/totals', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
    'query' => [
        'search' => 'manzana',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "pipeline_id": 1,
    "stages": [
      {
        "stage_id": 1,
        "count": 4,
        "amount": "18500.00",
        "weighted_amount": "1850.00"
      },
      {
        "stage_id": 4,
        "count": 2,
        "amount": "32000.00",
        "weighted_amount": "16000.00"
      }
    ],
    "kpis": {
      "open_count": 6,
      "open_amount": "50500.00",
      "weighted_amount": "17850.00",
      "won_count": 3,
      "won_amount": "41200.00"
    }
  }
}

Active loss reasons

GET /crm/loss-reasons

Responses

  • 200 Loss reasons
  • 403 Error envelope
curl https://demo.nori.software/api/v1/crm/loss-reasons \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/crm/loss-reasons', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('crm/loss-reasons', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "id": 1,
      "name": "Precio"
    },
    {
      "id": 2,
      "name": "Competencia"
    },
    {
      "id": 3,
      "name": "Sin presupuesto"
    }
  ]
}

Deal tags

GET /crm/tags

Responses

  • 200 Tags
  • 403 Error envelope
curl https://demo.nori.software/api/v1/crm/tags \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/crm/tags', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('crm/tags', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "id": 1,
      "name": "Distribuidor",
      "color": "#2563eb"
    }
  ]
}

Deals the user may see

GET /crm/deals

Without crm_deals.view_all only the user's own deals (owner, or of their sales employee). For offline work, keep meta.server_time and send it as updated_since next time: you get what changed since then (closed deals included) and meta.deleted with the ids deleted since then.

Query parameters

Name Type Req. Description
scope string no mine: only the deals the user owns; all needs crm_deals.view_all (403 otherwise). Default: whatever the user may see Values: mineall
status string no Default: every status Values: openwonlost
pipeline_id integer no
stage_id integer no
partner_id integer no
tag_id integer no
owner_id integer no Only with crm_deals.view_all
overdue boolean no Only deals with an overdue follow-up
search string no Title, customer name, trade name or code
updated_since string<date-time> no meta.server_time of the previous pull (inclusive)
per_page integer no Default: 50 Maximum: 200
cursor string no meta.next_cursor of the previous page

Responses

  • 200 Deals
  • 403 Error envelope
  • 422 Error envelope
curl https://demo.nori.software/api/v1/crm/deals?search=manzana&per_page=50 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/crm/deals?search=manzana&per_page=50', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('crm/deals', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
    'query' => [
        'search' => 'manzana',
        'per_page' => 50,
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "links": {},
  "meta": {
    "server_time": "2026-11-03T10:15:00-06:00",
    "deleted": [
      1
    ]
  },
  "data": [
    {
      "id": 31,
      "title": "Surtido de temporada",
      "status": "open",
      "pipeline_id": 1,
      "stage_id": 4,
      "stage": {
        "id": 4,
        "name": "Cotización enviada",
        "color": "#f59e0b",
        "probability": "50.00",
        "outcome": "open"
      },
      "business_partner_id": 12,
      "partner": {
        "id": 12,
        "code": "C000012",
        "name": "ABARROTES LUPITA SA DE CV",
        "trade_name": "Abarrotes Lupita",
        "type": "customer",
        "phone": "3312345678",
        "mobile": "3398765432",
        "email": "compras@lupita.mx",
        "latitude": 20.67,
        "longitude": -103.35
      },
      "owner": {
        "id": 5,
        "name": "Ana Torres"
      },
      "amount": "25000.000000",
      "weighted_amount": "12500.00",
      "currency": "MXN",
      "expected_close_date": "2026-12-15",
      "close_overdue": false,
      "tags": [
        {
          "id": 1,
          "name": "Distribuidor",
          "color": "#2563eb"
        }
      ],
      "next_activity": {
        "id": 88,
        "type": "call",
        "subject": "Confirmar pedido",
        "due_at": "2026-11-04T10:00:00-06:00",
        "overdue": false
      },
      "pending_count": 1,
      "overdue_count": 0,
      "idle": false,
      "days_in_stage": 2,
      "updated_at": "2026-11-02T17:40:11-06:00"
    }
  ]
}

Create a deal

POST /crm/deals Idempotency-Key

Without pipeline it goes to the default one; without stage, to its first open stage. The title defaults to the customer's name and the owner to the user (only someone with crm_deals.view_all gives a deal to somebody else). Automations of "deal created" run as on the web.

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
business_partner_id integer no Required on create
title string no Default: the customer name
crm_pipeline_id integer no Only on create. Default: the default pipeline
crm_stage_id integer no Only on create, a stage of that pipeline. Default: its first open stage
partner_contact_id integer | null no
owner_id integer | null no Only with crm_deals.view_all; otherwise the user
amount number no
currency_id integer no
probability number | null no 0 to 100; null = the stage's
expected_close_date string<date> | null no
source string no
description string no
tag_ids array<integer> no Replaces the tags

Responses

  • 201 Created
  • 403 Error envelope
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/crm/deals \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "business_partner_id": 12,
    "title": "Surtido de temporada",
    "amount": 25000,
    "expected_close_date": "2026-12-15",
    "tag_ids": [
      1
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/crm/deals', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "business_partner_id": 12,
    "title": "Surtido de temporada",
    "amount": 25000,
    "expected_close_date": "2026-12-15",
    "tag_ids": [
      1
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('crm/deals', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'business_partner_id' => 12,
        'title' => 'Surtido de temporada',
        'amount' => 25000,
        'expected_close_date' => '2026-12-15',
        'tag_ids' => [
            1,
        ],
    ],
]);
$data = json_decode($response->getBody(), true);
Response 201
{
  "data": {
    "id": 42,
    "title": "…",
    "status": "open",
    "pipeline_id": 42,
    "stage_id": 42,
    "stage": {
      "id": 42,
      "name": "Rosa roja",
      "color": "…",
      "probability": "…",
      "outcome": "…"
    },
    "business_partner_id": 42,
    "partner": {
      "id": 42,
      "code": "ROSA",
      "name": "Abarrotes Don Pepe",
      "trade_name": "…",
      "type": "…",
      "phone": "3312345678",
      "mobile": "3312345678",
      "email": "ana@example.com",
      "latitude": 1.5,
      "longitude": 1.5
    },
    "partner_contact_id": 42,
    "owner": {
      "id": 42,
      "name": "Rosa roja"
    },
    "sales_employee_id": 42,
    "amount": "232.000000",
    "weighted_amount": "232.000000",
    "currency": "MXN",
    "currency_id": 42,
    "probability": "…",
    "effective_probability": "…",
    "expected_close_date": "2026-11-03",
    "close_overdue": false,
    "source": "…",
    "description": "…",
    "tags": [
      {
        "id": 42,
        "name": "Rosa roja",
        "color": "#2563eb"
      }
    ],
    "next_activity": {
      "id": 42,
      "type": "…",
      "subject": "…",
      "due_at": "2026-11-03T10:15:00-06:00",
      "overdue": false
    },
    "pending_count": 1,
    "overdue_count": 1,
    "idle": false,
    "days_in_stage": 1,
    "loss_reason": {
      "id": 42,
      "name": "Rosa roja"
    },
    "loss_notes": "…",
    "closed_at": "2026-11-03T10:15:00-06:00",
    "last_activity_at": "2026-11-03T10:15:00-06:00",
    "created_at": "2026-11-03T10:15:00-06:00",
    "updated_at": "2026-11-03T10:15:00-06:00",
    "contact": {
      "id": 42,
      "name": "Rosa roja",
      "position": "…",
      "email": "ana@example.com",
      "phone": "3312345678",
      "mobile": "3312345678"
    },
    "pipeline": {
      "id": 42,
      "name": "Rosa roja",
      "stages": [
        {
          "id": null,
          "name": null,
          "color": "#f59e0b",
          "probability": "232.000000",
          "outcome": "open",
          "sort_order": null,
          "auto_on": "quotation"
        }
      ]
    },
    "documents": [
      {
        "id": 42,
        "type": "quotation",
        "status": "…",
        "folio": "A-1043",
        "document_date": "2026-11-03",
        "total": "232.000000"
      }
    ],
    "timeline": {
      "pending": [
        {
          "id": null,
          "deal_id": null,
          "business_partner_id": null,
          "type": "note",
          "subject": null,
          "body": null,
          "occurred_at": null,
          "due_at": null,
          "remind_at": null,
          "completed_at": null,
          "is_follow_up": null,
          "pending": null,
          "overdue": null,
          "editable": null,
          "assigned_to": null,
          "created_by": null,
          "document": null,
          "updated_at": null
        }
      ],
      "history": [
        {
          "id": null,
          "deal_id": null,
          "business_partner_id": null,
          "type": "note",
          "subject": null,
          "body": null,
          "occurred_at": null,
          "due_at": null,
          "remind_at": null,
          "completed_at": null,
          "is_follow_up": null,
          "pending": null,
          "overdue": null,
          "editable": null,
          "assigned_to": null,
          "created_by": null,
          "document": null,
          "updated_at": null
        }
      ]
    }
  }
}

Deal with its stages, documents and timeline

GET /crm/deals/{deal}

Path parameters

Name Type Req. Description
deal integer yes

Responses

  • 200 Deal
  • 403 Error envelope
  • 404 Error envelope
curl https://demo.nori.software/api/v1/crm/deals/42 \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/crm/deals/42', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('crm/deals/42', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "id": 42,
    "title": "…",
    "status": "open",
    "pipeline_id": 42,
    "stage_id": 42,
    "stage": {
      "id": 42,
      "name": "Rosa roja",
      "color": "…",
      "probability": "…",
      "outcome": "…"
    },
    "business_partner_id": 42,
    "partner": {
      "id": 42,
      "code": "ROSA",
      "name": "Abarrotes Don Pepe",
      "trade_name": "…",
      "type": "…",
      "phone": "3312345678",
      "mobile": "3312345678",
      "email": "ana@example.com",
      "latitude": 1.5,
      "longitude": 1.5
    },
    "partner_contact_id": 42,
    "owner": {
      "id": 42,
      "name": "Rosa roja"
    },
    "sales_employee_id": 42,
    "amount": "232.000000",
    "weighted_amount": "232.000000",
    "currency": "MXN",
    "currency_id": 42,
    "probability": "…",
    "effective_probability": "…",
    "expected_close_date": "2026-11-03",
    "close_overdue": false,
    "source": "…",
    "description": "…",
    "tags": [
      {
        "id": 42,
        "name": "Rosa roja",
        "color": "#2563eb"
      }
    ],
    "next_activity": {
      "id": 42,
      "type": "…",
      "subject": "…",
      "due_at": "2026-11-03T10:15:00-06:00",
      "overdue": false
    },
    "pending_count": 1,
    "overdue_count": 1,
    "idle": false,
    "days_in_stage": 1,
    "loss_reason": {
      "id": 42,
      "name": "Rosa roja"
    },
    "loss_notes": "…",
    "closed_at": "2026-11-03T10:15:00-06:00",
    "last_activity_at": "2026-11-03T10:15:00-06:00",
    "created_at": "2026-11-03T10:15:00-06:00",
    "updated_at": "2026-11-03T10:15:00-06:00",
    "contact": {
      "id": 42,
      "name": "Rosa roja",
      "position": "…",
      "email": "ana@example.com",
      "phone": "3312345678",
      "mobile": "3312345678"
    },
    "pipeline": {
      "id": 42,
      "name": "Rosa roja",
      "stages": [
        {
          "id": null,
          "name": null,
          "color": "#f59e0b",
          "probability": "232.000000",
          "outcome": "open",
          "sort_order": null,
          "auto_on": "quotation"
        }
      ]
    },
    "documents": [
      {
        "id": 42,
        "type": "quotation",
        "status": "…",
        "folio": "A-1043",
        "document_date": "2026-11-03",
        "total": "232.000000"
      }
    ],
    "timeline": {
      "pending": [
        {
          "id": null,
          "deal_id": null,
          "business_partner_id": null,
          "type": "note",
          "subject": null,
          "body": null,
          "occurred_at": null,
          "due_at": null,
          "remind_at": null,
          "completed_at": null,
          "is_follow_up": null,
          "pending": null,
          "overdue": null,
          "editable": null,
          "assigned_to": null,
          "created_by": null,
          "document": null,
          "updated_at": null
        }
      ],
      "history": [
        {
          "id": null,
          "deal_id": null,
          "business_partner_id": null,
          "type": "note",
          "subject": null,
          "body": null,
          "occurred_at": null,
          "due_at": null,
          "remind_at": null,
          "completed_at": null,
          "is_follow_up": null,
          "pending": null,
          "overdue": null,
          "editable": null,
          "assigned_to": null,
          "created_by": null,
          "document": null,
          "updated_at": null
        }
      ]
    }
  }
}

Edit a deal

PATCH /crm/deals/{deal} Idempotency-Key

Only the fields sent change. tag_ids replaces the tags. Pipeline and stage change with /move.

Path parameters

Name Type Req. Description
deal integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
business_partner_id integer no Required on create
title string no Default: the customer name
crm_pipeline_id integer no Only on create. Default: the default pipeline
crm_stage_id integer no Only on create, a stage of that pipeline. Default: its first open stage
partner_contact_id integer | null no
owner_id integer | null no Only with crm_deals.view_all; otherwise the user
amount number no
currency_id integer no
probability number | null no 0 to 100; null = the stage's
expected_close_date string<date> | null no
source string no
description string no
tag_ids array<integer> no Replaces the tags

Responses

  • 200 Updated
  • 403 Error envelope
  • 422 Error envelope
curl -X PATCH https://demo.nori.software/api/v1/crm/deals/42 \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 31000,
    "expected_close_date": "2026-12-20",
    "partner_contact_id": 7
  }'
const response = await fetch('https://demo.nori.software/api/v1/crm/deals/42', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "amount": 31000,
    "expected_close_date": "2026-12-20",
    "partner_contact_id": 7
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->patch('crm/deals/42', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'amount' => 31000,
        'expected_close_date' => '2026-12-20',
        'partner_contact_id' => 7,
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "id": 42,
    "title": "…",
    "status": "open",
    "pipeline_id": 42,
    "stage_id": 42,
    "stage": {
      "id": 42,
      "name": "Rosa roja",
      "color": "…",
      "probability": "…",
      "outcome": "…"
    },
    "business_partner_id": 42,
    "partner": {
      "id": 42,
      "code": "ROSA",
      "name": "Abarrotes Don Pepe",
      "trade_name": "…",
      "type": "…",
      "phone": "3312345678",
      "mobile": "3312345678",
      "email": "ana@example.com",
      "latitude": 1.5,
      "longitude": 1.5
    },
    "partner_contact_id": 42,
    "owner": {
      "id": 42,
      "name": "Rosa roja"
    },
    "sales_employee_id": 42,
    "amount": "232.000000",
    "weighted_amount": "232.000000",
    "currency": "MXN",
    "currency_id": 42,
    "probability": "…",
    "effective_probability": "…",
    "expected_close_date": "2026-11-03",
    "close_overdue": false,
    "source": "…",
    "description": "…",
    "tags": [
      {
        "id": 42,
        "name": "Rosa roja",
        "color": "#2563eb"
      }
    ],
    "next_activity": {
      "id": 42,
      "type": "…",
      "subject": "…",
      "due_at": "2026-11-03T10:15:00-06:00",
      "overdue": false
    },
    "pending_count": 1,
    "overdue_count": 1,
    "idle": false,
    "days_in_stage": 1,
    "loss_reason": {
      "id": 42,
      "name": "Rosa roja"
    },
    "loss_notes": "…",
    "closed_at": "2026-11-03T10:15:00-06:00",
    "last_activity_at": "2026-11-03T10:15:00-06:00",
    "created_at": "2026-11-03T10:15:00-06:00",
    "updated_at": "2026-11-03T10:15:00-06:00",
    "contact": {
      "id": 42,
      "name": "Rosa roja",
      "position": "…",
      "email": "ana@example.com",
      "phone": "3312345678",
      "mobile": "3312345678"
    },
    "pipeline": {
      "id": 42,
      "name": "Rosa roja",
      "stages": [
        {
          "id": null,
          "name": null,
          "color": "#f59e0b",
          "probability": "232.000000",
          "outcome": "open",
          "sort_order": null,
          "auto_on": "quotation"
        }
      ]
    },
    "documents": [
      {
        "id": 42,
        "type": "quotation",
        "status": "…",
        "folio": "A-1043",
        "document_date": "2026-11-03",
        "total": "232.000000"
      }
    ],
    "timeline": {
      "pending": [
        {
          "id": null,
          "deal_id": null,
          "business_partner_id": null,
          "type": "note",
          "subject": null,
          "body": null,
          "occurred_at": null,
          "due_at": null,
          "remind_at": null,
          "completed_at": null,
          "is_follow_up": null,
          "pending": null,
          "overdue": null,
          "editable": null,
          "assigned_to": null,
          "created_by": null,
          "document": null,
          "updated_at": null
        }
      ],
      "history": [
        {
          "id": null,
          "deal_id": null,
          "business_partner_id": null,
          "type": "note",
          "subject": null,
          "body": null,
          "occurred_at": null,
          "due_at": null,
          "remind_at": null,
          "completed_at": null,
          "is_follow_up": null,
          "pending": null,
          "overdue": null,
          "editable": null,
          "assigned_to": null,
          "created_by": null,
          "document": null,
          "updated_at": null
        }
      ]
    }
  }
}

Move a deal to another stage

POST /crm/deals/{deal}/move Idempotency-Key

Any stage, of its pipeline or another one. A won or lost stage closes the deal (lost with its reason); an open stage reopens it. The change is written on the timeline and fires the automations.

Path parameters

Name Type Req. Description
deal integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
stage_id integer yes
comment string no Written with the stage change on the timeline
loss_reason_id integer no When the stage is the lost one
loss_notes string no

Responses

  • 200 Moved
  • 403 Error envelope
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/crm/deals/42/move \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "stage_id": 5,
    "comment": "Pidió precio especial por volumen"
  }'
const response = await fetch('https://demo.nori.software/api/v1/crm/deals/42/move', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "stage_id": 5,
    "comment": "Pidió precio especial por volumen"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('crm/deals/42/move', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'stage_id' => 5,
        'comment' => 'Pidió precio especial por volumen',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "id": 42,
    "title": "…",
    "status": "open",
    "pipeline_id": 42,
    "stage_id": 42,
    "stage": {
      "id": 42,
      "name": "Rosa roja",
      "color": "…",
      "probability": "…",
      "outcome": "…"
    },
    "business_partner_id": 42,
    "partner": {
      "id": 42,
      "code": "ROSA",
      "name": "Abarrotes Don Pepe",
      "trade_name": "…",
      "type": "…",
      "phone": "3312345678",
      "mobile": "3312345678",
      "email": "ana@example.com",
      "latitude": 1.5,
      "longitude": 1.5
    },
    "partner_contact_id": 42,
    "owner": {
      "id": 42,
      "name": "Rosa roja"
    },
    "sales_employee_id": 42,
    "amount": "232.000000",
    "weighted_amount": "232.000000",
    "currency": "MXN",
    "currency_id": 42,
    "probability": "…",
    "effective_probability": "…",
    "expected_close_date": "2026-11-03",
    "close_overdue": false,
    "source": "…",
    "description": "…",
    "tags": [
      {
        "id": 42,
        "name": "Rosa roja",
        "color": "#2563eb"
      }
    ],
    "next_activity": {
      "id": 42,
      "type": "…",
      "subject": "…",
      "due_at": "2026-11-03T10:15:00-06:00",
      "overdue": false
    },
    "pending_count": 1,
    "overdue_count": 1,
    "idle": false,
    "days_in_stage": 1,
    "loss_reason": {
      "id": 42,
      "name": "Rosa roja"
    },
    "loss_notes": "…",
    "closed_at": "2026-11-03T10:15:00-06:00",
    "last_activity_at": "2026-11-03T10:15:00-06:00",
    "created_at": "2026-11-03T10:15:00-06:00",
    "updated_at": "2026-11-03T10:15:00-06:00",
    "contact": {
      "id": 42,
      "name": "Rosa roja",
      "position": "…",
      "email": "ana@example.com",
      "phone": "3312345678",
      "mobile": "3312345678"
    },
    "pipeline": {
      "id": 42,
      "name": "Rosa roja",
      "stages": [
        {
          "id": null,
          "name": null,
          "color": "#f59e0b",
          "probability": "232.000000",
          "outcome": "open",
          "sort_order": null,
          "auto_on": "quotation"
        }
      ]
    },
    "documents": [
      {
        "id": 42,
        "type": "quotation",
        "status": "…",
        "folio": "A-1043",
        "document_date": "2026-11-03",
        "total": "232.000000"
      }
    ],
    "timeline": {
      "pending": [
        {
          "id": null,
          "deal_id": null,
          "business_partner_id": null,
          "type": "note",
          "subject": null,
          "body": null,
          "occurred_at": null,
          "due_at": null,
          "remind_at": null,
          "completed_at": null,
          "is_follow_up": null,
          "pending": null,
          "overdue": null,
          "editable": null,
          "assigned_to": null,
          "created_by": null,
          "document": null,
          "updated_at": null
        }
      ],
      "history": [
        {
          "id": null,
          "deal_id": null,
          "business_partner_id": null,
          "type": "note",
          "subject": null,
          "body": null,
          "occurred_at": null,
          "due_at": null,
          "remind_at": null,
          "completed_at": null,
          "is_follow_up": null,
          "pending": null,
          "overdue": null,
          "editable": null,
          "assigned_to": null,
          "created_by": null,
          "document": null,
          "updated_at": null
        }
      ]
    }
  }
}

Mark the deal as won

POST /crm/deals/{deal}/win Idempotency-Key

Moves it to the won stage of its pipeline.

Path parameters

Name Type Req. Description
deal integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Responses

  • 200 Won
  • 403 Error envelope
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/crm/deals/42/win \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
const response = await fetch('https://demo.nori.software/api/v1/crm/deals/42/win', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('crm/deals/42/win', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "id": 42,
    "title": "…",
    "status": "open",
    "pipeline_id": 42,
    "stage_id": 42,
    "stage": {
      "id": 42,
      "name": "Rosa roja",
      "color": "…",
      "probability": "…",
      "outcome": "…"
    },
    "business_partner_id": 42,
    "partner": {
      "id": 42,
      "code": "ROSA",
      "name": "Abarrotes Don Pepe",
      "trade_name": "…",
      "type": "…",
      "phone": "3312345678",
      "mobile": "3312345678",
      "email": "ana@example.com",
      "latitude": 1.5,
      "longitude": 1.5
    },
    "partner_contact_id": 42,
    "owner": {
      "id": 42,
      "name": "Rosa roja"
    },
    "sales_employee_id": 42,
    "amount": "232.000000",
    "weighted_amount": "232.000000",
    "currency": "MXN",
    "currency_id": 42,
    "probability": "…",
    "effective_probability": "…",
    "expected_close_date": "2026-11-03",
    "close_overdue": false,
    "source": "…",
    "description": "…",
    "tags": [
      {
        "id": 42,
        "name": "Rosa roja",
        "color": "#2563eb"
      }
    ],
    "next_activity": {
      "id": 42,
      "type": "…",
      "subject": "…",
      "due_at": "2026-11-03T10:15:00-06:00",
      "overdue": false
    },
    "pending_count": 1,
    "overdue_count": 1,
    "idle": false,
    "days_in_stage": 1,
    "loss_reason": {
      "id": 42,
      "name": "Rosa roja"
    },
    "loss_notes": "…",
    "closed_at": "2026-11-03T10:15:00-06:00",
    "last_activity_at": "2026-11-03T10:15:00-06:00",
    "created_at": "2026-11-03T10:15:00-06:00",
    "updated_at": "2026-11-03T10:15:00-06:00",
    "contact": {
      "id": 42,
      "name": "Rosa roja",
      "position": "…",
      "email": "ana@example.com",
      "phone": "3312345678",
      "mobile": "3312345678"
    },
    "pipeline": {
      "id": 42,
      "name": "Rosa roja",
      "stages": [
        {
          "id": null,
          "name": null,
          "color": "#f59e0b",
          "probability": "232.000000",
          "outcome": "open",
          "sort_order": null,
          "auto_on": "quotation"
        }
      ]
    },
    "documents": [
      {
        "id": 42,
        "type": "quotation",
        "status": "…",
        "folio": "A-1043",
        "document_date": "2026-11-03",
        "total": "232.000000"
      }
    ],
    "timeline": {
      "pending": [
        {
          "id": null,
          "deal_id": null,
          "business_partner_id": null,
          "type": "note",
          "subject": null,
          "body": null,
          "occurred_at": null,
          "due_at": null,
          "remind_at": null,
          "completed_at": null,
          "is_follow_up": null,
          "pending": null,
          "overdue": null,
          "editable": null,
          "assigned_to": null,
          "created_by": null,
          "document": null,
          "updated_at": null
        }
      ],
      "history": [
        {
          "id": null,
          "deal_id": null,
          "business_partner_id": null,
          "type": "note",
          "subject": null,
          "body": null,
          "occurred_at": null,
          "due_at": null,
          "remind_at": null,
          "completed_at": null,
          "is_follow_up": null,
          "pending": null,
          "overdue": null,
          "editable": null,
          "assigned_to": null,
          "created_by": null,
          "document": null,
          "updated_at": null
        }
      ]
    }
  }
}

Mark the deal as lost, with the reason

POST /crm/deals/{deal}/lose Idempotency-Key

Moves it to the lost stage of its pipeline. The reason is required unless the notes explain it.

Path parameters

Name Type Req. Description
deal integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
loss_reason_id integer no
loss_notes string no

Responses

  • 200 Lost
  • 403 Error envelope
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/crm/deals/42/lose \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "loss_reason_id": 2,
    "loss_notes": "Compró con la competencia a menor precio"
  }'
const response = await fetch('https://demo.nori.software/api/v1/crm/deals/42/lose', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "loss_reason_id": 2,
    "loss_notes": "Compró con la competencia a menor precio"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('crm/deals/42/lose', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'loss_reason_id' => 2,
        'loss_notes' => 'Compró con la competencia a menor precio',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "id": 42,
    "title": "…",
    "status": "open",
    "pipeline_id": 42,
    "stage_id": 42,
    "stage": {
      "id": 42,
      "name": "Rosa roja",
      "color": "…",
      "probability": "…",
      "outcome": "…"
    },
    "business_partner_id": 42,
    "partner": {
      "id": 42,
      "code": "ROSA",
      "name": "Abarrotes Don Pepe",
      "trade_name": "…",
      "type": "…",
      "phone": "3312345678",
      "mobile": "3312345678",
      "email": "ana@example.com",
      "latitude": 1.5,
      "longitude": 1.5
    },
    "partner_contact_id": 42,
    "owner": {
      "id": 42,
      "name": "Rosa roja"
    },
    "sales_employee_id": 42,
    "amount": "232.000000",
    "weighted_amount": "232.000000",
    "currency": "MXN",
    "currency_id": 42,
    "probability": "…",
    "effective_probability": "…",
    "expected_close_date": "2026-11-03",
    "close_overdue": false,
    "source": "…",
    "description": "…",
    "tags": [
      {
        "id": 42,
        "name": "Rosa roja",
        "color": "#2563eb"
      }
    ],
    "next_activity": {
      "id": 42,
      "type": "…",
      "subject": "…",
      "due_at": "2026-11-03T10:15:00-06:00",
      "overdue": false
    },
    "pending_count": 1,
    "overdue_count": 1,
    "idle": false,
    "days_in_stage": 1,
    "loss_reason": {
      "id": 42,
      "name": "Rosa roja"
    },
    "loss_notes": "…",
    "closed_at": "2026-11-03T10:15:00-06:00",
    "last_activity_at": "2026-11-03T10:15:00-06:00",
    "created_at": "2026-11-03T10:15:00-06:00",
    "updated_at": "2026-11-03T10:15:00-06:00",
    "contact": {
      "id": 42,
      "name": "Rosa roja",
      "position": "…",
      "email": "ana@example.com",
      "phone": "3312345678",
      "mobile": "3312345678"
    },
    "pipeline": {
      "id": 42,
      "name": "Rosa roja",
      "stages": [
        {
          "id": null,
          "name": null,
          "color": "#f59e0b",
          "probability": "232.000000",
          "outcome": "open",
          "sort_order": null,
          "auto_on": "quotation"
        }
      ]
    },
    "documents": [
      {
        "id": 42,
        "type": "quotation",
        "status": "…",
        "folio": "A-1043",
        "document_date": "2026-11-03",
        "total": "232.000000"
      }
    ],
    "timeline": {
      "pending": [
        {
          "id": null,
          "deal_id": null,
          "business_partner_id": null,
          "type": "note",
          "subject": null,
          "body": null,
          "occurred_at": null,
          "due_at": null,
          "remind_at": null,
          "completed_at": null,
          "is_follow_up": null,
          "pending": null,
          "overdue": null,
          "editable": null,
          "assigned_to": null,
          "created_by": null,
          "document": null,
          "updated_at": null
        }
      ],
      "history": [
        {
          "id": null,
          "deal_id": null,
          "business_partner_id": null,
          "type": "note",
          "subject": null,
          "body": null,
          "occurred_at": null,
          "due_at": null,
          "remind_at": null,
          "completed_at": null,
          "is_follow_up": null,
          "pending": null,
          "overdue": null,
          "editable": null,
          "assigned_to": null,
          "created_by": null,
          "document": null,
          "updated_at": null
        }
      ]
    }
  }
}

Reopen a won or lost deal

POST /crm/deals/{deal}/reopen Idempotency-Key

Back to the first open stage of its pipeline.

Path parameters

Name Type Req. Description
deal integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Responses

  • 200 Reopened
  • 403 Error envelope
curl -X POST https://demo.nori.software/api/v1/crm/deals/42/reopen \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
const response = await fetch('https://demo.nori.software/api/v1/crm/deals/42/reopen', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('crm/deals/42/reopen', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "id": 42,
    "title": "…",
    "status": "open",
    "pipeline_id": 42,
    "stage_id": 42,
    "stage": {
      "id": 42,
      "name": "Rosa roja",
      "color": "…",
      "probability": "…",
      "outcome": "…"
    },
    "business_partner_id": 42,
    "partner": {
      "id": 42,
      "code": "ROSA",
      "name": "Abarrotes Don Pepe",
      "trade_name": "…",
      "type": "…",
      "phone": "3312345678",
      "mobile": "3312345678",
      "email": "ana@example.com",
      "latitude": 1.5,
      "longitude": 1.5
    },
    "partner_contact_id": 42,
    "owner": {
      "id": 42,
      "name": "Rosa roja"
    },
    "sales_employee_id": 42,
    "amount": "232.000000",
    "weighted_amount": "232.000000",
    "currency": "MXN",
    "currency_id": 42,
    "probability": "…",
    "effective_probability": "…",
    "expected_close_date": "2026-11-03",
    "close_overdue": false,
    "source": "…",
    "description": "…",
    "tags": [
      {
        "id": 42,
        "name": "Rosa roja",
        "color": "#2563eb"
      }
    ],
    "next_activity": {
      "id": 42,
      "type": "…",
      "subject": "…",
      "due_at": "2026-11-03T10:15:00-06:00",
      "overdue": false
    },
    "pending_count": 1,
    "overdue_count": 1,
    "idle": false,
    "days_in_stage": 1,
    "loss_reason": {
      "id": 42,
      "name": "Rosa roja"
    },
    "loss_notes": "…",
    "closed_at": "2026-11-03T10:15:00-06:00",
    "last_activity_at": "2026-11-03T10:15:00-06:00",
    "created_at": "2026-11-03T10:15:00-06:00",
    "updated_at": "2026-11-03T10:15:00-06:00",
    "contact": {
      "id": 42,
      "name": "Rosa roja",
      "position": "…",
      "email": "ana@example.com",
      "phone": "3312345678",
      "mobile": "3312345678"
    },
    "pipeline": {
      "id": 42,
      "name": "Rosa roja",
      "stages": [
        {
          "id": null,
          "name": null,
          "color": "#f59e0b",
          "probability": "232.000000",
          "outcome": "open",
          "sort_order": null,
          "auto_on": "quotation"
        }
      ]
    },
    "documents": [
      {
        "id": 42,
        "type": "quotation",
        "status": "…",
        "folio": "A-1043",
        "document_date": "2026-11-03",
        "total": "232.000000"
      }
    ],
    "timeline": {
      "pending": [
        {
          "id": null,
          "deal_id": null,
          "business_partner_id": null,
          "type": "note",
          "subject": null,
          "body": null,
          "occurred_at": null,
          "due_at": null,
          "remind_at": null,
          "completed_at": null,
          "is_follow_up": null,
          "pending": null,
          "overdue": null,
          "editable": null,
          "assigned_to": null,
          "created_by": null,
          "document": null,
          "updated_at": null
        }
      ],
      "history": [
        {
          "id": null,
          "deal_id": null,
          "business_partner_id": null,
          "type": "note",
          "subject": null,
          "body": null,
          "occurred_at": null,
          "due_at": null,
          "remind_at": null,
          "completed_at": null,
          "is_follow_up": null,
          "pending": null,
          "overdue": null,
          "editable": null,
          "assigned_to": null,
          "created_by": null,
          "document": null,
          "updated_at": null
        }
      ]
    }
  }
}

Log or schedule an activity

POST /crm/deals/{deal}/activities Idempotency-Key

Without due_at it is logged as done now (a call made, a visit, a note). With due_at it is a follow-up, pending until completed, assigned to the deal's owner unless assigned_to says otherwise; remind_minutes sets the reminder that many minutes before. The subject defaults to the type's name.

Path parameters

Name Type Req. Description
deal integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
type string yes Values: notecallemailwhatsappmeetingvisittask
subject string no Default: the type's name
body string no
due_at string<date-time> no Makes it a follow-up
remind_at string<date-time> no
remind_minutes integer no Reminder this many minutes before due_at
assigned_to integer no
completed boolean no A follow-up already done

Responses

  • 201 Created
  • 403 Error envelope
  • 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/crm/deals/42/activities \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "visit",
    "body": "Le gustó el exhibidor; pide muestra de 3 sabores."
  }'
const response = await fetch('https://demo.nori.software/api/v1/crm/deals/42/activities', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "type": "visit",
    "body": "Le gustó el exhibidor; pide muestra de 3 sabores."
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->post('crm/deals/42/activities', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'type' => 'visit',
        'body' => 'Le gustó el exhibidor; pide muestra de 3 sabores.',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 201
{
  "data": {
    "id": 42,
    "deal_id": 42,
    "business_partner_id": 42,
    "type": "note",
    "subject": "…",
    "body": "…",
    "occurred_at": "2026-11-03T10:15:00-06:00",
    "due_at": "2026-11-03T10:15:00-06:00",
    "remind_at": "2026-11-03T10:15:00-06:00",
    "completed_at": "2026-11-03T10:15:00-06:00",
    "is_follow_up": false,
    "pending": false,
    "overdue": false,
    "editable": false,
    "assigned_to": {
      "id": 42,
      "name": "Rosa roja"
    },
    "created_by": {
      "id": 42,
      "name": "Rosa roja"
    },
    "document": {
      "id": 42,
      "type": "…",
      "folio": "A-1043"
    },
    "updated_at": "2026-11-03T10:15:00-06:00"
  }
}

Complete, reopen, reschedule or edit an activity

PATCH /crm/activities/{activity} Idempotency-Key

complete (with an optional outcome added to its notes), reopen and reschedule (due_at) apply to follow-ups; edit changes subject and body of what a person logged. What Nori wrote (stage changes, documents, automations) cannot be edited.

Path parameters

Name Type Req. Description
activity integer yes

Headers

Name Type Req. Description
Idempotency-Key string<uuid> yes UUID made by the app when the user acts and kept with the operation in its queue.

Request body

Name Type Req. Description
action string yes Values: completereopenrescheduleedit
outcome string no
due_at string<date-time> no Required to reschedule
subject string no Required to edit
body string no

Responses

  • 200 Updated
  • 403 Error envelope
  • 404 Error envelope
  • 422 Error envelope
curl -X PATCH https://demo.nori.software/api/v1/crm/activities/42 \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "complete",
    "outcome": "Confirmó el pedido para el lunes."
  }'
const response = await fetch('https://demo.nori.software/api/v1/crm/activities/42', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "action": "complete",
    "outcome": "Confirmó el pedido para el lunes."
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->patch('crm/activities/42', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
        'Idempotency-Key' => Ramsey\Uuid\Uuid::uuid4()->toString(),
    ],
    'json' => [
        'action' => 'complete',
        'outcome' => 'Confirmó el pedido para el lunes.',
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": {
    "id": 42,
    "deal_id": 42,
    "business_partner_id": 42,
    "type": "note",
    "subject": "…",
    "body": "…",
    "occurred_at": "2026-11-03T10:15:00-06:00",
    "due_at": "2026-11-03T10:15:00-06:00",
    "remind_at": "2026-11-03T10:15:00-06:00",
    "completed_at": "2026-11-03T10:15:00-06:00",
    "is_follow_up": false,
    "pending": false,
    "overdue": false,
    "editable": false,
    "assigned_to": {
      "id": 42,
      "name": "Rosa roja"
    },
    "created_by": {
      "id": 42,
      "name": "Rosa roja"
    },
    "document": {
      "id": 42,
      "type": "…",
      "folio": "A-1043"
    },
    "updated_at": "2026-11-03T10:15:00-06:00"
  }
}

My agenda (follow-ups due)

GET /crm/agenda

Pending follow-ups of open deals assigned to the user, soonest first, up to days ahead; each with its bucket (overdue, today, upcoming) and its deal and customer. scope=all (with crm_deals.view_all) lists everyone's.

Query parameters

Name Type Req. Description
scope string no Values: mineall
days integer no How many days ahead Default: 14 Maximum: 90

Responses

  • 200 Follow-ups and counts per bucket
  • 403 Error envelope
curl https://demo.nori.software/api/v1/crm/agenda \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/crm/agenda', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

$response = $client->get('crm/agenda', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Response 200
{
  "data": [
    {
      "id": 42,
      "deal_id": 42,
      "business_partner_id": 42,
      "type": "note",
      "subject": "…",
      "body": "…",
      "occurred_at": "2026-11-03T10:15:00-06:00",
      "due_at": "2026-11-03T10:15:00-06:00",
      "remind_at": "2026-11-03T10:15:00-06:00",
      "completed_at": "2026-11-03T10:15:00-06:00",
      "is_follow_up": false,
      "pending": false,
      "overdue": false,
      "editable": false,
      "assigned_to": {
        "id": 42,
        "name": "Rosa roja"
      },
      "created_by": {
        "id": 42,
        "name": "Rosa roja"
      },
      "document": {
        "id": 42,
        "type": "…",
        "folio": "A-1043"
      },
      "updated_at": "2026-11-03T10:15:00-06:00",
      "bucket": "overdue",
      "deal": {
        "id": 42,
        "title": "…",
        "status": "…",
        "partner": {
          "id": null,
          "name": null,
          "phone": null
        }
      }
    }
  ],
  "meta": {
    "counts": {
      "overdue": 1,
      "today": 1,
      "upcoming": 1
    },
    "server_time": "2026-11-03T10:15:00-06:00"
  }
}