API de OnBox
Una sola API para conectar tu sistema con el nuestro en las dos direcciones: nos mandás pedidos, te devolvemos estado, stock y seguimiento, y te avisamos apenas algo cambia.
Es REST sobre HTTPS, con JSON en los dos sentidos y autenticación por clave. No hay SDK que instalar:
si podés hacer un POST, ya podés integrarte.
Lo que resuelve
| Nos mandás pedidos | Un POST y el pedido queda cargado con el stock reservado. Idempotente: un reintento nunca duplica. |
| Te devolvemos estado | Consultás cuando querés, o te avisamos nosotros por webhook. Recomendamos webhook. |
| Stock en vivo | Lo que realmente se puede vender, ya descontado lo reservado. |
| Seguimiento | Estado del transportista y línea de tiempo del envío. |
Base URL
https://api.onboxservicioslogisticos.com
Todas las rutas cuelgan de /v1. Las respuestas siempre traen ok:
si es true mirás data, si es false mirás error.
{
"ok": true,
"data": { ... }
}
{
"ok": false,
"error": {
"code": "unknown_sku",
"message": "...",
"details": [ ... ]
}
}
Toda respuesta incluye el header X-Request-Id. Guardalo en tus logs: con ese identificador
ubicamos la llamada exacta, sin adivinar.
Empezar en 5 minutos
Cuatro llamadas y ya tenés la integración funcionando de punta a punta.
1 · Verificá la clave
curl https://api.onboxservicioslogisticos.com/v1/ping \
-H "Authorization: Bearer obx_test_tuClave"
Te dice a qué cuenta pertenece, qué permisos tiene y si es de prueba o de producción.
2 · Mirá qué SKU tenés
curl https://api.onboxservicioslogisticos.com/v1/stock?limit=10 \
-H "Authorization: Bearer obx_test_tuClave"
Los SKU del alta tienen que existir en tu catálogo. Si no existen, rechazamos el pedido entero.
3 · Creá un pedido
curl -X POST https://api.onboxservicioslogisticos.com/v1/orders \
-H "Authorization: Bearer obx_test_tuClave" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: mi-primer-pedido" \
-d '{
"externalId": "MI-0001",
"buyer": { "name": "Juan Perez", "phone": "1155667788" },
"shipping": { "address": "Av. Mitre 750", "city": "Avellaneda", "zip": "1870" },
"items": [ { "sku": "TU-SKU", "quantity": 1, "unitPrice": 15400 } ]
}'
Mandá la misma llamada dos veces: la segunda te devuelve duplicate: true y el mismo pedido.
Eso es lo que te protege de los timeouts.
4 · Enterate de los cambios
curl -X POST https://api.onboxservicioslogisticos.com/v1/webhooks \
-H "Authorization: Bearer obx_test_tuClave" \
-H "Content-Type: application/json" \
-d '{ "url": "https://tu-sistema.com/hooks", "events": ["*"] }'
Guardá el secret que te devolvemos: se muestra una sola vez.
Todo esto con clave de prueba no toca producción. Los pedidos son simulados y no mueven stock real. Cuando funcione, cambiás la clave por la de producción y no tocás una línea más de código.
Autenticación
Una clave por integración, en un header. Sin OAuth, sin tokens que vencen a mitad de un proceso.
Authorization: Bearer obx_live_tuClaveSecreta
Dos entornos, dos claves
| Clave | Qué hace |
|---|---|
| obx_test_… | Entorno de prueba. Valida todo igual que producción, pero los pedidos son simulados y no mueven stock ni llegan al depósito. |
| obx_live_… | Producción. Lo que hacés acá pasa de verdad. |
El endpoint /v1/ping te dice en cuál estás, en el campo mode. Si tu código
no está seguro de qué clave cargó, preguntale a la API antes de operar.
Permisos
Cada clave lleva sólo los permisos que necesita. Si le falta uno, la llamada devuelve 403 forbidden
diciendo exactamente cuál falta.
| Permiso | Habilita |
|---|---|
orders:read | Leer pedidos y sus líneas. |
orders:write | Crear y cancelar pedidos. |
stock:read | Consultar stock disponible por SKU. |
tracking:read | Consultar el estado y la línea de tiempo de un envío. |
webhooks:write | Registrar, probar y dar de baja webhooks. |
Vencimiento y rotación
Una clave puede tener fecha de vencimiento. Si venció, devolvemos 401 key_expired: no es que
falle la llamada, es que hay que renovarla. Para rotar sin ventana de corte emitimos la nueva y dejamos la
anterior viva 24 horas; en ese lapso las dos funcionan y podés desplegar tranquilo.
Seguridad
Lo que hacemos de nuestro lado, y lo que necesitamos del tuyo.
De nuestro lado
- La clave se guarda hasheada con SHA-256. Ni nosotros podemos volver a verla: si se pierde, se revoca y se emite otra.
- Cada clave está atada a una cuenta y sólo ve los datos de esa cuenta. No hay forma de leer pedidos ajenos.
- Podemos atar una clave a una lista de IPs. Si la llamada viene de otra, devolvemos
403 ip_not_allowed. - Los avisos van firmados con HMAC-SHA256 para que puedas comprobar que salieron de nosotros.
- Queda registro de cada llamada con su identificador, código y duración. Vos lo ves en
/v1/logs. - Sólo HTTPS. Los webhooks también: no aceptamos una URL
http://.
De tu lado
- La clave va en el servidor. Nunca en una app móvil, en el navegador ni en un repositorio.
- Validá la firma de cada webhook antes de creerle. Está explicado más abajo, con código.
- Rechazá lo viejo: si el
tde la firma tiene más de 5 minutos, descartá el aviso. - Si sospechás que la clave se filtró, avisanos: la revocamos en el momento.
Nunca te vamos a pedir la clave por mail ni por teléfono. Si alguien lo hace, no es OnBox.
Entorno de prueba
Integrá completo sin ensuciar producción ni mover una unidad de stock.
Con una clave obx_test_… la API se comporta igual: mismas validaciones, mismos
errores, mismos formatos. La diferencia es que los pedidos quedan en un espacio aparte, no llegan al depósito
y no tocan el stock real. El stock que consultás sí es el de verdad, para que trabajes con datos creíbles.
Simular el recorrido de un pedido
En producción el estado lo mueve la operación. En prueba lo movés vos, y los webhooks salen igual que si fuera real:
curl -X POST https://api.onboxservicioslogisticos.com/v1/sandbox/orders/MI-0001/advance \
-H "Authorization: Bearer obx_test_tuClave" \
-H "Content-Type: application/json" \
-d '{ "status": "Despachado", "tracking": "OCA-123456" }'
Sin status avanza al siguiente estado natural: Pendiente → En preparación → Despachado →
En camino → Entregado. Así probás la secuencia completa en cuatro llamadas.
Ver qué te mandamos
Si todavía no tenés endpoint donde recibir, apuntá el webhook a https://api.onboxservicioslogisticos.com/dev/echo
y consultá GET /dev/echo: ahí ves el aviso tal cual sale, con la firma ya verificada de nuestro
lado. Sirve para comparar contra tu implementación cuando la firma no te da.
Los datos de prueba se borran solos a los 30 días. No los uses como registro de nada.
Estados de un pedido
Un pedido viaja por una serie de estados. Integrá tu lógica contra el código estable, no contra el texto.
Cada pedido trae dos campos de estado:
status— el texto en castellano que ve nuestra operación (Pendiente, En preparación, Despachado…). Es para mostrar. Puede cambiar de redacción sin aviso.statusCode— un código estable que no cambia nunca. Es contra este que tenés que programar.
| statusCode | Qué significa |
|---|---|
pending | Entró el pedido y todavía no se preparó. |
processing | Se está preparando / armando en el depósito. |
shipped | Despachado: salió del depósito, ya tiene guía asignada. |
in_transit | En camino / en reparto hacia el destino. |
delivered | Entregado al destinatario. |
cancelled | Anulado, cancelado o rechazado. |
returned | Devuelto al depósito. |
unknown | Un estado que todavía no mapeamos a un código. Tratalo como “sin cambios”. |
El statusCode viene en el detalle del pedido, en el listado, en el seguimiento y en el
data de cada webhook (junto con previousStatusCode, el estado anterior).
El camino habitual es
pending → processing → shipped → in_transit → delivered. No todos los pedidos pasan por todos
los estados, y algunos derivan a cancelled o returned. Si te llega un
unknown, tratalo como “sin cambios” hasta el próximo aviso: nunca rompas por un estado que no
conocés.
Idempotencia
Reintentar tiene que ser seguro. Acá lo es, por diseño.
Un timeout no significa que la operación falló: puede haber entrado igual. Por eso el alta de pedidos está protegida dos veces:
- Por
externalId. Es el identificador del pedido en tu sistema. Si mandás uno que ya existe, no creamos otro: te devolvemos el que hay, conduplicate: truey HTTP200. - Por
Idempotency-Key. Si mandás ese header, guardamos la respuesta exacta de ese intento y ante un reenvío te devolvemos la misma, byte por byte.
La consecuencia práctica: ante cualquier error de red o un 5xx, reintentá. Es la conducta correcta y no vas a duplicar pedidos ni descontar stock dos veces.
Un externalId por pedido, para siempre.
Si reutilizás uno viejo para un pedido nuevo, te vamos a devolver el viejo y vas a creer que cargaste algo
que no cargaste.
Las claves de idempotencia se conservan 7 días. Después, el que sigue mandando es el externalId.
Paginación y sincronización
Dos formas de leer: recorrer todo una vez, o traer sólo lo que cambió.
Recorrer
Pedís una página, y si paging.nextCursor no es null, volvés a llamar pasándolo
en cursor. Cuando viene null, terminaste.
GET /v1/orders?limit=100
GET /v1/orders?limit=100&cursor=89696349
Sincronizar
Para mantenerte al día no barras todo: pedí sólo lo modificado desde la última vez.
GET /v1/orders?updatedSince=2026-08-18T13:00:00Z&limit=200
Guardá el updatedAt más alto que recibiste y usalo en la llamada siguiente. Si te vuelve una
página llena, seguí paginando antes de dormir.
Mejor todavía: usá webhooks. Sincronizar por consulta te deja siempre un poco atrasado y gasta llamadas de tu límite. Con webhooks te enterás en segundos. Lo ideal es tener los dos: el webhook para el día a día y una sincronización cada tanto como red de seguridad.
Límites de uso
| Llamadas | 120 por minuto por clave. Se puede subir: pedilo y lo ajustamos. |
| Cuerpo | Hasta 512 KB por llamada. |
| Items por pedido | Hasta 200. |
| Pedidos por página | Hasta 200. Stock, hasta 500. |
Si te pasás del límite devolvemos 429 con el header Retry-After en segundos.
Esperá eso y reintentá; como todo es idempotente, reintentar no rompe nada.
En cada respuesta va X-RateLimit-Remaining: cuántas llamadas te quedan en el minuto en curso.
Errores
Todos los errores que la API puede devolver. Si te encontrás con uno que no está acá, es un
bug nuestro: escribinos con el X-Request-Id.
El formato es siempre el mismo. Cuando el error es de datos, details trae la lista exacta de
qué está mal, campo por campo: no tenés que adivinar.
{
"ok": false,
"error": {
"code": "validation_error",
"message": "El pedido tiene campos inválidos.",
"details": [
{ "field": "buyer.name", "message": "Requerido: nombre de quien recibe." },
{ "field": "items[0].quantity", "message": "Entero mayor a cero." }
]
}
}
| HTTP | Código | Cuándo | Qué hacer |
|---|---|---|---|
| 400 | bad_request | El cuerpo no es un JSON válido o falta un parámetro obligatorio. | Revisá el JSON y el Content-Type. |
| 401 | unauthorized | Falta la API key, está mal escrita o fue revocada. | Mandá el header Authorization: Bearer … |
| 401 | key_expired | La clave tenía fecha de vencimiento y ya pasó. | Pedinos una clave nueva. |
| 403 | ip_not_allowed | La clave tiene lista de IPs permitidas y la llamada vino de otra. | Avisanos desde qué IP salen. |
| 403 | forbidden | La clave no tiene el permiso que ese endpoint requiere. | Pedinos que le sumemos el permiso. |
| 404 | not_found | El pedido, envío o webhook no existe (o no es tuyo). | Verificá el identificador. |
| 405 | method_not_allowed | La ruta existe pero no con ese método. | Mirá la referencia. |
| 409 | cannot_cancel | El pedido ya salió de la etapa en la que se puede anular solo. | Escribinos: lo resolvemos a mano para que el stock no quede mal. |
| 409 | insufficient_stock | No hay stock para todo lo pedido y mandaste rejectIfNoStock. | Sacá el item o bajá la cantidad. En error.details va cuánto hay. |
| 413 | payload_too_large | El cuerpo supera 512 KB. | Partí el pedido o sacá texto innecesario. |
| 422 | validation_error | Falta un campo o tiene un valor inválido. | En error.details va la lista exacta, campo por campo. |
| 422 | unknown_sku | Uno o más SKU no existen en tu catálogo. | Se rechaza el pedido entero: no cargamos pedidos a medias. En error.details van los SKU. |
| 429 | rate_limited | Superaste las llamadas por minuto de tu clave. | Esperá lo que dice Retry-After y reintentá. Todo es idempotente: reintentar es seguro. |
| 500 | server_error | Se rompió algo nuestro. | Reintentá. Si sigue, mandanos el X-Request-Id. |
| 502 | upstream_error | Nuestro sistema interno no respondió a tiempo. | Reintentá con la misma Idempotency-Key: no se duplica. |
Qué reintentar
Sí: 429, 500, 502 y cualquier error de red — con espera
creciente y la misma Idempotency-Key.
No: 4xx de datos (422, 409). Van a fallar igual hasta que
corrijas lo que te decimos.
Webhooks
En vez de preguntarnos cada rato, te avisamos nosotros.
Registrás una URL y te mandamos un POST con JSON cada vez que pasa algo. La URL tiene que ser
https y de un dominio público.
Eventos
| Evento | Cuándo |
|---|---|
order.created | Entró un pedido nuevo a tu cuenta. |
order.status_changed | Cambió el estado. El payload trae previousStatus. |
order.tracking_assigned | Se le asignó número de seguimiento y transportista. |
order.delivered | Se entregó. Llega además del status_changed. |
order.cancelled | Se anuló. Llega además del status_changed. |
Si querés todos, mandá ["*"]. Un cambio a "Entregado" dispara order.status_changed
y order.delivered: suscribite a lo que uses, no a los dos, para no procesar dos veces.
Lo que recibís
POST /tu-endpoint
X-OnBox-Event: order.status_changed
X-OnBox-Delivery: 118
X-OnBox-Timestamp: 1786898527
X-OnBox-Signature: t=1786898527,v1=6f08c3d87ab6352b75e39e31f136755f…
{
"event": "order.status_changed",
"createdAt": "2026-08-18T13:22:07.000Z",
"data": {
"id": "SM-10045",
"status": "Despachado",
"statusCode": "shipped",
"previousStatus": "En preparación",
"previousStatusCode": "processing",
"tracking": "OCA-123456",
"updatedAt": "2026-08-18T13:22:05.000Z"
}
}
Validar la firma
El header X-OnBox-Signature trae t (el momento) y v1 (la firma).
La firma es HMAC-SHA256 de <t>.<cuerpo crudo> con tu secret.
Firmá el cuerpo crudo, no el JSON reparseado. Si tu framework convierte el body a objeto y lo volvés a serializar, la firma no va a dar nunca: cambia un espacio y cambia el hash.
const crypto = require('crypto');
function verificar(rawBody, header, secret) {
const m = /t=(\d+),v1=([a-f0-9]+)/.exec(header || '');
if (!m) return false;
const [, t, firma] = m;
// Descartar lo viejo: protege contra reenvíos.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const esperado = crypto.createHmac('sha256', secret)
.update(t + '.' + rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(firma), Buffer.from(esperado));
}
// Express: guardá el cuerpo crudo
app.use('/hooks', express.json({ verify: (req, _res, buf) => { req.raw = buf.toString(); } }));
app.post('/hooks', (req, res) => {
if (!verificar(req.raw, req.get('X-OnBox-Signature'), process.env.ONBOX_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
res.sendStatus(200); // contestá primero
procesarEnSegundoPlano(req.body);
});
<?php
function verificar(string $rawBody, ?string $header, string $secret): bool {
if (!preg_match('/t=(\d+),v1=([a-f0-9]+)/', (string)$header, $m)) return false;
[, $t, $firma] = $m;
if (abs(time() - (int)$t) > 300) return false;
$esperado = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
return hash_equals($esperado, $firma);
}
$raw = file_get_contents('php://input');
if (!verificar($raw, $_SERVER['HTTP_X_ONBOX_SIGNATURE'] ?? null, getenv('ONBOX_WEBHOOK_SECRET'))) {
http_response_code(401); exit;
}
http_response_code(200);
$evento = json_decode($raw, true);
import hmac, hashlib, re, time
def verificar(raw_body: bytes, header: str, secret: str) -> bool:
m = re.match(r"t=(\d+),v1=([a-f0-9]+)", header or "")
if not m:
return False
t, firma = m.group(1), m.group(2)
if abs(time.time() - int(t)) > 300:
return False
esperado = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperado, firma)
Cómo contestar
- Devolvé
2xxrápido y procesá después. Si tardás más de 12 segundos lo tomamos como fallido y reintentamos. - Cualquier respuesta que no sea
2xxcuenta como fallo. - Los avisos pueden llegar más de una vez. Usá
X-OnBox-Deliverypara descartar repetidos. - No asumas orden. Si te llegan dos cambios juntos, ordenalos por
data.updatedAt.
Reintentos
Si no contestás 2xx, reintentamos a los 1 min, 5, 15, 1 h, 3 h, 6 h y 12 h. Después
marcamos la entrega como fallida y queda en el historial. En GET /v1/webhooks/{id}/deliveries
ves cada intento con lo que devolviste, así podés diagnosticar sin escribirnos.
Versiones y cambios
La ruta /v1 es estable. No rompemos lo que ya funciona.
Podemos agregar campos a una respuesta o endpoints nuevos sin avisar: tu integración tiene que
ignorar lo que no conoce en vez de romperse. Si alguna vez hubiera que cambiar o sacar algo, sale una
/v2 y /v1 sigue andando con al menos 6 meses de aviso.
Historial
| Versión | Fecha | Cambios |
|---|---|---|
2.2.0 | 20/08/2026 | Código de estado estable (statusCode) en pedidos, seguimiento y webhooks, con previousStatusCode. El alta acepta shipping.zip y shipping.province y con eso resolvemos la zona y el transportista. Detalle de pedido con CP y provincia. Nueva guía de estados. |
2.1.0 | 16/08/2026 | Entorno de prueba con claves _test_ y simulación de estados. Vencimiento, rotación y lista de IPs por clave. Rotación del secret de webhook. Documentación reescrita y colección de Postman. |
2.0.0 | 16/08/2026 | Alta y cancelación de pedidos. Webhooks firmados con reintentos. Límite por clave, X-Request-Id y registro de llamadas. Consola de developers. Corregida una fuga en el detalle de pedido que podía mostrar líneas de otro pedido. |
1.0.0 | 12/08/2026 | Primera versión: lectura de pedidos, stock y seguimiento. |
Antes de salir a producción
Si podés tildar todo esto, la integración está sana.
| 1 | Probaste el circuito completo con clave _test_: alta, avance de estados y webhook recibido. |
| 2 | Mandás Idempotency-Key en cada alta y reintentás ante 429, 5xx y errores de red. |
| 3 | Tu externalId es único y estable: nunca se reutiliza. |
| 4 | Validás la firma de cada webhook sobre el cuerpo crudo y descartás lo de más de 5 minutos. |
| 5 | Descartás avisos repetidos por X-OnBox-Delivery y contestás 2xx antes de procesar. |
| 6 | Guardás el X-Request-Id de cada llamada en tus logs. |
| 7 | La clave vive en el servidor, en una variable de entorno. No está en el repositorio. |
| 8 | Manejás 422 unknown_sku: sabés qué hacer cuando un producto tuyo no está en el catálogo. |
| 9 | Nos dijiste desde qué IPs vas a llamar, para que atemos la clave. |
| 10 | Tenés una sincronización periódica con updatedSince como red de seguridad del webhook. |
Diagnóstico
Devuelve a qué cuenta pertenece tu clave, qué permisos tiene y cuánto te queda del límite. Es la primera llamada que conviene hacer.
Ejemplo
curl https://api.onboxservicioslogisticos.com/v1/ping \
-H "Authorization: Bearer obx_live_tuClave"
Respuesta
{
"ok": true,
"sellerId": "CLI-0042",
"mode": "live",
"scopes": [
"orders:read",
"orders:write"
],
"rateLimit": {
"limit": 120,
"remaining": 119
},
"apiVersion": "2.2.0",
"time": "2026-08-18T13:00:00.000Z"
}
Las últimas llamadas hechas con tu clave, con su X-Request-Id, el código HTTP y cuánto tardaron. Sirve para depurar sin escribirnos.
Parámetros
| Nombre | Tipo | Descripción |
|---|---|---|
limit | integer | 1–200. Por defecto 50. |
Ejemplo
curl https://api.onboxservicioslogisticos.com/v1/logs?limit=20 \
-H "Authorization: Bearer obx_live_tuClave"
Respuesta
{
"ok": true,
"data": [
{
"request_id": "req_2ac903e5c69fb8db",
"method": "POST",
"path": "/v1/orders",
"status": 201,
"ms": 174,
"created_at": "2026-08-18T13:02:11.204Z"
}
]
}
Pedidos
Tus pedidos, paginados por cursor. Para sincronizar cada X minutos usá updatedSince en vez de barrer todo: te devuelve sólo lo que cambió.
Parámetros
| Nombre | Tipo | Descripción |
|---|---|---|
from | date | Desde (YYYY-MM-DD), por fecha de pedido. |
to | date | Hasta (YYYY-MM-DD). |
status | string | Filtrar por estado exacto, sin distinguir mayúsculas. |
updatedSince | date-time | Sólo lo modificado después de ese momento (ISO 8601). Ordena por updatedAt. |
limit | integer | 1–200. Por defecto 50. |
cursor | integer | Seguí desde acá. Viene en paging.nextCursor. |
Ejemplo
curl https://api.onboxservicioslogisticos.com/v1/orders?limit=20 \
-H "Authorization: Bearer obx_live_tuClave"
Respuesta
{
"ok": true,
"data": [
{
"id": "SM-10045",
"internalId": 89696349,
"status": "Pendiente",
"statusCode": "pending",
"amount": 30800,
"buyer": "Juan Perez",
"platform": "API · Seller Manager",
"shippingType": null,
"tracking": null,
"orderDate": "2026-08-18",
"createdAt": "2026-08-18T13:02:11.204Z",
"updatedAt": "2026-08-18T13:02:11.400Z"
}
],
"paging": {
"limit": 50,
"nextCursor": null
}
}
El pedido completo con sus líneas de producto, dirección y estado. Acepta tanto el id nuestro como el externalId que nos mandaste.
Ejemplo
curl https://api.onboxservicioslogisticos.com/v1/orders/SM-10045 \
-H "Authorization: Bearer obx_live_tuClave"
Respuesta
{
"ok": true,
"data": {
"id": "SM-10045",
"internalId": 89696349,
"status": "Pendiente",
"statusCode": "pending",
"amount": 30800,
"buyer": "Juan Perez",
"platform": "API · Seller Manager",
"shippingType": null,
"tracking": null,
"orderDate": "2026-08-18",
"createdAt": "2026-08-18T13:02:11.204Z",
"updatedAt": "2026-08-18T13:02:11.400Z",
"address": "Av. Mitre 750",
"city": "Avellaneda",
"zip": "1870",
"province": "Buenos Aires",
"lines": [
{
"sku": "ABC-123",
"name": "Producto de ejemplo",
"qty": 2,
"unitPrice": 15400
}
]
}
}
Nos mandás el pedido y queda cargado con el stock reservado. Es idempotente: reenviar el mismo externalId (o la misma Idempotency-Key) devuelve el pedido que ya existe con duplicate: true, sin duplicar nada.
Headers
Idempotency-Key | Opcional pero recomendado. Un identificador único del intento; si el reintento llega con la misma, te devolvemos la misma respuesta. |
Cuerpo
| Campo | Tipo | Descripción |
|---|---|---|
externalId | string · requerido | El identificador del pedido EN TU SISTEMA. Es la llave de idempotencia: repetirlo no duplica. Hasta 60 caracteres, letras, números, punto, guion y guion bajo. |
buyer.name | string · requerido | Nombre de quien recibe. |
buyer.phone | string | Teléfono de contacto. Sin esto el chofer no puede avisar. |
buyer.email | string | Correo de contacto. |
shipping.address | string · requerido | Calle y número. |
shipping.city | string | Localidad. |
shipping.zip | string | Código postal. Muy recomendado: es lo que usamos para resolver la zona y elegir el transportista (OCA / Andreani / cadete). |
shipping.province | string | Provincia. Ayuda a la asignación de zona cuando el CP es ambiguo. |
shipping.notes | string | Indicaciones para la entrega (timbre, horario, referencia). |
shipping.type | string | Modalidad, si tenés una acordada con nosotros. |
items | Item[] · requerido | Entre 1 y 200 items. |
items[].sku | string · requerido | Tiene que existir en tu catálogo. Si uno no existe, se rechaza el pedido entero. |
items[].quantity | integer · requerido | Entero mayor a cero. |
items[].unitPrice | number | Precio unitario. |
items[].name | string | Descripción, sólo informativa. |
amount | number | Total del pedido. Si no lo mandás, lo calculamos con los items. |
paymentType | string | Forma de pago, si aplica. |
notes | string | Nota interna del pedido. |
rejectIfNoStock | boolean | Por defecto false: el pedido entra igual y te avisamos en warnings. En true, lo rechazamos con 409. |
Ejemplo
curl -X POST https://api.onboxservicioslogisticos.com/v1/orders \
-H "Authorization: Bearer obx_live_tuClave" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f9c-1" \
-d '{
"externalId": "SM-10045",
"buyer": {
"name": "Juan Perez",
"phone": "1155667788"
},
"shipping": {
"address": "Av. Mitre 750",
"city": "Avellaneda",
"zip": "1870",
"province": "Buenos Aires"
},
"items": [
{
"sku": "ABC-123",
"quantity": 2,
"unitPrice": 15400
}
],
"amount": 30800
}'
Respuesta
{
"ok": true,
"duplicate": false,
"data": {
"id": "SM-10045",
"internalId": 89696349,
"status": "Pendiente",
"statusCode": "pending",
"amount": 30800,
"buyer": "Juan Perez",
"platform": "API · Seller Manager",
"shippingType": null,
"tracking": null,
"orderDate": "2026-08-18",
"createdAt": "2026-08-18T13:02:11.204Z",
"updatedAt": "2026-08-18T13:02:11.400Z",
"externalId": "SM-10045",
"items": [
{
"sku": "ABC-123",
"name": null,
"quantity": 2,
"unitPrice": 15400
}
]
}
}
Errores propios
422 validation_error | Falta un campo o tiene un valor inválido. |
422 unknown_sku | Uno o más SKU no existen en tu catálogo. |
409 insufficient_stock | No hay stock para todo lo pedido y mandaste rejectIfNoStock. |
Anula un pedido que todavía no salió del depósito y libera lo reservado. Si ya se preparó o despachó devuelve 409: eso lo resolvemos con una persona, no por API. Cancelar algo ya cancelado devuelve 200.
Ejemplo
curl -X POST https://api.onboxservicioslogisticos.com/v1/orders/SM-10045/cancel \
-H "Authorization: Bearer obx_live_tuClave" \
-H "Content-Type: application/json" \
-d '{
"reason": "El comprador se arrepintió"
}'
Respuesta
{
"ok": true,
"data": {
"id": "SM-10045",
"internalId": 89696349,
"status": "Cancelado",
"statusCode": "pending",
"amount": 30800,
"buyer": "Juan Perez",
"platform": "API · Seller Manager",
"shippingType": null,
"tracking": null,
"orderDate": "2026-08-18",
"createdAt": "2026-08-18T13:02:11.204Z",
"updatedAt": "2026-08-18T13:02:11.400Z"
}
}
Errores propios
404 not_found | El pedido, envío o webhook no existe (o no es tuyo). |
409 cannot_cancel | El pedido ya salió de la etapa en la que se puede anular solo. |
Stock
Unidades disponibles por SKU. Es lo que se puede vender: ya tiene descontado lo reservado por pedidos abiertos.
Parámetros
| Nombre | Tipo | Descripción |
|---|---|---|
sku | string | Un SKU puntual. Vacío devuelve todos. |
limit | integer | 1–500. Por defecto 100. |
Ejemplo
curl https://api.onboxservicioslogisticos.com/v1/stock?limit=20 \
-H "Authorization: Bearer obx_live_tuClave"
Respuesta
{
"ok": true,
"data": [
{
"sku": "ABC-123",
"name": "Producto de ejemplo",
"available": 42
}
]
}
Seguimiento
Estado vivo y línea de tiempo. Acepta el número de pedido o el de seguimiento del transportista.
Ejemplo
curl https://api.onboxservicioslogisticos.com/v1/tracking/SM-10045 \
-H "Authorization: Bearer obx_live_tuClave"
Respuesta
{
"ok": true,
"data": {
"reference": "SM-10045",
"status": "En camino",
"statusCode": "in_transit",
"carrier": "OCA",
"carrierTracking": "OCA-123456",
"eta": "2026-08-20",
"timeline": [
{
"date": "2026-08-19T10:11:00-03:00",
"label": "Retirado del depósito"
}
]
}
}
Webhooks
Tus URLs registradas, con el conteo de fallas seguidas de cada una.
Ejemplo
curl https://api.onboxservicioslogisticos.com/v1/webhooks \
-H "Authorization: Bearer obx_live_tuClave"
Respuesta
{
"ok": true,
"data": [
{
"id": 7,
"url": "https://tu-sistema.com/hooks/onbox",
"events": [
"*"
],
"active": true,
"consecutive_failures": 0
}
]
}
La URL tiene que ser https y de un dominio público. Te devolvemos el secret UNA sola vez: con ese secret validás la firma de cada aviso. El webhook queda escuchando desde ese momento; no reenviamos historia vieja.
Ejemplo
curl -X POST https://api.onboxservicioslogisticos.com/v1/webhooks \
-H "Authorization: Bearer obx_live_tuClave" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tu-sistema.com/hooks/onbox",
"events": [
"order.status_changed",
"order.delivered"
],
"description": "Producción"
}'
Respuesta
{
"ok": true,
"data": {
"id": 7,
"url": "https://tu-sistema.com/hooks/onbox",
"events": [
"order.status_changed",
"order.delivered"
],
"active": true,
"secret": "whsec_…"
}
}
Errores propios
422 validation_error | Falta un campo o tiene un valor inválido. |
Te mandamos un evento ping firmado igual que los reales y te devolvemos qué contestó tu servidor. Sirve para verificar la firma antes de salir a producción.
Ejemplo
curl -X POST https://api.onboxservicioslogisticos.com/v1/webhooks/SM-10045/test \
-H "Authorization: Bearer obx_live_tuClave"
Respuesta
{
"ok": true,
"data": {
"deliveryId": 118,
"httpStatus": 200,
"error": null
}
}
Genera un secret nuevo. El anterior sigue siendo válido 24 horas para que puedas desplegar sin ventana de corte: durante ese lapso aceptá cualquiera de los dos.
Ejemplo
curl -X POST https://api.onboxservicioslogisticos.com/v1/webhooks/SM-10045/rotate-secret \
-H "Authorization: Bearer obx_live_tuClave"
Respuesta
{
"ok": true,
"data": {
"id": 7,
"secret": "whsec_…",
"anteriorValidoHasta": "2026-08-19T13:00:00.000Z"
}
}
Los últimos 50 avisos que te mandamos, con el código que devolviste, los intentos y el error si lo hubo.
Ejemplo
curl https://api.onboxservicioslogisticos.com/v1/webhooks/SM-10045/deliveries \
-H "Authorization: Bearer obx_live_tuClave"
Respuesta
{
"ok": true,
"data": [
{
"id": 118,
"event": "order.status_changed",
"order_key": "SM-10045",
"status": "delivered",
"attempts": 1,
"response_status": 200,
"last_error": null
}
]
}
Borra el webhook y su historial. También acepta DELETE sobre /v1/webhooks/{id}.
Ejemplo
curl -X POST https://api.onboxservicioslogisticos.com/v1/webhooks/SM-10045/delete \
-H "Authorization: Bearer obx_live_tuClave"
Respuesta
{
"ok": true,
"deleted": 7
}
Entorno de prueba
Sólo con clave de prueba. Avanza un pedido simulado al estado que le pidas y dispara los webhooks de prueba como si fuera real. Es la forma de probar tu integración de punta a punta sin tocar producción.
Ejemplo
curl -X POST https://api.onboxservicioslogisticos.com/v1/sandbox/orders/SM-10045/advance \
-H "Authorization: Bearer obx_live_tuClave" \
-H "Content-Type: application/json" \
-d '{
"status": "Despachado",
"tracking": "OCA-123456"
}'
Respuesta
{
"ok": true,
"data": {
"id": "SM-10045",
"status": "Despachado",
"tracking": "OCA-123456"
}
}
Apuntá tu webhook a POST /dev/echo y después consultá acá para ver exactamente qué te mandamos: headers, firma y cuerpo, con la firma ya verificada de nuestro lado. Sirve para comparar contra tu implementación.
Ejemplo
curl https://api.onboxservicioslogisticos.com/dev/echo \
-H "Authorization: Bearer obx_live_tuClave"
Respuesta
{
"ok": true,
"data": [
{
"delivery_id": 118,
"headers": {
"x-onbox-event": "ping"
},
"signature_ok": true
}
]
}
Objetos
Pedido
Un pedido tuyo dentro de nuestro sistema.
| Campo | Tipo | Descripción |
|---|---|---|
id | string | Identificador del pedido en nuestro sistema. Es el que usás en el resto de los endpoints. |
externalId | string | El identificador que nos mandaste vos al crearlo. Sólo viene en el alta. |
internalId | integer | Número interno. No lo uses como referencia: usá id. |
status | string | Estado actual, en castellano (Pendiente, En preparación, Despachado, Entregado, Cancelado…). Texto para mostrar, no para lógica. |
statusCode | string | Código ESTABLE del estado: pending, processing, shipped, in_transit, delivered, cancelled, returned, unknown. Integrá contra esto. |
amount | number|null | Importe total declarado. |
buyer | string|null | Nombre de quien recibe. |
platform | string|null | De dónde entró el pedido. |
shippingType | string|null | Modalidad de envío. |
tracking | string|null | Número de seguimiento, cuando ya se asignó. |
orderDate | string|null | Fecha del pedido (YYYY-MM-DD). |
createdAt | string|null | Alta, en ISO 8601 UTC. |
updatedAt | string|null | Última modificación, en ISO 8601 UTC. Es el campo que usás para sincronizar. |
lines | Linea[] | Sólo en el detalle: las líneas de producto. |
address | string|null | Sólo en el detalle: dirección de entrega. |
city | string|null | Sólo en el detalle: localidad. |
zip | string|null | Sólo en el detalle: código postal. |
province | string|null | Sólo en el detalle: provincia. |
Linea
Una línea de producto de un pedido.
| Campo | Tipo | Descripción |
|---|---|---|
sku | string|null | Código del producto. |
name | string|null | Descripción. |
qty | integer | Unidades. |
unitPrice | number | Precio unitario. |
NuevoPedido
Lo que nos mandás para crear un pedido.
| Campo | Tipo | Descripción |
|---|---|---|
externalId | string · requerido | El identificador del pedido EN TU SISTEMA. Es la llave de idempotencia: repetirlo no duplica. Hasta 60 caracteres, letras, números, punto, guion y guion bajo. |
buyer.name | string · requerido | Nombre de quien recibe. |
buyer.phone | string | Teléfono de contacto. Sin esto el chofer no puede avisar. |
buyer.email | string | Correo de contacto. |
shipping.address | string · requerido | Calle y número. |
shipping.city | string | Localidad. |
shipping.zip | string | Código postal. Muy recomendado: es lo que usamos para resolver la zona y elegir el transportista (OCA / Andreani / cadete). |
shipping.province | string | Provincia. Ayuda a la asignación de zona cuando el CP es ambiguo. |
shipping.notes | string | Indicaciones para la entrega (timbre, horario, referencia). |
shipping.type | string | Modalidad, si tenés una acordada con nosotros. |
items | Item[] · requerido | Entre 1 y 200 items. |
items[].sku | string · requerido | Tiene que existir en tu catálogo. Si uno no existe, se rechaza el pedido entero. |
items[].quantity | integer · requerido | Entero mayor a cero. |
items[].unitPrice | number | Precio unitario. |
items[].name | string | Descripción, sólo informativa. |
amount | number | Total del pedido. Si no lo mandás, lo calculamos con los items. |
paymentType | string | Forma de pago, si aplica. |
notes | string | Nota interna del pedido. |
rejectIfNoStock | boolean | Por defecto false: el pedido entra igual y te avisamos en warnings. En true, lo rechazamos con 409. |
Stock
Disponibilidad por SKU.
| Campo | Tipo | Descripción |
|---|---|---|
sku | string | Código del producto. |
name | string|null | Descripción. |
available | integer | Unidades disponibles para vender. |
Seguimiento
Estado vivo de un envío.
| Campo | Tipo | Descripción |
|---|---|---|
reference | string | Lo que consultaste. |
status | string|null | Estado actual informado por el transportista (texto para mostrar). |
statusCode | string | Código estable del estado (mismo vocabulario que el pedido). |
carrier | string|null | Transportista (nombre): OCA, Andreani, cadete propio… |
carrierTracking | string|null | Número de seguimiento del transportista, si ya se asignó. |
eta | string|null | Fecha de despacho / estimada, si la hay. |
timeline | object[] | Los eventos en orden, del más viejo al más nuevo. Cada uno: { date, label }. |
Webhook
Una URL tuya a la que te avisamos.
| Campo | Tipo | Descripción |
|---|---|---|
id | integer | Identificador del webhook. |
url | string | A dónde te avisamos. Tiene que ser https y un dominio público. |
events | string[] | Qué eventos querés. ["*"] es todos. |
secret | string | Sólo al crearlo y al rotarlo. Con esto validás la firma. |
active | boolean | Si está recibiendo. |
consecutive_failures | integer | Entregas seguidas que fallaron. Si crece, algo pasa de tu lado. |
OnBox Servicios Logísticos · API 2.2.0 · Soporte: onboxlogistica@gmail.com
Consola de developers · OpenAPI · Postman