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 pedidosUn POST y el pedido queda cargado con el stock reservado. Idempotente: un reintento nunca duplica.
Te devolvemos estadoConsultás cuando querés, o te avisamos nosotros por webhook. Recomendamos webhook.
Stock en vivoLo que realmente se puede vender, ya descontado lo reservado.
SeguimientoEstado 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

ClaveQué 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.

PermisoHabilita
orders:readLeer pedidos y sus líneas.
orders:writeCrear y cancelar pedidos.
stock:readConsultar stock disponible por SKU.
tracking:readConsultar el estado y la línea de tiempo de un envío.
webhooks:writeRegistrar, 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 t de 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.
statusCodeQué significa
pendingEntró el pedido y todavía no se preparó.
processingSe está preparando / armando en el depósito.
shippedDespachado: salió del depósito, ya tiene guía asignada.
in_transitEn camino / en reparto hacia el destino.
deliveredEntregado al destinatario.
cancelledAnulado, cancelado o rechazado.
returnedDevuelto al depósito.
unknownUn 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:

  1. 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, con duplicate: true y HTTP 200.
  2. 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

Llamadas120 por minuto por clave. Se puede subir: pedilo y lo ajustamos.
CuerpoHasta 512 KB por llamada.
Items por pedidoHasta 200.
Pedidos por páginaHasta 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." }
    ]
  }
}
HTTPCódigoCuándoQué hacer
400bad_requestEl cuerpo no es un JSON válido o falta un parámetro obligatorio.Revisá el JSON y el Content-Type.
401unauthorizedFalta la API key, está mal escrita o fue revocada.Mandá el header Authorization: Bearer …
401key_expiredLa clave tenía fecha de vencimiento y ya pasó.Pedinos una clave nueva.
403ip_not_allowedLa clave tiene lista de IPs permitidas y la llamada vino de otra.Avisanos desde qué IP salen.
403forbiddenLa clave no tiene el permiso que ese endpoint requiere.Pedinos que le sumemos el permiso.
404not_foundEl pedido, envío o webhook no existe (o no es tuyo).Verificá el identificador.
405method_not_allowedLa ruta existe pero no con ese método.Mirá la referencia.
409cannot_cancelEl 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.
409insufficient_stockNo hay stock para todo lo pedido y mandaste rejectIfNoStock.Sacá el item o bajá la cantidad. En error.details va cuánto hay.
413payload_too_largeEl cuerpo supera 512 KB.Partí el pedido o sacá texto innecesario.
422validation_errorFalta un campo o tiene un valor inválido.En error.details va la lista exacta, campo por campo.
422unknown_skuUno 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.
429rate_limitedSuperaste las llamadas por minuto de tu clave.Esperá lo que dice Retry-After y reintentá. Todo es idempotente: reintentar es seguro.
500server_errorSe rompió algo nuestro.Reintentá. Si sigue, mandanos el X-Request-Id.
502upstream_errorNuestro 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

EventoCuándo
order.createdEntró un pedido nuevo a tu cuenta.
order.status_changedCambió el estado. El payload trae previousStatus.
order.tracking_assignedSe le asignó número de seguimiento y transportista.
order.deliveredSe entregó. Llega además del status_changed.
order.cancelledSe 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é 2xx rápido y procesá después. Si tardás más de 12 segundos lo tomamos como fallido y reintentamos.
  • Cualquier respuesta que no sea 2xx cuenta como fallo.
  • Los avisos pueden llegar más de una vez. Usá X-OnBox-Delivery para 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ónFechaCambios
2.2.020/08/2026Có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.016/08/2026Entorno 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.016/08/2026Alta 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.012/08/2026Primera 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.

1Probaste el circuito completo con clave _test_: alta, avance de estados y webhook recibido.
2Mandás Idempotency-Key en cada alta y reintentás ante 429, 5xx y errores de red.
3Tu externalId es único y estable: nunca se reutiliza.
4Validás la firma de cada webhook sobre el cuerpo crudo y descartás lo de más de 5 minutos.
5Descartás avisos repetidos por X-OnBox-Delivery y contestás 2xx antes de procesar.
6Guardás el X-Request-Id de cada llamada en tus logs.
7La clave vive en el servidor, en una variable de entorno. No está en el repositorio.
8Manejás 422 unknown_sku: sabés qué hacer cuando un producto tuyo no está en el catálogo.
9Nos dijiste desde qué IPs vas a llamar, para que atemos la clave.
10Tenés una sincronización periódica con updatedSince como red de seguridad del webhook.

Diagnóstico

GET /v1/ping

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"
}
GET /v1/logs

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

NombreTipoDescripción
limitinteger1–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

GET /v1/orders orders:read

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

NombreTipoDescripción
fromdateDesde (YYYY-MM-DD), por fecha de pedido.
todateHasta (YYYY-MM-DD).
statusstringFiltrar por estado exacto, sin distinguir mayúsculas.
updatedSincedate-timeSólo lo modificado después de ese momento (ISO 8601). Ordena por updatedAt.
limitinteger1–200. Por defecto 50.
cursorintegerSeguí 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
  }
}
GET /v1/orders/{id} orders:read

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
      }
    ]
  }
}
POST /v1/orders orders:write

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-KeyOpcional pero recomendado. Un identificador único del intento; si el reintento llega con la misma, te devolvemos la misma respuesta.

Cuerpo

CampoTipoDescripción
externalIdstring · requeridoEl 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.namestring · requeridoNombre de quien recibe.
buyer.phonestringTeléfono de contacto. Sin esto el chofer no puede avisar.
buyer.emailstringCorreo de contacto.
shipping.addressstring · requeridoCalle y número.
shipping.citystringLocalidad.
shipping.zipstringCódigo postal. Muy recomendado: es lo que usamos para resolver la zona y elegir el transportista (OCA / Andreani / cadete).
shipping.provincestringProvincia. Ayuda a la asignación de zona cuando el CP es ambiguo.
shipping.notesstringIndicaciones para la entrega (timbre, horario, referencia).
shipping.typestringModalidad, si tenés una acordada con nosotros.
itemsItem[] · requeridoEntre 1 y 200 items.
items[].skustring · requeridoTiene que existir en tu catálogo. Si uno no existe, se rechaza el pedido entero.
items[].quantityinteger · requeridoEntero mayor a cero.
items[].unitPricenumberPrecio unitario.
items[].namestringDescripción, sólo informativa.
amountnumberTotal del pedido. Si no lo mandás, lo calculamos con los items.
paymentTypestringForma de pago, si aplica.
notesstringNota interna del pedido.
rejectIfNoStockbooleanPor 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_errorFalta un campo o tiene un valor inválido.
422 unknown_skuUno o más SKU no existen en tu catálogo.
409 insufficient_stockNo hay stock para todo lo pedido y mandaste rejectIfNoStock.
POST /v1/orders/{id}/cancel orders:write

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_foundEl pedido, envío o webhook no existe (o no es tuyo).
409 cannot_cancelEl pedido ya salió de la etapa en la que se puede anular solo.

Stock

GET /v1/stock stock:read

Unidades disponibles por SKU. Es lo que se puede vender: ya tiene descontado lo reservado por pedidos abiertos.

Parámetros

NombreTipoDescripción
skustringUn SKU puntual. Vacío devuelve todos.
limitinteger1–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

GET /v1/tracking/{id} tracking:read

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

GET /v1/webhooks webhooks:write

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
    }
  ]
}
POST /v1/webhooks webhooks:write

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_errorFalta un campo o tiene un valor inválido.
POST /v1/webhooks/{id}/test webhooks:write

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
  }
}
POST /v1/webhooks/{id}/rotate-secret webhooks:write

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"
  }
}
GET /v1/webhooks/{id}/deliveries webhooks:write

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
    }
  ]
}
POST /v1/webhooks/{id}/delete webhooks:write

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

POST /v1/sandbox/orders/{id}/advance orders:write

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"
  }
}
GET /dev/echo webhooks:write

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.

CampoTipoDescripción
idstringIdentificador del pedido en nuestro sistema. Es el que usás en el resto de los endpoints.
externalIdstringEl identificador que nos mandaste vos al crearlo. Sólo viene en el alta.
internalIdintegerNúmero interno. No lo uses como referencia: usá id.
statusstringEstado actual, en castellano (Pendiente, En preparación, Despachado, Entregado, Cancelado…). Texto para mostrar, no para lógica.
statusCodestringCódigo ESTABLE del estado: pending, processing, shipped, in_transit, delivered, cancelled, returned, unknown. Integrá contra esto.
amountnumber|nullImporte total declarado.
buyerstring|nullNombre de quien recibe.
platformstring|nullDe dónde entró el pedido.
shippingTypestring|nullModalidad de envío.
trackingstring|nullNúmero de seguimiento, cuando ya se asignó.
orderDatestring|nullFecha del pedido (YYYY-MM-DD).
createdAtstring|nullAlta, en ISO 8601 UTC.
updatedAtstring|nullÚltima modificación, en ISO 8601 UTC. Es el campo que usás para sincronizar.
linesLinea[]Sólo en el detalle: las líneas de producto.
addressstring|nullSólo en el detalle: dirección de entrega.
citystring|nullSólo en el detalle: localidad.
zipstring|nullSólo en el detalle: código postal.
provincestring|nullSólo en el detalle: provincia.

Linea

Una línea de producto de un pedido.

CampoTipoDescripción
skustring|nullCódigo del producto.
namestring|nullDescripción.
qtyintegerUnidades.
unitPricenumberPrecio unitario.

NuevoPedido

Lo que nos mandás para crear un pedido.

CampoTipoDescripción
externalIdstring · requeridoEl 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.namestring · requeridoNombre de quien recibe.
buyer.phonestringTeléfono de contacto. Sin esto el chofer no puede avisar.
buyer.emailstringCorreo de contacto.
shipping.addressstring · requeridoCalle y número.
shipping.citystringLocalidad.
shipping.zipstringCódigo postal. Muy recomendado: es lo que usamos para resolver la zona y elegir el transportista (OCA / Andreani / cadete).
shipping.provincestringProvincia. Ayuda a la asignación de zona cuando el CP es ambiguo.
shipping.notesstringIndicaciones para la entrega (timbre, horario, referencia).
shipping.typestringModalidad, si tenés una acordada con nosotros.
itemsItem[] · requeridoEntre 1 y 200 items.
items[].skustring · requeridoTiene que existir en tu catálogo. Si uno no existe, se rechaza el pedido entero.
items[].quantityinteger · requeridoEntero mayor a cero.
items[].unitPricenumberPrecio unitario.
items[].namestringDescripción, sólo informativa.
amountnumberTotal del pedido. Si no lo mandás, lo calculamos con los items.
paymentTypestringForma de pago, si aplica.
notesstringNota interna del pedido.
rejectIfNoStockbooleanPor defecto false: el pedido entra igual y te avisamos en warnings. En true, lo rechazamos con 409.

Stock

Disponibilidad por SKU.

CampoTipoDescripción
skustringCódigo del producto.
namestring|nullDescripción.
availableintegerUnidades disponibles para vender.

Seguimiento

Estado vivo de un envío.

CampoTipoDescripción
referencestringLo que consultaste.
statusstring|nullEstado actual informado por el transportista (texto para mostrar).
statusCodestringCódigo estable del estado (mismo vocabulario que el pedido).
carrierstring|nullTransportista (nombre): OCA, Andreani, cadete propio…
carrierTrackingstring|nullNúmero de seguimiento del transportista, si ya se asignó.
etastring|nullFecha de despacho / estimada, si la hay.
timelineobject[]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.

CampoTipoDescripción
idintegerIdentificador del webhook.
urlstringA dónde te avisamos. Tiene que ser https y un dominio público.
eventsstring[]Qué eventos querés. ["*"] es todos.
secretstringSólo al crearlo y al rotarlo. Con esto validás la firma.
activebooleanSi está recibiendo.
consecutive_failuresintegerEntregas 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