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"
}'
{
"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}.
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"
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.
| HTTP | code | Description |
|---|---|---|
| 400 | tenant_required | X-Tenant missing on the central domain
|
| 404 | tenant_not_found | The company code does not exist |
| 403 | account_suspended | The company is suspended |
# 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.
{
"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
Search
Customers and items accept search. For customers it matches code, name, trade name, RFC (Mexican tax ID) or phone; for items, code or description. For a barcode scanner use barcode, which looks for the exact code.
With mine=true a sales rep sees only their own customers. For a price checker, GET /items/{item}/price-check returns prices per list and unit with tax, stock per warehouse and the default bin location.
curl "https://demo.nori.software/api/v1/customers?search=don%20pepe&mine=true" \
-H "Authorization: Bearer $NORI_TOKEN"
curl "https://demo.nori.software/api/v1/items?barcode=7501055363057" \
-H "Authorization: Bearer $NORI_TOKEN"
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.
| HTTP | code | Description |
|---|---|---|
| 422 | validation | Invalid data; fields has the messages per field
|
| 422 | business_rule | A business rule failed (credit, stock…); field says what to fix
|
| 422 | authorization_required | Needs supervisor approval; authorization contains the request
|
| 401 | unauthenticated | No token, or an expired or revoked one |
| 403 | forbidden | The user lacks the permission or is out of scope |
| 404 | not_found | The resource does not exist |
| 428 | idempotency_key_required | A write without Idempotency-Key
|
| 422 | idempotency_key_reused | The key was already used with another request |
| 429 | rate_limited | Too many requests; wait for the time in Retry-After
|
| 500 | server_error | Unexpected error; show the request_id to the user
|
{
"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.
- 01The first request runs and its successful response is stored.
- 02The same key with the same request returns the stored response with
Idempotent-Replayed: true, and nothing runs again. - 03The same key with a different request returns
422 idempotency_key_reused. - 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
}'
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 company | 240 per minute |
| Sign in | 5 per minute per user and IP |
| Records per list page | Up to 200 |
| Rows per sync page | 500 by default, up to 2,000 |
| Operations per queue push | 100 |
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.
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
}
]
}
}
]
}'
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.
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 | RequiredReq. | Description |
|---|---|---|---|
| X-Tenant | string | no | Company code, only on a central domain. |
Request body
| Name | Type | RequiredReq. | 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"
}'
{
"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"
{
"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"
{
"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"
{
"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 | RequiredReq. | 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"
{
"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"
{
"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 | RequiredReq. | 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 | RequiredReq. | 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
}'
{
"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 | RequiredReq. | 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 | RequiredReq. | 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"
}'
{
"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 | RequiredReq. | 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
{
"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"
{
"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"
{
"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 | RequiredReq. | 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"
{
"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 | RequiredReq. | 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 | RequiredReq. | Description |
|---|---|---|---|
| name | string | yes | |
| trade_name | string | no | |
| phone | string | no | |
| mobile | string | no | |
| 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
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| customer | integer | yes |
Responses
- 200 Customer
- 404 Error envelope
curl https://demo.nori.software/api/v1/customers/42 \
-H "Authorization: Bearer $NORI_TOKEN"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| customer | integer | yes |
Responses
- 200 Open documents
curl https://demo.nori.software/api/v1/customers/42/open-documents \
-H "Authorization: Bearer $NORI_TOKEN"
{
"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 | RequiredReq. | 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"
{
"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 | RequiredReq. | 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"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| item | integer | yes |
Responses
- 200 Item
- 404 Error envelope
curl https://demo.nori.software/api/v1/items/42 \
-H "Authorization: Bearer $NORI_TOKEN"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| item | integer | yes |
Query parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| warehouse_ids[] | array<integer> | no |
Responses
- 200 Stock
curl https://demo.nori.software/api/v1/items/42/stock \
-H "Authorization: Bearer $NORI_TOKEN"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| item | integer | yes |
Query parameters
| Name | Type | RequiredReq. | 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"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| item | integer | yes |
Responses
- 200 Images
curl https://demo.nori.software/api/v1/items/42/images \
-H "Authorization: Bearer $NORI_TOKEN"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| item | integer | yes |
Query parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| warehouse_id | integer | no |
Responses
- 200 Package
curl https://demo.nori.software/api/v1/items/42/package \
-H "Authorization: Bearer $NORI_TOKEN"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| item | integer | yes |
Query parameters
| Name | Type | RequiredReq. | 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"
{
"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 | RequiredReq. | 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"
}'
{
"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 | RequiredReq. | 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
}
]
}'
{
"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 | RequiredReq. | 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"
{
"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 | RequiredReq. | 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 | RequiredReq. | 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
}
]
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| document | integer | yes |
Responses
- 200 Document
- 404 Error envelope
curl https://demo.nori.software/api/v1/documents/42 \
-H "Authorization: Bearer $NORI_TOKEN"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| document | integer | yes |
Headers
| Name | Type | RequiredReq. | 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 | RequiredReq. | 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
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| document | integer | yes |
Query parameters
| Name | Type | RequiredReq. | 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"
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 | RequiredReq. | 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"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| document | integer | yes |
Query parameters
| Name | Type | RequiredReq. | 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
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 | RequiredReq. | Description |
|---|---|---|---|
| document | integer | yes |
Headers
| Name | Type | RequiredReq. | 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 | RequiredReq. | 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."
}'
{
"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 | RequiredReq. | 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"
{
"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 | RequiredReq. | 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 | RequiredReq. | 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"
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| payment | integer | yes |
Responses
- 200 Payment
- 404 Error envelope
curl https://demo.nori.software/api/v1/payments/42 \
-H "Authorization: Bearer $NORI_TOKEN"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| payment | integer | yes |
Query parameters
| Name | Type | RequiredReq. | 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"
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 | RequiredReq. | 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"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| payment | integer | yes |
Query parameters
| Name | Type | RequiredReq. | 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
The response is a file (application/pdf).
Email the receipt
POST
/payments/{payment}/email
Idempotency-Key
Path parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| payment | integer | yes |
Headers
| Name | Type | RequiredReq. | 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 | RequiredReq. | 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
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| item | integer | yes |
Query parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| warehouse_ids[] | array<integer> | no |
Responses
- 200 Batches
curl https://demo.nori.software/api/v1/items/42/batches \
-H "Authorization: Bearer $NORI_TOKEN"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| item | integer | yes |
Query parameters
| Name | Type | RequiredReq. | 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"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| item | integer | yes |
Query parameters
| Name | Type | RequiredReq. | 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"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| warehouse | integer | yes |
Query parameters
| Name | Type | RequiredReq. | 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"
{
"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 | RequiredReq. | 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"
{
"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 | RequiredReq. | 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"
{
"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 | RequiredReq. | 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"
{
"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 | RequiredReq. | 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"
{
"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 | RequiredReq. | 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"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| movement | string | yes |
Values: transfer-requeststransfersgoods-receiptsgoods-issues
|
Headers
| Name | Type | RequiredReq. | 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 | RequiredReq. | 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
}
]
}'
{
"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 | RequiredReq. | 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"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| count | integer | yes |
Responses
- 200 Count
curl https://demo.nori.software/api/v1/inventory/counts/42 \
-H "Authorization: Bearer $NORI_TOKEN"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| count | integer | yes |
Responses
- 200 Progress
curl https://demo.nori.software/api/v1/inventory/counts/42/progress \
-H "Authorization: Bearer $NORI_TOKEN"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| count | integer | yes |
Query parameters
| Name | Type | RequiredReq. | 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"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| count | integer | yes |
Query parameters
| Name | Type | RequiredReq. | 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"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| count | integer | yes |
Headers
| Name | Type | RequiredReq. | 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 | RequiredReq. | 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"
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| count | integer | yes | |
| line | integer | yes |
Headers
| Name | Type | RequiredReq. | 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)"
{
"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 | RequiredReq. | 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"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| plan | integer | yes |
Responses
- 200 Plan (not wrapped)
curl https://demo.nori.software/api/v1/route/plans/42 \
-H "Authorization: Bearer $NORI_TOKEN"
{
"plan": {},
"replayed": false
}
What the truck carries
GET
/route/plans/{plan}/stock
Path parameters
| Name | Type | RequiredReq. | 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"
{
"items": [
{}
]
}
Start the day (opens the seller's cash register with the fund)
POST
/route/plans/{plan}/start
Idempotency-Key
Path parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| plan | integer | yes |
Headers
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| Idempotency-Key | string<uuid> | no | Optional on route endpoints; also accepted as the "key" field. |
Request body
| Name | Type | RequiredReq. | 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
}'
{
"plan": {},
"replayed": false
}
Back to the depot
POST
/route/plans/{plan}/finish
Idempotency-Key
Path parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| plan | integer | yes |
Headers
| Name | Type | RequiredReq. | 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)"
{
"plan": {},
"replayed": false
}
Unplanned stop
POST
/route/plans/{plan}/visits
Idempotency-Key
Path parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| plan | integer | yes |
Headers
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| Idempotency-Key | string<uuid> | no | Optional on route endpoints; also accepted as the "key" field. |
Request body
| Name | Type | RequiredReq. | 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
]
}'
{
"visit": {},
"replayed": false
}
Customers for an unplanned stop
GET
/route/customers
Query parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| search | string | no |
Responses
- 200 Customers
curl https://demo.nori.software/api/v1/route/customers?search=manzana \
-H "Authorization: Bearer $NORI_TOKEN"
[
{
"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 | RequiredReq. | 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"
{
"visit": {},
"replayed": false
}
Arrived (GPS and device time)
POST
/route/visits/{visit}/check-in
Idempotency-Key
Path parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| visit | integer | yes |
Headers
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| Idempotency-Key | string<uuid> | no | Optional on route endpoints; also accepted as the "key" field. |
Request body
| Name | Type | RequiredReq. | 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"
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| visit | integer | yes |
Headers
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| Idempotency-Key | string<uuid> | no | Optional on route endpoints; also accepted as the "key" field. |
Request body
| Name | Type | RequiredReq. | 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": "…"
}'
{
"visit": {},
"replayed": false
}
Not visited (reason)
POST
/route/visits/{visit}/skip
Idempotency-Key
Path parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| visit | integer | yes |
Headers
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| Idempotency-Key | string<uuid> | no | Optional on route endpoints; also accepted as the "key" field. |
Request body
| Name | Type | RequiredReq. | 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": "…"
}'
{
"visit": {},
"replayed": false
}
Visited without a sale (reason)
POST
/route/visits/{visit}/no-sale
Idempotency-Key
Path parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| visit | integer | yes |
Headers
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| Idempotency-Key | string<uuid> | no | Optional on route endpoints; also accepted as the "key" field. |
Request body
| Name | Type | RequiredReq. | 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": "…"
}'
{
"visit": {},
"replayed": false
}
Engine prices and totals of a cart before selling
POST
/route/visits/{visit}/quote
Path parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| visit | integer | yes |
Request body
| Name | Type | RequiredReq. | 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
}
]
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| visit | integer | yes |
Headers
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| Idempotency-Key | string<uuid> | no | Optional on route endpoints; also accepted as the "key" field. |
Request body
| Name | Type | RequiredReq. | 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
}
]
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| visit | integer | yes |
Headers
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| Idempotency-Key | string<uuid> | no | Optional on route endpoints; also accepted as the "key" field. |
Request body
| Name | Type | RequiredReq. | 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"
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| visit | integer | yes |
Headers
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| Idempotency-Key | string<uuid> | no | Optional on route endpoints; also accepted as the "key" field. |
Request body
| Name | Type | RequiredReq. | 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
}
]
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| visit | integer | yes |
Headers
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| Idempotency-Key | string<uuid> | no | Optional on route endpoints; also accepted as the "key" field. |
Request body
| Name | Type | RequiredReq. | 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
}
]
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| visit | integer | yes |
Headers
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| Idempotency-Key | string<uuid> | no | Optional on route endpoints; also accepted as the "key" field. |
Request body
| Name | Type | RequiredReq. | 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
]
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| visit | integer | yes | |
| shipment | integer | yes |
Headers
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| Idempotency-Key | string<uuid> | no | Optional on route endpoints; also accepted as the "key" field. |
Request body · multipart/form-data
| Name | Type | RequiredReq. | 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=…"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| visit | integer | yes | |
| shipment | integer | yes |
Headers
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| Idempotency-Key | string<uuid> | no | Optional on route endpoints; also accepted as the "key" field. |
Request body
| Name | Type | RequiredReq. | 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": "…"
}'
{
"visit": {},
"replayed": false
}
Cooler/display service with photos (multipart)
POST
/route/visits/{visit}/asset-service
Idempotency-Key
Path parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| visit | integer | yes |
Headers
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| Idempotency-Key | string<uuid> | no | Optional on route endpoints; also accepted as the "key" field. |
Request body · multipart/form-data
| Name | Type | RequiredReq. | 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"
{
"visit": {},
"replayed": false
}
Photo of the visit (multipart)
POST
/route/visits/{visit}/photos
Idempotency-Key
Path parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| visit | integer | yes |
Headers
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| Idempotency-Key | string<uuid> | no | Optional on route endpoints; also accepted as the "key" field. |
Request body · multipart/form-data
| Name | Type | RequiredReq. | 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=…"
{
"visit": {},
"replayed": false
}
Route offline queue (actions by name; /sync/push is the general one)
POST
/route/sync
Request body
| Name | Type | RequiredReq. | 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": {}
}
]
}'
{
"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 | RequiredReq. | 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"
{
"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"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| entity | string | yes |
Values: itemsitem_pricesstockcustomersprice_liststender_typeswarehousescausalitiesvisit_reasons
|
Query parameters
| Name | Type | RequiredReq. | 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"
{
"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 | RequiredReq. | 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
}
]
}
}
]
}'
{
"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 | RequiredReq. | 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 | RequiredReq. | 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?"
}'
{
"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"
{
"data": [
{}
]
}
Messages of a conversation
GET
/assistant/conversations/{conversation}/messages
Path parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| conversation | integer | yes |
Responses
- 200 Messages
curl https://demo.nori.software/api/v1/assistant/conversations/42/messages \
-H "Authorization: Bearer $NORI_TOKEN"
{
"data": [
{}
]
}
Suggested questions
GET
/assistant/templates
Responses
- 200 Templates
curl https://demo.nori.software/api/v1/assistant/templates \
-H "Authorization: Bearer $NORI_TOKEN"
{
"data": [
{}
]
}
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"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| pipeline | integer | yes |
Query parameters
| Name | Type | RequiredReq. | 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"
{
"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"
{
"data": [
{
"id": 1,
"name": "Precio"
},
{
"id": 2,
"name": "Competencia"
},
{
"id": 3,
"name": "Sin presupuesto"
}
]
}
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 | RequiredReq. | 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"
{
"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 | RequiredReq. | 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 | RequiredReq. | 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
]
}'
{
"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 | RequiredReq. | 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"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| deal | integer | yes |
Headers
| Name | Type | RequiredReq. | 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 | RequiredReq. | 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
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| deal | integer | yes |
Headers
| Name | Type | RequiredReq. | 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 | RequiredReq. | 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"
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| deal | integer | yes |
Headers
| Name | Type | RequiredReq. | 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)"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| deal | integer | yes |
Headers
| Name | Type | RequiredReq. | 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 | RequiredReq. | 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"
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| deal | integer | yes |
Headers
| Name | Type | RequiredReq. | 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)"
{
"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
}
]
}
}
}
Link a quotation, order or invoice of the customer
POST
/crm/deals/{deal}/documents
Idempotency-Key
The document must be of the deal's customer (422 business_rule otherwise). A posted document moves the deal to the stage marked for it ("move here when"), never backwards. To make the quotation from the deal, send crm_deal_id in POST /documents instead.
Path parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| deal | integer | yes |
Headers
| Name | Type | RequiredReq. | 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 | RequiredReq. | Description |
|---|---|---|---|
| document_id | integer | yes |
Responses
- 200 Linked
- 403 Error envelope
- 404 Error envelope
- 422 Error envelope
curl -X POST https://demo.nori.software/api/v1/crm/deals/42/documents \
-H "Authorization: Bearer $NORI_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"document_id": 845
}'
{
"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
}
]
}
}
}
Unlink a document from the deal
DELETE
/crm/deals/{deal}/documents/{document}
Idempotency-Key
Path parameters
| Name | Type | RequiredReq. | Description |
|---|---|---|---|
| deal | integer | yes | |
| document | integer | yes |
Headers
| Name | Type | RequiredReq. | 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 Unlinked
- 403 Error envelope
- 404 Error envelope
curl -X DELETE https://demo.nori.software/api/v1/crm/deals/42/documents/42 \
-H "Authorization: Bearer $NORI_TOKEN" \
-H "Idempotency-Key: $(uuidgen)"
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| deal | integer | yes |
Headers
| Name | Type | RequiredReq. | 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 | RequiredReq. | 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."
}'
{
"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 | RequiredReq. | Description |
|---|---|---|---|
| activity | integer | yes |
Headers
| Name | Type | RequiredReq. | 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 | RequiredReq. | 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."
}'
{
"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 | RequiredReq. | 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"
{
"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"
}
}