Saltar al contenido
openapi.yaml

API v1.1.0 · REST y JSON

API de Nori

Es la misma API que usa la app móvil. Lees catálogos, creas pedidos y facturas, registras cobros y trabajas sin señal con las reglas y los permisos que ya tiene la web: lo que un usuario no puede hacer en pantalla tampoco lo puede hacer por API.

URL base Cada empresa tiene su dominio y su base de datos.

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

Formato
JSON; importes como cadenas decimales
Autenticación
Bearer, un token por dispositivo
Escrituras
Idempotency-Key obligatoria
Paginación
Por cursor, hasta 200 por página
Límite
240 peticiones por minuto

¿Vas a integrar un sistema? Escríbenos y lo revisamos contigo.

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

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

Guía

Autenticación

Cada dispositivo inicia sesión con el usuario y la contraseña de la web y recibe su propio token. Volver a iniciar sesión en el mismo dispositivo (device_name) reemplaza el token anterior.

Manda el token en cada llamada con Authorization: Bearer <token>. Vence a los 30 días: refréscalo con POST /auth/refresh cuando falten pocos días para expires_at, y el anterior deja de servir en ese momento.

El login acepta cinco intentos por minuto por usuario e IP. Con GET /auth/devices y DELETE /auth/devices/{token} el usuario cierra la sesión de un teléfono perdido.

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

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

Guía

Empresa

Cada empresa tiene su dominio y su propia base de datos. Lo más simple es llamar al dominio de la empresa, igual que la web: https://tu-empresa.nori.software/api/v1.

Desde el dominio central (api.nori.software) manda el código de la empresa en el encabezado X-Tenant; en el login también sirve el campo tenant. El login responde tenant.domain, así que basta con pedir el código una vez.

Un token solo existe en la base de su empresa y no sirve en otra.

HTTPcodeDescripción
400tenant_requiredFalta X-Tenant en el dominio central
404tenant_not_foundEl código de empresa no existe
403account_suspendedLa empresa está suspendida
cURL
# Dominio de la empresa
curl https://tu-empresa.nori.software/api/v1/items \
  -H "Authorization: Bearer $NORI_TOKEN"

# Dominio central
curl https://api.nori.software/api/v1/items \
  -H "X-Tenant: tu-empresa" \
  -H "Authorization: Bearer $NORI_TOKEN"

Guía

Formato y paginación

Las respuestas exitosas vienen en {"data": …} y algunas agregan meta. Los endpoints de venta en ruta (/route/*) responden sin data, con la misma forma que usa la pantalla web de la ruta.

Importes y cantidades son cadenas decimales con la precisión guardada ("232.000000"). No los conviertas a double para hacer cuentas de dinero. Las fechas van en YYYY-MM-DD y los instantes en ISO 8601 con zona horaria.

Las listas se paginan por cursor. Pide ?per_page=50 (hasta 200) y sigue con cursor=meta.next_cursor hasta que llegue null.

Documentos y pagos traen pdf_url, una liga firmada que sirve 30 minutos para abrir o compartir el PDF sin token.

GET /items 200
{
  "data": [
    {
      "id": 5,
      "code": "ROSA",
      "name": "Rosa roja",
      "barcode": "7501055363057",
      "uom": "Pieza",
      "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
  }
}

Guía

Errores

Todos los errores usan el mismo sobre: un message listo para mostrar y un objeto error con un code estable, el field que hay que corregir cuando aplica y un request_id para soporte.

authorization_required trae la solicitud firmada que también recibe la pantalla web. No la modifiques: úsala con los endpoints de autorizaciones para pedir la aprobación o aplicar una ya dada.

HTTPcodeDescripción
422validationDatos inválidos; fields trae los mensajes por campo
422business_ruleUna regla del negocio (crédito, existencia…); field dice qué corregir
422authorization_requiredNecesita un supervisor; authorization trae la solicitud
401unauthenticatedSin token, o con un token vencido o revocado
403forbiddenEl usuario no tiene el permiso o está fuera de su alcance
404not_foundEl recurso no existe
428idempotency_key_requiredEscritura sin Idempotency-Key
422idempotency_key_reusedLa llave ya se usó con otra petición
429rate_limitedDemasiadas peticiones; espera lo que indique Retry-After
500server_errorError inesperado; muestra el request_id al usuario
Respuesta 422
{
  "message": "El pago excede el saldo de Factura A-120 (232.00).",
  "error": {
    "code": "business_rule",
    "message": "El pago excede el saldo de Factura A-120 (232.00).",
    "field": "applications",
    "request_id": "K3J9QX2A"
  }
}

Guía

Idempotencia

Toda escritura (POST, PUT, PATCH y DELETE) lleva Idempotency-Key con un UUID v4. Genera la llave cuando el usuario actúa, guárdala junto con la operación y usa la misma en cada reintento.

  1. 01La primera vez se ejecuta y la respuesta exitosa queda guardada.
  2. 02Con la misma llave y la misma petición regresa la respuesta guardada con Idempotent-Replayed: true, sin volver a ejecutar nada.
  3. 03La misma llave con otra petición responde 422 idempotency_key_reused.
  4. 04Si la operación falló no se guarda, así que puedes corregir y reintentar con la misma llave.

No llevan llave el login, refresh, logout, revocar un dispositivo, los cálculos de /pricing/* y /sync/push, donde cada operación trae la suya. En venta en ruta la llave es opcional.

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

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

Guía

Límites

Al pasar el límite la API responde 429 rate_limited con el encabezado Retry-After en segundos.

Peticiones por usuario y empresa240 por minuto
Inicio de sesión5 por minuto por usuario e IP
Registros por página de una listaHasta 200
Filas por página de sincronización500 por omisión, hasta 2,000
Operaciones por envío de la cola100
Respuesta 429
HTTP/1.1 429 Too Many Requests
Retry-After: 12

{
  "message": "Demasiadas peticiones.",
  "error": {
    "code": "rate_limited",
    "message": "Demasiadas peticiones.",
    "request_id": "K3J9QX2A"
  }
}

Guía

Trabajo sin conexión

GET /sync lista los catálogos que el usuario puede guardar en el teléfono según sus permisos: artículos, precios, existencias, clientes, listas de precios, formas de cobro, almacenes, causas y motivos de visita.

Cada catálogo se baja por cambios. Sin token, o con uno muy viejo, llega meta.reset: true y hay que vaciar la tabla local. Borra lo que llegue en deleted, inserta o actualiza data por id y guarda meta.sync_token solo al terminar todas las páginas.

Lo que se captura sin señal se manda en orden con POST /sync/push, hasta 100 operaciones por envío. Una que falla no detiene a las demás. Un valor "$ref:<key>:<ruta>" toma un dato de una operación anterior del mismo envío, como el id de un cliente creado sin señal.

sync
token = guardado[entidad]    # null la primera vez
cursor = null
loop:
  r = GET /sync/{entidad}?updated_since={token}&cursor={cursor}
  if r.meta.reset and cursor == null: vaciar la tabla local
  borrar r.deleted             # primero borrar
  upsert r.data por id         # después insertar o actualizar
  if not r.meta.has_more: break
  cursor = r.meta.next_cursor
guardado[entidad] = r.meta.sync_token
curl -X POST https://demo.nori.software/api/v1/sync/push \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "operations": [
      {
        "key": "6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001",
        "method": "POST",
        "path": "/api/v1/customers",
        "body": {
          "name": "Tienda nueva"
        }
      },
      {
        "key": "6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0002",
        "method": "POST",
        "path": "/api/v1/documents",
        "body": {
          "type": "sales_order",
          "business_partner_id": "$ref:6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001:data.id",
          "lines": [
            {
              "item_id": 5,
              "quantity": 1
            }
          ]
        }
      }
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/sync/push', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "operations": [
      {
        "key": "6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001",
        "method": "POST",
        "path": "/api/v1/customers",
        "body": {
          "name": "Tienda nueva"
        }
      },
      {
        "key": "6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0002",
        "method": "POST",
        "path": "/api/v1/documents",
        "body": {
          "type": "sales_order",
          "business_partner_id": "$ref:6f1c2a9e-0d0b-4b8e-9a43-0c1f1f3c0001:data.id",
          "lines": [
            {
              "item_id": 5,
              "quantity": 1
            }
          ]
        }
      }
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

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

Guía

Versionado

El prefijo /api/v1 es la versión mayor. Dentro de v1 solo entran cambios compatibles: endpoints, campos o valores nuevos, así que ignora los campos que no conozcas.

Un cambio incompatible abre /api/v2. La versión anterior se mantiene al menos seis meses y lo avisa con los encabezados Deprecation, Sunset y Link.

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

Referencia

Autenticación

Inicia sesión desde un dispositivo, renueva o revoca su token y administra los dispositivos con sesión abierta.

Iniciar sesión en un dispositivo

POST /auth/login Sin sesión

Encabezados

Nombre Tipo Req. Descripción
X-Tenant string no Código de la empresa, solo en un dominio central.

Cuerpo de la petición

Nombre Tipo Req. Descripción
username string sí
password string sí
device_name string sí Un token por dispositivo: iniciar sesión de nuevo lo reemplaza.
tenant string no Código de la empresa, solo necesario en un dominio central.

Respuestas

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

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

Renovar el token del dispositivo

POST /auth/refresh

Respuestas

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

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

Revocar el token del dispositivo

POST /auth/logout

Respuestas

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

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

Dispositivos con sesión abierta

GET /auth/devices

Respuestas

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

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

Cerrar la sesión de un dispositivo

DELETE /auth/devices/{token}

Parámetros de ruta

Nombre Tipo Req. Descripción
token integer sí

Respuestas

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

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

Usuario, permisos y contexto de trabajo

GET /me

Respuestas

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

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

Elegir el almacén de trabajo

PUT /me/working-warehouse Idempotency-Key

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
warehouse_id integer sí

Respuestas

  • 200 Sesión con el nuevo almacén de trabajo
  • 422 Sobre de error
curl -X PUT https://demo.nori.software/api/v1/me/working-warehouse \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "warehouse_id": 3
  }'
const response = await fetch('https://demo.nori.software/api/v1/me/working-warehouse', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "warehouse_id": 3
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

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

Pedir que se elimine la cuenta del usuario

POST /me/deletion-request Idempotency-Key

El usuario con sesión pide que Nori borre su cuenta y sus datos personales (nombre, correo, teléfono, accesos de dispositivos, sesiones y los puntos GPS de sus visitas de ruta) en un máximo de 30 días. Los documentos y registros fiscales de la empresa se conservan y el usuario aparece en ellos como «Usuario eliminado #id». La sesión verifica la solicitud; se avisa a Nori y a los administradores de la empresa. Si el usuario ya tiene una solicitud abierta, se devuelve esa con 200. Después, la app debe cerrar sesión y borrar sus datos locales.

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
reason string | null no Motivo, opcional

Respuestas

  • 201 Solicitud recibida
  • 200 La solicitud abierta del usuario, ya recibida
  • 422 Sobre de error
curl -X POST https://demo.nori.software/api/v1/me/deletion-request \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Ya no trabajo en la empresa"
  }'
const response = await fetch('https://demo.nori.software/api/v1/me/deletion-request', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "reason": "Ya no trabajo en la empresa"
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

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

Referencia

Meta

Consulta la sesión del usuario, sus permisos, el almacén de trabajo, la hora del servidor y los catálogos de referencia.

Hora del servidor y versión de la API

GET /meta/server-time Sin sesión

Encabezados

Nombre Tipo Req. Descripción
X-Tenant string no Código de la empresa, solo en un dominio central.

Respuestas

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

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

Catálogos de referencia

GET /catalogs

Respuestas

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

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

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

GET /user-fields

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

Respuestas

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

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

Referencia

Clientes

Busca y da de alta clientes, consulta su cuenta (saldo, crédito, antigüedad) y los documentos abiertos por cobrar.

Buscar clientes

GET /customers

Parámetros de consulta

Nombre Tipo Req. Descripción
search string no Código, nombre, nombre comercial, RFC o teléfono
mine boolean no Solo los clientes del empleado de ventas del usuario
route_id integer no
include_inactive boolean no
per_page integer no Por omisión: 50 Máximo: 200
cursor string no meta.next_cursor de la página anterior

Respuestas

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

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

Alta rápida de cliente

POST /customers Idempotency-Key

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
name string sí
trade_name string no
phone string no
mobile string no
email string<email> no
rfc string no
notes string no
latitude number no
longitude number no

Respuestas

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

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

Cliente con direcciones, contactos y saldo

GET /customers/{customer}

Parámetros de ruta

Nombre Tipo Req. Descripción
customer integer sí

Respuestas

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

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

Documentos abiertos por cobrar

GET /customers/{customer}/open-documents

Parámetros de ruta

Nombre Tipo Req. Descripción
customer integer sí

Respuestas

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

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

Referencia

Artículos

Busca artículos por texto o código de barras y consulta existencias, lotes, series, ubicaciones, imágenes y precios.

Buscar artículos por texto o código de barras

GET /items

Parámetros de consulta

Nombre Tipo Req. Descripción
search string no
barcode string no Código de barras o código exacto
item_group_id integer no
sales boolean no
inventory boolean no
per_page integer no Por omisión: 50 Máximo: 200
cursor string no meta.next_cursor de la página anterior

Respuestas

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

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

Artículos más vendidos por el usuario

GET /items/frequent

Pedidos, entregas y facturas que creó el usuario o que llevan a su empleado de ventas; los más frecuentes primero.

Parámetros de consulta

Nombre Tipo Req. Descripción
days integer no Por omisión: 30 Máximo: 365
limit integer no Por omisión: 30 Máximo: 100

Respuestas

  • 200 Artículos con la cantidad vendida y en cuántos documentos
  • 403 Sobre de error
curl https://demo.nori.software/api/v1/items/frequent \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/items/frequent', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

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

Artículo con imágenes, existencias y kit

GET /items/{item}

Parámetros de ruta

Nombre Tipo Req. Descripción
item integer sí

Respuestas

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

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

Existencias por almacén

GET /items/{item}/stock

Parámetros de ruta

Nombre Tipo Req. Descripción
item integer sí

Parámetros de consulta

Nombre Tipo Req. Descripción
warehouse_ids[] array<integer> no

Respuestas

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

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

Precio del artículo para un cliente o lista

GET /items/{item}/price

Parámetros de ruta

Nombre Tipo Req. Descripción
item integer sí

Parámetros de consulta

Nombre Tipo Req. Descripción
partner_id integer no
price_list_id integer no
quantity number no Por omisión: 1
uom_id integer no
warehouse_id integer no
document_type string no Valores: quotationsales_orderdeliveryreturndown_paymentinvoicecredit_memodebit_memotransfer_requestinventory_transfergoods_receiptgoods_issue

Respuestas

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

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

Galería del artículo

GET /items/{item}/images

Parámetros de ruta

Nombre Tipo Req. Descripción
item integer sí

Respuestas

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

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

Componentes del kit y opciones de paquete

GET /items/{item}/package

Parámetros de ruta

Nombre Tipo Req. Descripción
item integer sí

Parámetros de consulta

Nombre Tipo Req. Descripción
warehouse_id integer no

Respuestas

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

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

Verificador de precios

GET /items/{item}/price-check

Precios de las listas que el usuario puede usar (todas para administradores), por unidad (uom_id null = unidad de venta), con price_with_tax calculado con el impuesto de venta del artículo. Existencias de los almacenes que el usuario puede ver, primero el almacén de trabajo; si el almacén usa ubicaciones, bin_location es la ubicación habitual del artículo ahí y bins su existencia por ubicación. Los artículos manejados por lotes listan sus batches con existencia.

Parámetros de ruta

Nombre Tipo Req. Descripción
item integer sí

Parámetros de consulta

Nombre Tipo Req. Descripción
warehouse_ids[] array<integer> no

Respuestas

  • 200 Artículo para el verificador de precios
  • 404 Sobre de error
curl https://demo.nori.software/api/v1/items/42/price-check \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/items/42/price-check', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

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

Referencia

Precios

Calcula el precio de una partida o de un carrito completo con descuentos, promociones e impuestos sin guardar nada.

Cotizar una partida

POST /pricing/quote

Una partida a partir de texto ("3*ROSA"), código de barras o id de artículo. No guarda nada.

Cuerpo de la petición

Nombre Tipo Req. Descripción
document_type string sí Valores: quotationsales_orderdeliveryreturndown_paymentinvoicecredit_memodebit_memotransfer_requestinventory_transfergoods_receiptgoods_issue
partner_id integer no
price_list_id integer no Predeterminado: el del cliente; si no, el del usuario
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

Respuestas

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

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

Recalcular un carrito

POST /pricing/recalculate

Todas las partidas de un carrito y sus totales, con promociones. No guarda nada.

Cuerpo de la petición

Nombre Tipo Req. Descripción
document_type string sí Valores: quotationsales_orderdeliveryreturndown_paymentinvoicecredit_memodebit_memotransfer_requestinventory_transfergoods_receiptgoods_issue
partner_id integer no
price_list_id integer no Predeterminado: el del cliente; si no, el del usuario
currency_id integer no
exchange_rate number no
warehouse_id integer no
discount_percent number no
lines array<object> sí
lines[].item_id integer sí
lines[].quantity number sí
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

Respuestas

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

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

Referencia

Documentos

Crea, edita y consulta documentos de venta, descarga su PDF o la representación impresa del CFDI y envíalos por correo.

Documentos de venta

GET /documents

Parámetros de consulta

Nombre Tipo Req. Descripción
type string no Valores: quotationsales_orderdeliveryreturndown_paymentinvoicecredit_memodebit_memotransfer_requestinventory_transfergoods_receiptgoods_issue
mine boolean no Creados por mí o de mi empleado de ventas
status string no Valores: draftopenclosedcancelled
partner_id integer no
from string<date> no
to string<date> no
search string no
per_page integer no Por omisión: 50 Máximo: 200
cursor string no meta.next_cursor de la página anterior

Respuestas

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

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

Crear un documento de venta

POST /documents Idempotency-Key

Lo guarda el motor de documentos: precios, descuentos, impuestos y totales se recalculan y aplica la vista del usuario (los campos bloqueados conservan su valor predeterminado). draft: true lo deja sin contabilizar. Las entregas y facturas con tenders (o con on_credit: true) se contabilizan y cobran en una sola transacción. Con crm_deal_id (una cotización hecha desde una oportunidad del CRM) el documento se liga a la oportunidad.

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
type string no Obligatorio al crear Valores: quotationsales_orderdeliveryreturndown_paymentinvoicecredit_memodebit_memo
draft boolean no
on_credit boolean no Entregas y facturas: contabilizar y dejar a crédito (aplica la política de crédito)
tenders array<object> no
tenders[].tender_type_id integer sí
tenders[].amount number sí
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 Solo al crear: la oportunidad del CRM a la que pertenece el documento (mismo cliente; requiere crm_deals.update). Se liga y hace avanzar la oportunidad
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 Partida existente (edición)
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 Copiada de esta partida de un documento base
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 Opciones del paquete: {id de grupo: {id de artículo: cuántos}} por unidad

Respuestas

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

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

Documento con sus partidas

GET /documents/{document}

Parámetros de ruta

Nombre Tipo Req. Descripción
document integer sí

Respuestas

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

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

Editar un documento

PATCH /documents/{document} Idempotency-Key

Edita un borrador (y lo contabiliza con post=true) o los datos de encabezado de un documento contabilizado.

Parámetros de ruta

Nombre Tipo Req. Descripción
document integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
type string no Obligatorio al crear Valores: quotationsales_orderdeliveryreturndown_paymentinvoicecredit_memodebit_memo
draft boolean no
on_credit boolean no Entregas y facturas: contabilizar y dejar a crédito (aplica la política de crédito)
tenders array<object> no
tenders[].tender_type_id integer sí
tenders[].amount number sí
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 Solo al crear: la oportunidad del CRM a la que pertenece el documento (mismo cliente; requiere crm_deals.update). Se liga y hace avanzar la oportunidad
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 Partida existente (edición)
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 Copiada de esta partida de un documento base
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 Opciones del paquete: {id de grupo: {id de artículo: cuántos}} por unidad
post boolean no

Respuestas

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

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

PDF del documento

GET /documents/{document}/pdf

Parámetros de ruta

Nombre Tipo Req. Descripción
document integer sí

Parámetros de consulta

Nombre Tipo Req. Descripción
template integer no

Respuestas

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

$response = $client->get('documents/42/pdf', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Respuesta 200
La respuesta es un archivo (application/pdf).

Representación impresa del CFDI

GET /documents/{document}/cfdi

Se lee del XML timbrado: comprobante, emisor, receptor, conceptos con sus impuestos, impuestos por impuesto y tasa, totales con el importe con letra, el timbre (UUID, certificados, sellos, cadena original del complemento de certificación del SAT) y la URL de verificación del SAT para imprimir como QR. Facturas, notas de crédito y de cargo y anticipos ("I"/"E"); entregas y traslados con Carta Porte ("T", carta_porte). Un documento que aún no se timbra responde su vista previa con status: unstamped (stamp y verification_url en null; imprime "SIN TIMBRAR · No válido como comprobante fiscal" cuando fiscal es true). Un CFDI cancelado responde status: cancelled con cancelled_at.

Parámetros de ruta

Nombre Tipo Req. Descripción
document integer sí

Respuestas

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

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

PDF del documento por enlace firmado

GET /documents/{document}/pdf/signed Sin sesión

PDF desde el pdf_url firmado del recurso (válido 30 minutos, sin token).

Parámetros de ruta

Nombre Tipo Req. Descripción
document integer sí

Parámetros de consulta

Nombre Tipo Req. Descripción
expires integer sí
signature string sí

Respuestas

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

$response = $client->get('documents/42/pdf/signed', [
    'query' => [
        'expires' => 'valor',
        'signature' => 'valor',
    ],
]);
$data = json_decode($response->getBody(), true);
Respuesta 200
La respuesta es un archivo (application/pdf).

Enviar el documento por correo

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

Envía el documento con su PDF (y el XML si es un CFDI timbrado).

Parámetros de ruta

Nombre Tipo Req. Descripción
document integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
to array<string<email>> no Predeterminado: el socio de negocio y el contacto del documento
subject string no
message string no
copy_me boolean no

Respuestas

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

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

Referencia

Pagos

Registra cobros contra documentos abiertos o a cuenta, consulta pagos y obtén el recibo o el complemento de pago.

Cobros recibidos

GET /payments

Parámetros de consulta

Nombre Tipo Req. Descripción
mine boolean no
partner_id integer no
from string<date> no
to string<date> no
per_page integer no Por omisión: 50 Máximo: 200
cursor string no meta.next_cursor de la página anterior

Respuestas

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

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

Registrar un cobro

POST /payments Idempotency-Key

Se registra con formas de pago (el efectivo da cambio, se pide referencia cuando la forma de pago la requiere, las notas de crédito sirven como forma de pago) y aplicaciones por documento (amount: null = lo que aún debe). La caja del usuario debe estar abierta para el dinero que entra al cajón. Con on_account: true no necesita documentos y el sobrante queda como saldo sin aplicar del cliente (unapplied_total).

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
business_partner_id integer sí
on_account boolean no Pago a cuenta: no requiere documentos; el resto queda como saldo sin aplicar
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 sí
applications[].amount number | null no null = lo que el documento aún debe
tenders array<object> no
tenders[].tender_type_id integer sí
tenders[].amount number sí
tenders[].reference string no
tenders[].credit_memo_id integer no

Respuestas

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

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

Pago con formas de pago y aplicaciones

GET /payments/{payment}

Parámetros de ruta

Nombre Tipo Req. Descripción
payment integer sí

Respuestas

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

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

PDF del recibo de pago

GET /payments/{payment}/pdf

Parámetros de ruta

Nombre Tipo Req. Descripción
payment integer sí

Parámetros de consulta

Nombre Tipo Req. Descripción
template integer no

Respuestas

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

$response = $client->get('payments/42/pdf', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('NORI_TOKEN'),
    ],
]);
$data = json_decode($response->getBody(), true);
Respuesta 200
La respuesta es un archivo (application/pdf).

Representación impresa del complemento de pago

GET /payments/{payment}/cfdi

Mismo contrato que /documents/{document}/cfdi; payments trae cada pago con sus documentos relacionados (UUID, serie-folio, parcialidad, saldo anterior, pagado, saldo insoluto, objeto de impuesto) y payment_totals los totales del complemento. Un pago que no requiere complemento (PUE, venta de contado) responde fiscal: false. Los pagos efectuados responden 404.

Parámetros de ruta

Nombre Tipo Req. Descripción
payment integer sí

Respuestas

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

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

PDF del recibo por enlace firmado

GET /payments/{payment}/pdf/signed Sin sesión

PDF desde el pdf_url firmado (sin token).

Parámetros de ruta

Nombre Tipo Req. Descripción
payment integer sí

Parámetros de consulta

Nombre Tipo Req. Descripción
expires integer sí
signature string sí

Respuestas

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

$response = $client->get('payments/42/pdf/signed', [
    'query' => [
        'expires' => 'valor',
        'signature' => 'valor',
    ],
]);
$data = json_decode($response->getBody(), true);
Respuesta 200
La respuesta es un archivo (application/pdf).

Enviar el recibo por correo

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

Parámetros de ruta

Nombre Tipo Req. Descripción
payment integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
to array<string<email>> no Predeterminado: el socio de negocio y el contacto del documento
subject string no
message string no
copy_me boolean no

Respuestas

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

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

Referencia

Inventario

Consulta existencias al escanear, revisa lotes por caducar y registra solicitudes de traslado, traslados, entradas y salidas de mercancía.

Lotes con existencia

GET /items/{item}/batches

Parámetros de ruta

Nombre Tipo Req. Descripción
item integer sí

Parámetros de consulta

Nombre Tipo Req. Descripción
warehouse_ids[] array<integer> no

Respuestas

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

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

Números de serie del artículo

GET /items/{item}/serial-numbers

Parámetros de ruta

Nombre Tipo Req. Descripción
item integer sí

Parámetros de consulta

Nombre Tipo Req. Descripción
warehouse_ids[] array<integer> no
status string no Valores: in_stockissuedall Por omisión: in_stock
search string no

Respuestas

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

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

Existencias por ubicación

GET /items/{item}/bin-stock

Parámetros de ruta

Nombre Tipo Req. Descripción
item integer sí

Parámetros de consulta

Nombre Tipo Req. Descripción
warehouse_ids[] array<integer> no

Respuestas

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

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

Ubicaciones activas de un almacén

GET /warehouses/{warehouse}/bins

Ubicaciones tal como las lleva el ERP (subniveles, código de barras, atributos, restricción, mínimo y máximo). Con la integración al ERP activa, se heredan de él.

Parámetros de ruta

Nombre Tipo Req. Descripción
warehouse integer sí

Parámetros de consulta

Nombre Tipo Req. Descripción
code string no Solo la ubicación con este código o código de barras (al escanear la etiqueta del anaquel)

Respuestas

  • 200 Ubicaciones; meta.uses_bin_locations indica si el almacén lleva existencias por ubicación
  • 403 Sobre de error
curl https://demo.nori.software/api/v1/warehouses/42/bins \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/warehouses/42/bins', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

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

Contenido de una ubicación

GET /bins/{bin}/stock

Parámetros de ruta

Nombre Tipo Req. Descripción
bin integer sí

Respuestas

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

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

Consultar un artículo con su existencia

GET /inventory/lookup

Escanea o escribe un artículo ("3*CODIGO", código de barras, etiqueta de báscula) y obtén su existencia.

Parámetros de consulta

Nombre Tipo Req. Descripción
text string no
item_id integer no
warehouse_ids[] array<integer> no

Respuestas

  • 200 Artículo, cantidad y saldos por almacén
  • 422 Sobre de error
curl https://demo.nori.software/api/v1/inventory/lookup \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/inventory/lookup', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

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

Lotes vencidos o por vencer

GET /inventory/expiring

Lotes con existencia que ya vencieron o vencen dentro de los días indicados (el valor de la empresa si se omite).

Parámetros de consulta

Nombre Tipo Req. Descripción
days integer no
warehouse_ids[] array<integer> no

Respuestas

  • 200 Lotes por vencer, los más próximos primero
curl https://demo.nori.software/api/v1/inventory/expiring \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/inventory/expiring', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

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

Solicitudes de traslado pendientes

GET /inventory/transfer-requests/pending

Parámetros de consulta

Nombre Tipo Req. Descripción
warehouse_id integer no
limit integer no Por omisión: 50

Respuestas

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

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

Partidas abiertas de una solicitud

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

Partidas abiertas de una solicitud, listas para trasladar (base_line_id).

Parámetros de ruta

Nombre Tipo Req. Descripción
document integer sí

Respuestas

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

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

Registrar un movimiento de inventario

POST /inventory/{movement} Idempotency-Key

Solicitud de traslado, traslado, entrada o salida de mercancía. Las entradas y salidas requieren una causa de su sentido; las entradas, el costo por unidad. Un traslado desde una solicitud envía base_line_id en cada partida.

Parámetros de ruta

Nombre Tipo Req. Descripción
movement string sí Valores: transfer-requeststransfersgoods-receiptsgoods-issues

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
draft boolean no
document_series_id integer no
document_date string<date> no
warehouse_id integer sí
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> sí
lines[].item_id integer sí
lines[].quantity number sí
lines[].uom_id integer no
lines[].warehouse_id integer no
lines[].to_warehouse_id integer no
lines[].cost number no Entradas de mercancía: costo por unidad (obligatorio)
lines[].base_line_id integer no Traslados: partida de la solicitud
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 Lotes, números de serie y ubicaciones de la partida
lines[].allocations[].item_id integer no Un componente de un kit; si se omite, el artículo de la partida
lines[].allocations[].batch_id integer no
lines[].allocations[].batch_number string no Entradas: se crea un lote nuevo con él
lines[].allocations[].serial_number_id integer no
lines[].allocations[].serial_number string no
lines[].allocations[].bin_location_id integer no Ubicación a la que entra (entradas) o de la que sale (salidas, traslados)
lines[].allocations[].to_bin_location_id integer no Traslados: ubicación destino (mismo almacén = de ubicación a ubicación)
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

Respuestas

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

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

Referencia

Conteos físicos

Únete a un conteo de inventario, registra lo encontrado por artículo, lote o ubicación y revisa el avance.

Conteos físicos

GET /inventory/counts

Conteos (en curso por omisión) con su avance.

Parámetros de consulta

Nombre Tipo Req. Descripción
status string no Valores: countingpostedcancelled
warehouse_id integer no

Respuestas

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

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

Unirse a un conteo

GET /inventory/counts/{count}

Parámetros de ruta

Nombre Tipo Req. Descripción
count integer sí

Respuestas

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

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

Avance del conteo

GET /inventory/counts/{count}/progress

Partidas totales y partidas contadas.

Parámetros de ruta

Nombre Tipo Req. Descripción
count integer sí

Respuestas

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

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

Partidas del conteo

GET /inventory/counts/{count}/lines

Partidas del conteo (la cantidad en sistema se oculta en un conteo a ciegas).

Parámetros de ruta

Nombre Tipo Req. Descripción
count integer sí

Parámetros de consulta

Nombre Tipo Req. Descripción
counted boolean no
per_page integer no Por omisión: 50 Máximo: 200
cursor string no meta.next_cursor de la página anterior

Respuestas

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

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

Últimas capturas

GET /inventory/counts/{count}/entries

Últimas capturas (solo las mías con mine=1).

Parámetros de ruta

Nombre Tipo Req. Descripción
count integer sí

Parámetros de consulta

Nombre Tipo Req. Descripción
mine boolean no
limit integer no Por omisión: 100

Respuestas

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

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

Registrar lo contado

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

Registra lo encontrado (el escaneo suma; mode=set reemplaza).

Parámetros de ruta

Nombre Tipo Req. Descripción
count integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
text string no Escaneo o "3*CODIGO"; el código de barras de una caja cuenta sus unidades
item_id integer no
quantity number no
mode string no Valores: addset Por omisión: add
bin_location_id integer no Ubicación contada (almacenes con ubicaciones)
batch_id integer no Lote contado (artículos con lote)
batch_number string no Lote contado por número; se crea si no existe
expires_on string<date> no Caducidad de un lote encontrado durante el conteo

Respuestas

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

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

Deshacer la última captura

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

Revierte la última captura de una partida.

Parámetros de ruta

Nombre Tipo Req. Descripción
count integer sí
line integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Respuestas

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

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

Referencia

Venta en ruta

Opera el día de ruta: inicio y cierre, visitas, ventas, pedidos, entregas, cobros, recolecciones, servicio a equipos y fotos.

Rutas del día

GET /route/today

Las rutas del día del usuario con paradas, motivos y formas de pago.

Parámetros de consulta

Nombre Tipo Req. Descripción
date string<date> no

Respuestas

  • 200 Planes y catálogos (sin envolver en data)
  • 403 Sobre de error
curl https://demo.nori.software/api/v1/route/today \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/route/today', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

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

Un día de ruta

GET /route/plans/{plan}

Parámetros de ruta

Nombre Tipo Req. Descripción
plan integer sí

Respuestas

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

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

Carga del camión

GET /route/plans/{plan}/stock

Lo que lleva el camión.

Parámetros de ruta

Nombre Tipo Req. Descripción
plan integer sí

Respuestas

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

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

Iniciar el día

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

Inicia el día (abre la caja del vendedor con el fondo).

Parámetros de ruta

Nombre Tipo Req. Descripción
plan integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Cuerpo de la petición

Nombre Tipo Req. Descripción
fund number no

Respuestas

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

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

Regreso al almacén

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

Cierra el día al volver al almacén.

Parámetros de ruta

Nombre Tipo Req. Descripción
plan integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Respuestas

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

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

Agregar una parada no planeada

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

Parámetros de ruta

Nombre Tipo Req. Descripción
plan integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Cuerpo de la petición

Nombre Tipo Req. Descripción
business_partner_id integer sí
reason_ids array<integer> no

Respuestas

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

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

Clientes para una parada no planeada

GET /route/customers

Parámetros de consulta

Nombre Tipo Req. Descripción
search string no

Respuestas

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

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

Detalle de una parada

GET /route/visits/{visit}

Una parada con pedidos por entregar, saldo, equipos, sugerido y lo que ya se hizo.

Parámetros de ruta

Nombre Tipo Req. Descripción
visit integer sí

Respuestas

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

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

Registrar llegada

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

Llegada con GPS y hora del dispositivo.

Parámetros de ruta

Nombre Tipo Req. Descripción
visit integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Cuerpo de la petición

Nombre Tipo Req. Descripción
lat number no
lng number no
at string<date-time> no Hora del dispositivo

Respuestas

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

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

Registrar salida

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

Salida de la parada (una parada de venta sin venta necesita antes un motivo de no venta).

Parámetros de ruta

Nombre Tipo Req. Descripción
visit integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Cuerpo de la petición

Nombre Tipo Req. Descripción
lat number no
lng number no
at string<date-time> no Hora del dispositivo
notes string no

Respuestas

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

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

Parada no visitada

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

Marca la parada como no visitada, con motivo.

Parámetros de ruta

Nombre Tipo Req. Descripción
visit integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Cuerpo de la petición

Nombre Tipo Req. Descripción
reason_id integer sí
notes string no

Respuestas

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

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

Visita sin venta

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

Visitado sin venta, con motivo.

Parámetros de ruta

Nombre Tipo Req. Descripción
visit integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Cuerpo de la petición

Nombre Tipo Req. Descripción
reason_id integer sí
notes string no

Respuestas

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

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

Cotizar el carrito

POST /route/visits/{visit}/quote

Precios y totales del motor para un carrito antes de vender.

Parámetros de ruta

Nombre Tipo Req. Descripción
visit integer sí

Cuerpo de la petición

Nombre Tipo Req. Descripción
kind string no Valores: saleorderpickup
lines array<object> no
lines[].item_id integer sí
lines[].quantity number sí
lines[].uom_id integer no

Respuestas

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

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

Venta desde el camión

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

Venta desde el camión (remisión o factura), de contado o a crédito.

Parámetros de ruta

Nombre Tipo Req. Descripción
visit integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Cuerpo de la petición

Nombre Tipo Req. Descripción
lines array<object> sí
lines[].item_id integer sí
lines[].quantity number sí
lines[].uom_id integer no
tenders array<object> no
tenders[].tender_type_id integer sí
tenders[].amount number sí
tenders[].reference string no
tenders[].credit_memo_id integer no

Respuestas

  • 200 Documentos y pago hechos en la parada (sin envolver)
  • 422 Sobre de error
curl -X POST https://demo.nori.software/api/v1/route/visits/42/sale \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "lines": [
      {
        "item_id": 5,
        "quantity": 2
      }
    ],
    "tenders": [
      {
        "tender_type_id": 1,
        "amount": 46.4
      }
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/sale', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lines": [
      {
        "item_id": 5,
        "quantity": 2
      }
    ],
    "tenders": [
      {
        "tender_type_id": 1,
        "amount": 46.4
      }
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

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

Pedido de preventa

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

Pedido de preventa que se entrega en la siguiente visita.

Parámetros de ruta

Nombre Tipo Req. Descripción
visit integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Cuerpo de la petición

Nombre Tipo Req. Descripción
lines array<object> sí
lines[].item_id integer sí
lines[].quantity number sí
lines[].uom_id integer no
delivery_date string<date> no

Respuestas

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

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

Entregar un pedido

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

Entrega un pedido desde el camión (se permite parcial) y, si se quiere, cóbralo.

Parámetros de ruta

Nombre Tipo Req. Descripción
visit integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Cuerpo de la petición

Nombre Tipo Req. Descripción
lines array<object> sí
lines[].base_line_id integer no
lines[].quantity number no
tenders array<object> no
tenders[].tender_type_id integer sí
tenders[].amount number sí
tenders[].reference string no
tenders[].credit_memo_id integer no

Respuestas

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

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

Recolectar producto

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

Recolecta productos (caducados, calidad) al compartimento de devoluciones.

Parámetros de ruta

Nombre Tipo Req. Descripción
visit integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Cuerpo de la petición

Nombre Tipo Req. Descripción
lines array<object> sí
lines[].item_id integer no
lines[].quantity number no
lines[].reason_id integer no

Respuestas

  • 200 Documentos y pago hechos en la parada (sin envolver)
curl -X POST https://demo.nori.software/api/v1/route/visits/42/pickup \
  -H "Authorization: Bearer $NORI_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "lines": [
      {
        "item_id": 42,
        "quantity": 1.5,
        "reason_id": 42
      }
    ]
  }'
const response = await fetch('https://demo.nori.software/api/v1/route/visits/42/pickup', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "lines": [
      {
        "item_id": 42,
        "quantity": 1.5,
        "reason_id": 42
      }
    ]
  }),
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

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

Cobrar en la parada

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

Cobra los documentos elegidos o los más antiguos.

Parámetros de ruta

Nombre Tipo Req. Descripción
visit integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Cuerpo de la petición

Nombre Tipo Req. Descripción
tenders array<object> sí
tenders[].tender_type_id integer sí
tenders[].amount number sí
tenders[].reference string no
tenders[].credit_memo_id integer no
document_ids array<integer> no

Respuestas

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

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

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

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

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

Parámetros de ruta

Nombre Tipo Req. Descripción
visit integer sí
shipment integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Cuerpo de la petición · multipart/form-data

Nombre Tipo Req. Descripción
received_by string sí
notes string no
lat number no
lng number no
photo string<binary> no
tenders array<object> no
tenders[].tender_type_id integer sí
tenders[].amount number sí
tenders[].reference string no
tenders[].credit_memo_id integer no

Respuestas

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

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

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

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

Parámetros de ruta

Nombre Tipo Req. Descripción
visit integer sí
shipment integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Cuerpo de la petición

Nombre Tipo Req. Descripción
reason string sí
notes string no

Respuestas

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

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

Servicio a equipo

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

Servicio a refrigerador o exhibidor con fotos (multipart).

Parámetros de ruta

Nombre Tipo Req. Descripción
visit integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Cuerpo de la petición · multipart/form-data

Nombre Tipo Req. Descripción
customer_asset_id integer sí
kind string sí Valores: preventivecorrectiveinstallationremovalinspection
condition string sí Valores: workingneeds_repairout_of_service
temperature number no
notes string no
performed_at string<date-time> no
photos[] array<string<binary>> no

Respuestas

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

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

Foto de la visita

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

Foto de la visita (multipart).

Parámetros de ruta

Nombre Tipo Req. Descripción
visit integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> no Opcional en los endpoints de ruta; también se acepta como el campo "key".

Cuerpo de la petición · multipart/form-data

Nombre Tipo Req. Descripción
photo string<binary> sí
kind string no
caption string no
customer_asset_id integer no

Respuestas

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

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

Enviar la cola de ruta

POST /route/sync

Cola sin conexión de la venta en ruta (acciones por nombre; /sync/push es la general).

Cuerpo de la petición

Nombre Tipo Req. Descripción
operations array<object> sí
operations[].key string<uuid> sí
operations[].action string sí Valores: 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

Respuestas

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

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

Referencia

Supervisor

Indicadores de ventas, cobranza y cartera del día y del mes, por vendedor y sucursal.

Indicadores de ventas y cobranza

GET /kpis

Ventas del día y del mes contra el periodo anterior, cobranza, cuentas por cobrar, vencido, antigüedad de saldos, artículos más vendidos, por vendedor y sucursal. Las secciones que el usuario no puede ver llegan en null. Un vendedor sin approvals.grant solo ve sus propias ventas.

Parámetros de consulta

Nombre Tipo Req. Descripción
branch_id integer no
sales_employee_id integer no
top integer no Por omisión: 10

Respuestas

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

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

Referencia

Sincronización

Descarga los catálogos para trabajar sin conexión por cambios incrementales y envía la cola de operaciones pendientes.

Entidades para trabajar sin conexión

GET /sync

Respuestas

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

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

Cambios de un catálogo

GET /sync/{entity}

Una página de cambios de un catálogo desde un token de sincronización. Sin updated_since (o con uno más antiguo que los registros de borrado conservados), meta.reset es true: vacía la tabla local y toma todo. Sigue meta.next_cursor mientras meta.has_more sea true; después guarda meta.sync_token para el siguiente delta. Aplica deleted (ids) antes que data (altas y cambios).

Parámetros de ruta

Nombre Tipo Req. Descripción
entity string sí Valores: itemsitem_pricesstockcustomersprice_liststender_typeswarehousescausalitiesvisit_reasons

Parámetros de consulta

Nombre Tipo Req. Descripción
updated_since string<date-time> no El sync_token de la última sincronización completa
cursor string no
limit integer no Por omisión: 500 Máximo: 2000
scope string no customers: solo los míos Valores: allmine
warehouse_ids[] array<integer> no stock: estos almacenes

Respuestas

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

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

Enviar la cola sin conexión

POST /sync/push

Envía la cola sin conexión (escrituras de esta API, en orden). Cada operación se ejecuta con su llave como Idempotency-Key. Si una falla, las demás continúan. Una cadena "$ref:<key>:<path>" en un cuerpo toma ese valor de una respuesta anterior.

Cuerpo de la petición

Nombre Tipo Req. Descripción
operations array<object> sí
operations[].key string<uuid> sí
operations[].method string sí Valores: POSTPUTPATCHDELETE
operations[].path string sí
operations[].body object no

Respuestas

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

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

Referencia

Asistente

Haz preguntas al asistente de IA y consulta tus conversaciones y las preguntas sugeridas.

Preguntar al asistente

POST /assistant/messages Idempotency-Key

Pregunta al asistente de IA (sin conversation_id inicia una conversación nueva).

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
conversation_id integer no
message string sí

Respuestas

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

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

Mis conversaciones

GET /assistant/conversations

Mis últimas 50 conversaciones.

Respuestas

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

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

Mensajes de una conversación

GET /assistant/conversations/{conversation}/messages

Parámetros de ruta

Nombre Tipo Req. Descripción
conversation integer sí

Respuestas

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

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

Preguntas sugeridas

GET /assistant/templates

Respuestas

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

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

Referencia

Autorizaciones

Solicita la autorización de un supervisor, apruébala en sitio o decide las solicitudes pendientes.

Solicitudes por decidir

GET /authorizations/pending

Respuestas

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

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

Mis solicitudes

GET /authorizations/mine

Respuestas

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

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

Pedir autorización a un supervisor

POST /authorizations Idempotency-Key

Pide autorización a un supervisor con el token de un error authorization_required.

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Respuestas

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

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

Autorizar en sitio

POST /authorizations/on-site Idempotency-Key

Aprueba en sitio con las credenciales o el PIN de un supervisor.

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Respuestas

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

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

Estado de una solicitud

GET /authorizations/{authorization}

Parámetros de ruta

Nombre Tipo Req. Descripción
authorization integer sí

Respuestas

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

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

Aprobar o rechazar una solicitud

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

Parámetros de ruta

Nombre Tipo Req. Descripción
authorization integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Respuestas

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

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

Referencia

CRM

Trabaja tus oportunidades: embudos y etapas con sus totales, alta y edición, cambio de etapa, ganadas y perdidas con motivo, actividades, seguimientos, tu agenda y los documentos ligados.

Embudos con sus etapas

GET /crm/pipelines

Embudos activos, el predeterminado primero, cada uno con sus etapas en el orden del tablero. outcome indica si una etapa está abierta, es la de ganada o la de perdida.

Respuestas

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

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

Cantidad, monto y monto ponderado por etapa

GET /crm/pipelines/{pipeline}/totals

Los mismos totales e indicadores que el tablero de la web, con sus filtros y lo que el usuario puede ver. Las etapas abiertas cuentan oportunidades abiertas; las de ganada y perdida, lo que se cerró este mes.

Parámetros de ruta

Nombre Tipo Req. Descripción
pipeline integer sí

Parámetros de consulta

Nombre Tipo Req. Descripción
scope string no mine: solo las oportunidades de las que el usuario es responsable; all requiere crm_deals.view_all Valores: mineall
search string no
tag_id integer no
partner_id integer no
overdue boolean no Solo oportunidades con un seguimiento vencido

Respuestas

  • 200 Totales por etapa e indicadores
  • 403 Sobre de error
  • 404 Sobre de error
curl https://demo.nori.software/api/v1/crm/pipelines/42/totals?search=manzana \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/crm/pipelines/42/totals?search=manzana', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

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

Motivos de pérdida activos

GET /crm/loss-reasons

Respuestas

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

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

Etiquetas de oportunidades

GET /crm/tags

Respuestas

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

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

Oportunidades que puede ver el usuario

GET /crm/deals

Sin crm_deals.view_all, solo las del usuario (responsable o de su empleado de ventas). Para trabajar sin conexión, guarda meta.server_time y mándalo como updated_since la siguiente vez: recibes lo que cambió desde entonces (también las cerradas) y en meta.deleted los ids que se borraron.

Parámetros de consulta

Nombre Tipo Req. Descripción
scope string no mine: solo las oportunidades de las que el usuario es responsable; all requiere crm_deals.view_all (si no, 403). Por omisión: todo lo que el usuario puede ver Valores: mineall
status string no Por omisión: todos los estados Valores: openwonlost
pipeline_id integer no
stage_id integer no
partner_id integer no
tag_id integer no
owner_id integer no Solo con crm_deals.view_all
overdue boolean no Solo oportunidades con un seguimiento vencido
search string no Título, nombre del cliente, nombre comercial o código
updated_since string<date-time> no meta.server_time de la descarga anterior (inclusive)
per_page integer no Por omisión: 50 Máximo: 200
cursor string no meta.next_cursor de la página anterior

Respuestas

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

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

Crear una oportunidad

POST /crm/deals Idempotency-Key

Sin embudo va al predeterminado; sin etapa, a su primera etapa abierta. El título es el nombre del cliente si no se manda, y el responsable es el usuario (solo quien tiene crm_deals.view_all asigna una oportunidad a alguien más). Corren las automatizaciones de "oportunidad creada" igual que en la web.

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
business_partner_id integer no Obligatorio al crear
title string no Por omisión: el nombre del cliente
crm_pipeline_id integer no Solo al crear. Por omisión: el embudo predeterminado
crm_stage_id integer no Solo al crear, una etapa de ese embudo. Por omisión: su primera etapa abierta
partner_contact_id integer | null no
owner_id integer | null no Solo con crm_deals.view_all; si no, el usuario
amount number no
currency_id integer no
probability number | null no 0 a 100; null = la de la etapa
expected_close_date string<date> | null no
source string no
description string no
tag_ids array<integer> no Reemplaza las etiquetas

Respuestas

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

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

Oportunidad con sus etapas, documentos y línea de tiempo

GET /crm/deals/{deal}

Parámetros de ruta

Nombre Tipo Req. Descripción
deal integer sí

Respuestas

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

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

Editar una oportunidad

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

Solo cambian los campos enviados. tag_ids reemplaza las etiquetas. El embudo y la etapa cambian con /move.

Parámetros de ruta

Nombre Tipo Req. Descripción
deal integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
business_partner_id integer no Obligatorio al crear
title string no Por omisión: el nombre del cliente
crm_pipeline_id integer no Solo al crear. Por omisión: el embudo predeterminado
crm_stage_id integer no Solo al crear, una etapa de ese embudo. Por omisión: su primera etapa abierta
partner_contact_id integer | null no
owner_id integer | null no Solo con crm_deals.view_all; si no, el usuario
amount number no
currency_id integer no
probability number | null no 0 a 100; null = la de la etapa
expected_close_date string<date> | null no
source string no
description string no
tag_ids array<integer> no Reemplaza las etiquetas

Respuestas

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

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

Mover una oportunidad de etapa

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

A cualquier etapa, de su embudo o de otro. Una etapa de ganada o perdida cierra la oportunidad (perdida, con su motivo); una etapa abierta la reabre. El cambio queda en la línea de tiempo y dispara las automatizaciones.

Parámetros de ruta

Nombre Tipo Req. Descripción
deal integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
stage_id integer sí
comment string no Se escribe con el cambio de etapa en la línea de tiempo
loss_reason_id integer no Cuando la etapa es la de perdida
loss_notes string no

Respuestas

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

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

Marcar la oportunidad como ganada

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

La mueve a la etapa de ganada de su embudo.

Parámetros de ruta

Nombre Tipo Req. Descripción
deal integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Respuestas

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

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

Marcar la oportunidad como perdida, con el motivo

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

La mueve a la etapa de perdida de su embudo. El motivo es obligatorio salvo que las notas lo expliquen.

Parámetros de ruta

Nombre Tipo Req. Descripción
deal integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
loss_reason_id integer no
loss_notes string no

Respuestas

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

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

Reabrir una oportunidad ganada o perdida

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

Regresa a la primera etapa abierta de su embudo.

Parámetros de ruta

Nombre Tipo Req. Descripción
deal integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Respuestas

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

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

Registrar o programar una actividad

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

Sin due_at se registra como hecha en ese momento (una llamada, una visita, una nota). Con due_at es un seguimiento, pendiente hasta completarlo y asignado al responsable de la oportunidad salvo que assigned_to diga otra cosa; remind_minutes pone el recordatorio esos minutos antes. El asunto es el nombre del tipo si no se manda.

Parámetros de ruta

Nombre Tipo Req. Descripción
deal integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
type string sí Valores: notecallemailwhatsappmeetingvisittask
subject string no Por omisión: el nombre del tipo
body string no
due_at string<date-time> no La vuelve un seguimiento
remind_at string<date-time> no
remind_minutes integer no Recordatorio estos minutos antes de due_at
assigned_to integer no
completed boolean no Un seguimiento que ya se hizo

Respuestas

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

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

Completar, reabrir, reprogramar o editar una actividad

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

complete (con un outcome opcional que se agrega a sus notas), reopen y reschedule (due_at) aplican a seguimientos; edit cambia el asunto y el texto de lo que registró una persona. Lo que escribe Nori (cambios de etapa, documentos, automatizaciones) no se edita.

Parámetros de ruta

Nombre Tipo Req. Descripción
activity integer sí

Encabezados

Nombre Tipo Req. Descripción
Idempotency-Key string<uuid> sí UUID que genera la app cuando el usuario actúa y que se guarda con la operación en su cola.

Cuerpo de la petición

Nombre Tipo Req. Descripción
action string sí Valores: completereopenrescheduleedit
outcome string no
due_at string<date-time> no Obligatorio para reprogramar
subject string no Obligatorio para editar
body string no

Respuestas

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

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

Mi agenda: seguimientos por hacer

GET /crm/agenda

Seguimientos pendientes de oportunidades abiertas asignados al usuario, los más próximos primero, hasta days días adelante; cada uno con su bucket (overdue, today, upcoming) y su oportunidad y cliente. scope=all (con crm_deals.view_all) lista los de todos.

Parámetros de consulta

Nombre Tipo Req. Descripción
scope string no Valores: mineall
days integer no Cuántos días adelante Por omisión: 14 Máximo: 90

Respuestas

  • 200 Seguimientos y conteo por grupo
  • 403 Sobre de error
curl https://demo.nori.software/api/v1/crm/agenda \
  -H "Authorization: Bearer $NORI_TOKEN"
const response = await fetch('https://demo.nori.software/api/v1/crm/agenda', {
  headers: {
    Authorization: `Bearer ${process.env.NORI_TOKEN}`,
  },
});
const data = await response.json();
$client = new GuzzleHttp\Client(['base_uri' => 'https://demo.nori.software/api/v1/']);

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