Nuevo número: +51 948 997 674

38 resultados
↑ ↓ Navegar ↵ Seleccionar ESC Cerrar

Documentación.

API REST para consultar tracking, agencias y ubicaciones de shalom.com.pe, y crear pedidos en pro.shalom.pe.

Auth

X-API-Key

Rate limit

60–600 / min

Formato

JSON

Base URL: https://api.shalom-api-peru.com

Quickstart

De cero a tu primera guía creada y rastreada, en cuatro pasos. Cada paso usa lo que devolvió el anterior. Necesitas dos cosas antes de empezar: tu API key (la pides por WhatsApp) y las credenciales de Shalom Pro del cliente — email y password de pro.shalom.pe.

Paso 1 — Cambia email y password por un token

Todo lo que toca la cuenta del cliente (productos, órdenes) pide credenciales de Shalom Pro; en tracking son opcionales y suman la orden completa. Las canjeas una vez por un session_token que dura 2 horas, y de ahí en adelante mandas solo el token.

Esta llamada es la lenta: tarda entre 90 s y 2 min porque Shalom hace un login real, así que pon el timeout de tu cliente en ≥ 150 s o te vas a estrellar aquí. Las siguientes salen rápido. Detalle en Autenticación.

Request
curl -X POST https://api.shalom-api-peru.com/v1/shalom/sessions \
  -H "X-API-Key: tu-api-key" \
  -H "Content-Type: application/json" \
  --max-time 150 \
  -d '{
    "email": "cliente@empresa.com",
    "password": "tu-password-de-shalom-pro"
  }'

Te devuelve { "session_token": "ssk_a1b2c3d4e5f6...", "expires_at": "..." }. Ese ssk_... va en el header X-Shalom-Session de todos los pasos siguientes.

Si falla: 401 shalom_auth_failed = Shalom rechazó el email o el password (reintentar no ayuda). 503 shalom_login_unavailable con Retry-After = el login de Shalom está bloqueado temporalmente; tus credenciales están bien, reintenta después de ese tiempo. 502 shalom_unavailable = no pudimos contactar a Shalom; reintenta.

Paso 2 — Consigue los tres ids que pide la orden

Para crear una guía necesitas la agencia de origen, la de destino y el tipo de paquete. Las agencias salen de /v1/agencies/search, que solo pide X-API-Key:

Request — agencias
curl "https://api.shalom-api-peru.com/v1/agencies/search?q=arequipa" \
  -H "X-API-Key: tu-api-key"

El id de cada agencia es lo que mandas como origin_terminal_id y destiny_terminal_id. En el ejemplo: 404 y 7. El tipo de paquete sale de /v1/products, y aquí sí reusas el token:

Request — productos
curl https://api.shalom-api-peru.com/v1/products \
  -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Session: ssk_a1b2c3d4e5f6..."

Toma el id del producto — 3 es Sobre — y pásalo como product_id.

Paso 3 — Crea la guía

Junta los tres ids, el destinatario y una clave de retiro de 4 dígitos (pickup_code, ni repetida, ni consecutiva, ni un año (1900–2100)). El remitente no se manda: Shalom lo toma de la cuenta del token (salvo que pidas una guía empresarial).

Request
curl -X POST https://api.shalom-api-peru.com/v1/orders \
  -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Session: ssk_a1b2c3d4e5f6..." \
  -H "Content-Type: application/json" \
  -d '{
    "origin_terminal_id": 404,
    "destiny_terminal_id": 7,
    "product_id": 3,
    "quantity": 1,
    "payer": "sender",
    "declaracion_jurada": "docs",
    "receiver": {
      "document_type": "DNI",
      "document": "87654321",
      "name": "MARIA",
      "last_name": "GOMEZ",
      "sur_name": "TORRES",
      "phone": 998765432
    },
    "pickup_code": "2415"
  }'

Respuesta: { "guia": "80574902", "serie": "v872", "codigo": "CJTW", "ose_id": 584210 }. Guarda guia y codigo para rastrear, y el ose_id si vas a descargar el rótulo en PDF.

Esto crea una guía real y cobrable en la cuenta del cliente, visible en su panel. No hay sandbox y no hay idempotencia: si la llamada te da timeout, no la reintentes a ciegas — consulta primero GET /v1/orders para ver si la guía ya se creó, o la vas a duplicar. Si la creaste probando, bórrala con DELETE /v1/orders/{id} mientras no haya sido recibida en agencia.

Paso 4 — Rastréala

La guia del paso 3 va como numero, y el codigo tal cual. Mismo token del paso 1.

Request
curl -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Session: ssk_a1b2c3d4e5f6..." \
  "https://api.shalom-api-peru.com/v1/tracking?numero=80574902&codigo=CJTW"

Eso es la integración completa. Desde aquí: cotiza antes de crear, sube un lote por Excel, o rastrea hasta 50 guías de un tiro.

Autenticación

Hay dos niveles. El X-API-Key va en todas las rutas /v1. Los endpoints que tocan la cuenta del cliente en Shalom Pro (órdenes, productos, personas, tarifas) exigen además sus credenciales. En GET /v1/tracking y POST /v1/tracking/batch las credenciales son opcionales: sin ellas recibes solo el estado (detailed: false), nunca un 401. Agencias, ubicaciones y /v1/tracking/{ose_id}/events, /voucher y /grt solo necesitan la API key. /healthz y /readyz son públicos: no piden ni la API key.

X-API-Key: tu-api-key

Credenciales de Shalom Pro — dos opciones

OpciónHeadersCuándo
A — token efímero (recomendada)X-Shalom-Session: ssk_...Pides el token una vez con POST /v1/shalom/sessions y dura 2 h. El password viaja una sola vez. Tiene precedencia si mandas ambas. Solo funciona con la misma X-API-Key que lo emitió. Si el token es inválido, expiró o es de otra API key, no se cae a X-Shalom-Email/Password aunque los mandes: los endpoints responden 401 shalom_auth_failed, y el tracking degrada con degraded_reason: "sin_credenciales".
B — directoX-Shalom-Email
X-Shalom-Password
Sin paso previo, pero el password viaja en cada llamada. Para scripts cortos y pruebas.

El password no se loguea ni se persiste en nuestro servidor.

La primera llamada es lenta

La primera llamada de cada cuenta hace un login real contra Shalom y tarda ~90 s, hasta 2 min. Las siguientes son rápidas: la sesión queda caliente y se reutiliza. Configura el timeout de tu cliente en ≥ 150 s o la primera llamada se te cortará sola. Ver Latencia y reintentos.

Solicita tu API key por WhatsApp:

948 997 674

Identificadores

Un envío tiene varios ids y no son intercambiables. Los dos que más se confunden: ose_id sirve para rastrear y descargar PDFs; id sirve para borrar la orden.

CampoQué esDónde lo obtienesDónde lo usas
guia8–10 dígitos. Va impresa en el comprobante físico.POST /v1/ordersGET /v1/tracking?numero=
codigoAlfanumérico de 4 caracteres. Lo asigna Shalom.POST /v1/ordersGET /v1/tracking?codigo=
ose_idID interno de Shalom del envío. No hace falta enviarlo para rastrear: te lo damos nosotros.POST /v1/orders · order.ose_id del tracking/events, /grt, /label, /voucher
idID de la orden dentro de la cuenta empresarial.GET /v1/ordersDELETE /v1/orders/{id}
pickup_codeClave de 4 dígitos para retirar en agencia. La eliges tú.La defines al crear la ordenLa presenta el destinatario en agencia
seriePrefijo del talonario (ej. "v872").POST /v1/ordersInformativo. No se usa en esta API.
internal_idID interno del panel de Shalom Pro.GET /v1/ordersInformativo. No se usa en esta API.
GET /v1/tracking

Busca una guía y retorna su estado, más la orden completa si autenticas. Tiene dos modos según qué mandes.

Modo estado (público). Con solo X-API-Key + numero y codigo, cualquier guía del sistema devuelve su línea de tiempo (status). No necesita credenciales Shalom. La respuesta trae detailed: false y omite order.

Modo detallado. Si además mandas credenciales Shalom (ver Autenticación), agregamos la orden completa (order: montos, fechas, contenido) y detailed: true. Si el login con tus credenciales falla, la respuesta degrada al modo estado en vez de romper. Si rastreas por ose_id y Shalom no devuelve la orden, también degrada. En cambio, por numero + codigo la búsqueda de la guía es obligatoria (con o sin credenciales): si Shalom la rechaza, recibes un error HTTP (404 not_found o 422 upstream_rejected), no una respuesta degradada. Guía inexistente → 404.

Query params (numero + codigo, o ose_id)

ParamTipoDescripción
numerostring8 a 10 dígitos (ej. "12345678"). Es la guia.
codigostringAlfanumérico de 4 caracteres (ej. "CJTW").
ose_idstringID interno de Shalom del envío. No lo consigues tú: lo devuelve POST /v1/orders y también viene en order.ose_id del rastreo.

Hay dos formas válidas de identificar el envío: numero + codigo juntos, o ose_id solo. Cualquiera de los dos por separado devuelve 400: el numero por sí solo es ambiguo —los números de guía llegaron a colisionar con ose_id antiguos— y el codigo no identifica nada por sí mismo. El par numero+codigo viene impreso en el comprobante físico. Ver Identificadores.

Request — modo estado (solo API key)
curl -H "X-API-Key: tu-api-key" \
  "https://api.shalom-api-peru.com/v1/tracking?numero=82100156&codigo=W79H"
200 OK — modo estado (detailed: false)
{
  "detailed": false,
  "status": {
    "registrado": { "fecha": "2026-04-15 09:12:30" },
    "origen":     { "fecha": "2026-04-15 09:12:30" },
    "transito":   { "fecha": "2026-04-15 14:08:21", "completo": true,
                    "cargueros": ["964724", "966345"], "carguero": "966345" },
    "demora":     null,
    "destino":    { "fecha": "2026-04-16 01:01:03", "completo": true },
    "entregado":  { "fecha": "2026-04-16 11:40:45" },
    "reparto":    null
  },
  "degraded_reason": "sin_credenciales"
}
Request — modo detallado (numero + codigo + credenciales)
curl -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Email: cliente@empresa.com" \
  -H "X-Shalom-Password: tu-password-de-shalom-pro" \
  "https://api.shalom-api-peru.com/v1/tracking?numero=82100156&codigo=W79H"
Request — modo detallado por ose_id
curl -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Email: cliente@empresa.com" \
  -H "X-Shalom-Password: tu-password-de-shalom-pro" \
  "https://api.shalom-api-peru.com/v1/tracking?ose_id=584210"
200 OK — modo detallado (detailed: true)
{
  "detailed": true,
  "order": {
    "ose_id": 584210,
    "numero_orden": "82100156",
    "codigo_orden": "W79H",
    "fecha_emision": "2026-04-14 09:12:30",
    "fecha_traslado": "2026-04-15 09:12:30",
    "tipo_pago": "Contra entrega",
    "estado_pago": "CA",
    "contenido": "1 MINI PAQUETERIA XS",
    "monto": "10.00",
    "entregado": true,
    "reparto": false,
    "aereo": false,
    "tiempo_llegada": "",
    "origen":       { "id": 190, "nombre": "LIMA / LIMA / MIRAFLORES / AV. JOSE PARDO", "abrebiatura": "MIRJP", "ubigeo": 150122, "departamento": "LIMA", "provincia": "LIMA", "distrito": "MIRAFLORES" },
    "destino":      { "id": 622, "nombre": "AMAZONAS / BAGUA / BAGUA / BAGUA CAPITAL", "abrebiatura": "BGACHCA", "ubigeo": 10205, "departamento": "AMAZONAS", "provincia": "BAGUA", "distrito": "BAGUA" },
    "remitente":    { "documento": "20123456789", "nombre": "MI EMPRESA S.A.C." },
    "destinatario": { "documento": "87654321", "nombre": "MARIA GOMEZ TORRES" },
    "comprobante":  { "cop_id": 1234567, "serie": "B001", "numero": "00012345", "tipo": "BOLETA", "tipo_pago": "Contra entrega", "estado_pago": "CA", "fecha": "2026-04-14", "hora": "09:12:30" }
  },
  "status": {
    "registrado": { "fecha": "2026-04-15 09:12:30" },
    "origen":     { "fecha": "2026-04-15 09:12:30" },
    "transito":   { "fecha": "2026-04-15 14:08:21", "completo": true,
                    "cargueros": ["964724", "964583", "966345"], "carguero": "966345" },
    "demora":     null,
    "destino":    { "fecha": "2026-04-16 01:01:03", "completo": true },
    "entregado":  { "fecha": "2026-04-16 11:40:45" },
    "reparto":    null
  }
}

El campo detailed te dice qué esperar. Cuando es false (modo estado) la respuesta trae solo status y omite order. Cuando es true aparece order tal como la devuelve Shalom: identificadores, fechas, contenido, monto, tipo_pago, estado_pago, los flags (entregado, reparto, aereo) y los bloques origen, destino, remitente, destinatario y comprobante. Esos bloques dependen de lo que Shalom envíe, y Shalom puede recortar el nombre del remitente (~32 caracteres). El objeto status completo llega en ambos modos.

Por qué no vino la orden: degraded_reason

Cuando detailed es false, la respuesta trae degraded_reason con el motivo. Con detailed: true el campo no aparece. Cada respuesta refleja el motivo de tu request, aunque el dato venga de cache.

ValorQué significa
sin_credencialesNo mandaste credenciales Shalom, o mandaste un X-Shalom-Session inválido, expirado o de otra API key. Es el modo estado normal.
shalom_auth_failedShalom rechazó la cuenta. Reintentar no ayuda: revisa las credenciales.
shalom_login_unavailableEl login de Shalom está caído temporalmente (por ejemplo, episodios de reCAPTCHA). Tus credenciales no están en discusión: reintenta más tarde.
orden_no_disponibleRastreo por ose_id: la cuenta autenticó bien, pero Shalom no devolvió la orden.

Las respuestas se cachean. El resultado de GET /v1/tracking (y de cada guía del batch) se guarda hasta 10 min; una guía no encontrada, 1 min; y una respuesta degradada con credenciales válidas, 1 min. /events se cachea 10 min, y /grt y /voucher, 30 min. Consultar más seguido no te da datos más frescos: para enterarte de los cambios de estado usa los webhooks.

Los hitos de status

status trae los siete hitos del envío. Los que todavía no ocurrieron llegan en null — es lo normal en un envío en curso, no un error.

HitoDescripción
registradoLa orden se registró en el sistema.
origenRecibido en la agencia de origen.
transitoEn viaje. Único hito que trae cargueros[] (todos los transportistas que movieron el envío) y carguero (el último). Esos ids son el cap_id que pide el GRT.
demoraIncidencia que retrasó el envío. Normalmente null.
destinoLlegó a la agencia de destino.
repartoSalió a reparto a domicilio. null si la entrega es en agencia.
entregadoEntregado al destinatario. Puede traer cliente ({ documento, nombre }): quién recibió el envío.

La fecha incluye la hora. Normalmente cada hito trae solo fecha, con formato "YYYY-MM-DD HH:MM:SS".

POST /v1/tracking/batch

Rastrea varias guías en una sola llamada (hasta 50 por request). Cada item usa los mismos identificadores que GET /v1/tracking (numero + codigo juntos, u ose_id) más un custom_id opcional. Las credenciales Shalom son opcionales, igual que en el rastreo individual. Ideal para refrescar el estado de muchos envíos sin hacer N requests.

Body

CampoTipoDescripción
items[]arrayLista de guías a rastrear. Requerido, de 1 a 50 elementos.
items[].custom_idstringEtiqueta libre tuya (ej. tu id de pedido). No la interpretamos: la devolvemos verbatim en el resultado para que correlaciones cada respuesta con tu sistema. Opcional.
items[].numero / codigo / ose_idstringIdentificadores de la guía, con las mismas reglas que GET /v1/tracking: numero + codigo juntos (numero solo dígitos), u ose_id. Un item que no cumple sale con ok: false y error bad_request. Los tres van como string: un ose_id numérico (584210 en vez de "584210") hace fallar el batch entero con 400.

Cada guía del lote cuenta como una consulta. El batch tiene un cupo de rastreo propio, separado del rate limit general: se recarga al mismo ritmo por minuto que tu plan y admite ráfagas de hasta tu límite por minuto o 50 guías (lo que sea mayor), así que un lote completo de 50 siempre cabe. Un lote de 50 guías consume 50 de ese cupo, y además la llamada gasta 1 del límite general. El motivo es que por detrás cada guía se resuelve por separado contra Shalom: el lote te ahorra viajes y latencia, no cupo. Los headers X-RateLimit-* reflejan el límite general, no este cupo. Si te pasas, la respuesta es 429 rate_limited con Retry-After — espera ese tiempo y reintenta, o reparte el barrido en el minuto en vez de dispararlo todo junto.

La carga masiva por Excel no funciona así: ahí Shalom procesa el lote completo en una sola operación, así que cuenta como una sola llamada por más guías que lleve.

Errores por item, no por batch. Un item que falla (guía inexistente, sin identificador, etc.) no tumba al resto: ese item trae ok: false con su error (mismo code que daría GET /v1/tracking), y el status global sigue siendo 200. Solo devuelve 400 si el envelope está malformado (JSON inválido, items vacío, o más de 50).

Orden preservado. results sale en el mismo orden que mandaste items, y cada resultado hace echo de custom_id y de los identificadores enviados — puedes correlacionar por posición o por custom_id.

Request
curl -X POST https://api.shalom-api-peru.com/v1/tracking/batch \
  -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Email: cliente@empresa.com" \
  -H "X-Shalom-Password: tu-password-de-shalom-pro" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "custom_id": "pedido-001", "numero": "82100156", "codigo": "W79H" },
      { "custom_id": "pedido-002", "ose_id": "584210" },
      { "custom_id": "pedido-003", "numero": "00000000", "codigo": "ZZZZ" }
    ]
  }'
200 OK — order y status recortados por brevedad
{
  "results": [
    {
      "custom_id": "pedido-001",
      "numero": "82100156",
      "codigo": "W79H",
      "ok": true,
      "tracking": {
        "detailed": true,
        "order": { "ose_id": 584210, "numero_orden": "82100156", "entregado": true },
        "status": { "entregado": { "fecha": "2026-04-16 11:40:45" } }
      }
    },
    {
      "custom_id": "pedido-002",
      "ose_id": "584210",
      "ok": true,
      "tracking": {
        "detailed": true,
        "order": { "ose_id": 584210, "numero_orden": "82100156", "entregado": true },
        "status": { "entregado": { "fecha": "2026-04-16 11:40:45" } }
      }
    },
    {
      "custom_id": "pedido-003",
      "numero": "00000000",
      "codigo": "ZZZZ",
      "ok": false,
      "error": { "code": "not_found", "message": "orden no encontrada" }
    }
  ],
  "summary": { "total": 3, "ok": 2, "failed": 1 }
}

Campos de la respuesta

CampoTipoDescripción
results[].okbooltrue si la guía se rastreó; false si falló.
results[].trackingobjectPresente cuando ok: true. Mismo shape que la respuesta de GET /v1/tracking (detailed, order, status y, si degrada, degraded_reason).
results[].errorobjectPresente cuando ok: false. { code, message }, con el mismo code que devolvería el tracking individual.
summaryobject{ total, ok, failed } — conteos del batch.
GET /v1/tracking/{ose_id}/events

Solo los hitos del envío: registrado, origen, tránsito, demora, destino, entregado, reparto. Es el mismo objeto status que ya trae GET /v1/tracking — úsalo cuando solo necesitas refrescar el estado y ya tienes el ose_id.

PathTipoDescripción
ose_idstringID interno de Shalom del envío — el campo order.ose_id de la respuesta de GET /v1/tracking. Es el handle público para acceder a eventos, comprobante y GRT.
200 OK
{
  "registrado": { "fecha": "2026-04-15 09:12:30" },
  "origen":     { "fecha": "2026-04-15 09:12:30" },
  "transito":   { "fecha": "2026-04-15 14:08:21", "completo": true,
                  "cargueros": ["964724", "964583", "966345"], "carguero": "966345" },
  "demora":     null,
  "destino":    { "fecha": "2026-04-16 01:01:03", "completo": true },
  "entregado":  { "fecha": "2026-04-16 11:40:45" },
  "reparto":    null
}
GET /v1/tracking/{ose_id}/voucher

Comprobante de la orden.

Fuera de servicio temporalmente. Hoy este endpoint responde 404 not_found para cualquier orden. El comprobante se localiza con datos que Shalom dejó de enviar en su respuesta de rastreo (ver el aviso en GET /v1/tracking), así que no hay forma de resolverlo. Estamos migrando a la nueva ruta de comprobantes de Shalom; mientras tanto, no dependas de este endpoint.

GET /v1/tracking/{ose_id}/grt?cap_id={cap_id}

Enlace a la Guía de Remisión del Transportista. Un envío puede pasar por varios cargueros (transportistas) y cada uno emite su propio GRT — por eso el cap_id es obligatorio.

ParamEnDescripción
ose_idpathID interno de Shalom del envío — el campo order.ose_id de la respuesta de GET /v1/tracking.
cap_idqueryID del carguero (transportista). Los ids están en status.transito.cargueros[] de GET /v1/tracking. (requerido)
Request
curl -H "X-API-Key: tu-api-key" \
  "https://api.shalom-api-peru.com/v1/tracking/584210/grt?cap_id=898441"
200 OK
{
  "enlace": "https://..."
}

Webhooks de tracking

En vez de pollear GET /v1/tracking cada pocos minutos, suscribes los envíos que te importan y nosotros te empujamos un POST a tu servidor cuando cambian de estado. Menos trabajo para ti, y no gastas cuota consultando envíos que no se movieron.

Cómo se usa, en 3 pasos:

1. Registras la URL de tu servidor una vez (PUT /v1/webhooks) y guardas el signing_secret que te devolvemos.

2. Suscribes los envíos a vigilar (POST /v1/tracking/subscriptions), o pones "track": true al crear la orden.

3. Recibes un POST firmado por cada cambio de estado, con el timeline completo del envío.

El webhook es autocontenido. El payload trae el timeline entero del envío (los mismos hitos que GET /v1/tracking), así que no necesitas volver a llamar al API para ver el detalle.

PUT /v1/webhooks

Registra (o actualiza) la URL a la que te empujamos los eventos. La URL debe ser https y pública — rechazamos localhost e IPs internas por seguridad. En el alta te devolvemos el signing_secret una sola vez: guárdalo, lo necesitas para verificar la firma.

Request
curl -X PUT https://api.shalom-api-peru.com/v1/webhooks \
  -H "X-API-Key: tu-api-key" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://mi-servidor.com/webhooks/shalom" }'
200 OK — el signing_secret solo aparece al crear
{
  "url": "https://mi-servidor.com/webhooks/shalom",
  "enabled": true,
  "verified": true,
  "created": true,
  "signing_secret": "9f8b1c...guarda-este-secreto-de-64-hex...a2"
}

Verificación de propiedad (importante)

Al registrar, te mandamos un evento webhook.ping firmado con un data.challenge. Tu endpoint debe responder 2xx devolviendo ese challenge en el body. Hasta que lo haga, el webhook queda deshabilitado ("verified": false) y no entregamos nada. Esto confirma que controlas la URL — evita que alguien apunte el webhook a un servidor ajeno.

Responde el ping sin exigir la firma. El ping viene firmado, pero en el alta te llega antes de que tengas el signing_secret (te lo devolvemos en la respuesta de ese mismo PUT). Si tu handler verifica la firma antes de mirar el tipo de evento, rechaza el ping y el webhook queda deshabilitado. Devolver el challenge no expone nada; la firma verifícala en el resto de eventos (ver Verificar la firma).

Handler del ping (Node/Express)
// Al registrar, te mandamos un webhook.ping firmado con un challenge.
// Tu endpoint debe DEVOLVER ese challenge en el body (2xx) para habilitarse.
// Respóndelo SIN exigir la firma: en el alta el ping llega antes de que tengas
// el signing_secret (te lo devolvemos en la respuesta de ese mismo PUT).
app.post('/webhooks/shalom', express.raw({ type: 'application/json' }), (req, res) => {
  const event = JSON.parse(req.body);
  if (event.event === 'webhook.ping') {
    return res.status(200).send(event.data.challenge); // <- eco del challenge
  }
  // ... aquí procesas los eventos de tracking (tracking.updated, etc.) ...
  res.status(200).end();
});

El ping se envía UNA sola vez, al registrar — no lo reintentamos. Por eso, deja tu endpoint desplegado y escuchando antes de hacer el PUT. Si el webhook quedó "verified": false, arregla tu endpoint y vuelve a llamar PUT /v1/webhooks para que te mandemos un ping nuevo. Tu endpoint tiene ~5 segundos para responder el challenge.

Cada PUT deshabilita el webhook hasta que pase el ping, cambies la URL o no (el signing_secret se conserva y no se vuelve a mostrar). Mientras está deshabilitado no te entregamos eventos y tus suscripciones quedan en pausa (no se consultan), pero siguen ocupando cupo y su TTL de 21 días sigue corriendo. Si una vence en ese lapso, se cierra sin que te llegue el tracking.expired.

Ver la config y el uso — GET /v1/webhooks

Devuelve la config actual y tu uso de cuota de suscripciones (used / max).

200 OK
{
  "configured": true,
  "url": "https://mi-servidor.com/webhooks/shalom",
  "enabled": true,
  "last_delivery_at": "2026-07-30T15:04:05Z",
  "usage": { "used": 12, "max": 50 }
}

Otras operaciones

MétodoRutaDescripción
POST/v1/webhooks/rotateGenera un signing_secret nuevo (el anterior deja de valer) y lo devuelve una vez. 404 si no tienes webhook configurado.
DELETE/v1/webhooksDeshabilita el webhook (deja de entregar; las suscripciones quedan en pausa, ocupando cupo). Conserva el signing_secret. Para reactivarlo, vuelve a llamar PUT /v1/webhooks (pasa otra vez por el ping). Idempotente.
POST /v1/tracking/subscriptions

Suscribe un envío a vigilar por su numero y codigo de guía (ambos requeridos — son los que ves al crear la orden o en el comprobante). Requiere tener un webhook configurado y verificado (que haya pasado el ping); si no, responde 400. Tip: para tus propias órdenes, lo más cómodo es "track": true al crearlas.

Request
curl -X POST https://api.shalom-api-peru.com/v1/tracking/subscriptions \
  -H "X-API-Key: tu-api-key" \
  -H "Content-Type: application/json" \
  -d '{ "numero": "80574902", "codigo": "CJTW" }'
201 Created (outcome: created | reactivated) · 200 OK si ya existía (already)
{
  "subscription": {
    "id": 812,
    "numero": "80574902",
    "codigo": "CJTW",
    "status": "pending_resolve",
    "next_poll_at": "2026-07-30T12:00:00Z",
    "created_at": "2026-07-30T12:00:00Z"
  },
  "outcome": "created"
}

Poco después de suscribir te llega un primer evento con el estado actual del envío, aunque no haya cambiado nada: no hace falta esperar al próximo hito. Si el envío ya estaba entregado, ese primer evento es tracking.delivered y la suscripción se cierra ahí mismo.

Atajo: suscribir al crear la orden

Agrega "track": true al body de POST /v1/orders y la guía recién creada queda suscrita automáticamente. Es best-effort: si no tienes webhook verificado o llegaste al cupo, la orden se crea igual. La respuesta de la orden no indica si la suscripción se creó: confírmalo en GET /v1/tracking/subscriptions.

Request
curl -X POST https://api.shalom-api-peru.com/v1/orders \
  -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Email: cliente@empresa.com" \
  -H "X-Shalom-Password: tu-password" \
  -H "Content-Type: application/json" \
  -d '{ "...campos de la orden...": "...", "track": true }'

Listar y desuscribir

MétodoRutaDescripción
GET/v1/tracking/subscriptionsLista tus suscripciones con su status y next_poll_at.
DELETE/v1/tracking/subscriptions/{id}Desuscribe un envío (libera cupo). Idempotente.

Cupo por cuenta. Tienes un tope de suscripciones activas a la vez que depende de tu plan (Mensual 50, Anual 100, Impulso 300, Empresa 1000; 50 por defecto). Cuentan las activas y las pending_resolve. Al llegar al tope, suscribir responde 429 quota_exceeded. Como los envíos entregados se auto-desuscriben, rastreas miles a lo largo del tiempo — solo tu tope en simultáneo. Tu uso está en GET /v1/webhooks.

El payload que recibes

Un POST application/json por cada cambio de estado. El timeline trae los hitos presentes, en orden, con los datos del status de GET /v1/tracking: cada entrada tiene siempre milestone y completo; fecha, hora y descripcion vienen solo si Shalom los manda (en la práctica, casi siempre solo fecha). La entrada entregado puede traer cliente (documento, nombre): quién recibió. occurred_at puede traer fracciones de segundo.

ose_id puede no venir en el primer evento de una suscripción hecha con numero + codigo; identifica el envío por numero.

POST a tu servidor — body
{
  "id": "evt_812_tracking.updated_1110100",
  "event": "tracking.updated",
  "occurred_at": "2026-07-30T15:04:05.482913Z",
  "data": {
    "numero": "80574902",
    "ose_id": 84048736,
    "status": "destino",
    "previous_status": "transito",
    "delivered": false,
    "timeline": [
      { "milestone": "registrado", "fecha": "2026-07-28 09:12:30", "completo": false },
      { "milestone": "origen",     "fecha": "2026-07-28 09:12:30", "completo": false },
      { "milestone": "transito",   "fecha": "2026-07-29 14:08:21", "completo": true },
      { "milestone": "destino",    "fecha": "2026-07-30 10:05:03", "completo": true }
    ]
  }
}

Headers

Content-Type: application/json
User-Agent: shalom-api-webhooks/1
X-Shalom-Event: tracking.updated
X-Shalom-Event-Id: evt_812_tracking.updated_1110100
X-Shalom-Signature: t=1782140645,v1=8f3a9c...hmac-sha256-en-hex...

Tipos de evento

eventCuándo
tracking.updatedCambió el estado del envío (apareció un hito). También el primer evento al suscribir.
tracking.deliveredEntregado — estado final. La suscripción se cierra sola.
tracking.expiredPasaron ~21 días sin cerrarse. Soltamos la suscripción (los estados devuelto/cancelado no llegan por este canal — se cierran así). Trae el último status avisado, sin timeline.
webhook.pingVerificación de propiedad al registrar (PUT /v1/webhooks). Solo trae data.challenge; ver Verificación de propiedad.

Los hitos de status / timeline[].milestone son: registrado, origen, transito, demora, destino, reparto, entregado (en ese orden; status es el más avanzado presente).

Verificar la firma (opcional, recomendado)

Cada webhook viene firmado para que puedas comprobar que lo enviamos nosotros y no un impostor que conoce tu URL. Puedes empezar sin verificar para probar rápido, pero recomendamos hacerlo si tu sistema reacciona al evento con una acción automática — por ejemplo, dar la venta por cerrada, actualizar tu stock, o (en un marketplace) liberarle el pago al vendedor cuando el envío figura entregado. Si a tu servidor le llega un entregado falso y lo actúa a ciegas, te pueden engañar. El header X-Shalom-Signature tiene la forma t=<timestamp>,v1=<hmac>, donde v1 = HMAC-SHA256(t + "." + cuerpo_crudo, signing_secret) en hex. La clave es el signing_secret tal cual (el string de 64 caracteres hex), sin decodificarlo.

Si verificas: firma sobre el cuerpo CRUDO (los bytes tal cual llegaron), no sobre el JSON re-serializado. Compara con tiempo constante (timingSafeEqual) y rechaza si el t tiene más de 5 min (anti-replay). Si falta el header, o le falta t o v1, o los largos no coinciden, rechaza sin llamar a timingSafeEqual (lanza con largos distintos). Y responde el webhook.ping antes de verificar: en el alta te llega antes de que tengas el secret.

Node.js (Express)
const crypto = require('crypto');

// SECRET es el signing_secret TAL CUAL: el string de 64 caracteres hex.
// No lo decodifiques de hex — la clave del HMAC son esos caracteres como texto.
function verify(rawBody, header, secret) {
  if (!header) return false;
  const sig = {};
  for (const kv of header.split(',')) {
    const i = kv.indexOf('=');
    if (i > 0) sig[kv.slice(0, i).trim()] = kv.slice(i + 1).trim();
  }
  if (!sig.t || !sig.v1) return false;
  const expected = Buffer.from(crypto.createHmac('sha256', secret)
    .update(sig.t + '.').update(rawBody).digest('hex'));
  const recibida = Buffer.from(sig.v1);
  // timingSafeEqual lanza (→ 500) si los largos difieren: compáralos antes.
  if (expected.length !== recibida.length) return false;
  const firmaOk = crypto.timingSafeEqual(expected, recibida);
  const fresca  = Math.abs(Date.now() / 1000 - Number(sig.t)) < 300; // anti-replay
  return firmaOk && fresca;
}

// Express: usa el cuerpo CRUDO (express.raw), NO el JSON ya parseado.
app.post('/webhooks/shalom', express.raw({ type: 'application/json' }), (req, res) => {
  let event;
  try { event = JSON.parse(req.body); } catch { return res.status(400).end(); }

  // 1) El ping se responde ANTES de verificar la firma: en el alta llega antes
  //    de que tengas el signing_secret. Si lo rechazas, el webhook queda deshabilitado.
  if (event.event === 'webhook.ping') {
    return res.status(200).send(event.data.challenge);
  }

  // 2) Todo lo demás: verifica la firma antes de actuar.
  if (!verify(req.body, req.header('X-Shalom-Signature'), SECRET)) return res.status(400).end();
  // ... procesa event.data ... (deduplica por event.id)
  res.status(200).end();
});

Reintentos e idempotencia

  • Responde 2xx rápido: tienes 10 segundos. Si tu servidor responde algo distinto de 2xx (o no responde a tiempo), reintentamos el mismo cambio en el siguiente poll de esa suscripción (con backoff), con el mismo id, hasta que respondas 2xx o venza el TTL de 21 días. Si mientras tanto el envío avanzó, te llega directamente el estado nuevo (con otro id). Procesa en background si tu lógica es lenta.
  • tracking.expired se envía una sola vez. No se reintenta: si tu servidor no responde 2xx, la suscripción se cierra igual.
  • Deduplica por X-Shalom-Event-Id. Un reintento reusa el mismo id; guarda los ids procesados y descarta los repetidos (entrega at-least-once). El id es opaco: úsalo solo para deduplicar, no lo parsees.
  • El endpoint debe ser https y público. Rechazamos localhost, IPs privadas y de metadata (169.254.169.254) al registrar y en cada entrega.
  • No seguimos redirects. Apunta directo al endpoint final.
GET /v1/agencies

Lista paginada de agencias. La respuesta incluye total con el conteo del catálogo completo, para que sepas cuántas páginas esperar sin recorrerlas todas.

ParamDefaultDescripción
page1Página
per_page100Ítems por página (máx 1000). El catálogo completo (unas 546) entra en una sola llamada con per_page=1000; con incluir_deshabilitadas=false quedan solo las habilitadas (unas 485). El número exacto lo da total en la respuesta: Shalom abre y cierra locales, así que no lo fijes en tu código.
incluir_deshabilitadastruePor defecto se devuelven todas las agencias, cada una con su flag habilitado. false (o 0) devuelve solo las habilitadas.
200 OK
{
  "page": 1,
  "per_page": 100,
  "total": 546,
  "items": [
    {
      "id": 622,
      "abrebiatura": "BGACHCA",
      "nombre": "AMAZONAS / BAGUA / BAGUA / BAGUA CAPITAL",
      "departamento": "AMAZONAS",
      "provincia": "BAGUA",
      "direccion": "JR. AMAZONAS C-9 MZ. 126 LT. 25, BAGUA - BAGUA - AMAZONAS",
      "telefono": "(01) 500 7878",
      "latitud": -5.6347003028441,
      "longitud": -78.528378922503,
      "aereo": true,
      "estado": "ATENDIENDO EN ESTE MOMENTO",
      "categoria": "PEQUEÑA",
      "zona": "BAGUA",
      "lugar_over": "BAGUA CAPITAL",
      "dep_id": 1,
      "prov_id": 2,
      "dist_id": 5,
      "ubi_id": 10205,
      "reparto_habilitado": true,
      "habilitado": true,
      "principal": false,
      "internacional": false,
      "horario": { "hora_atencion": "LUNES A VIERNES - 8:00 AM A 8:00 PM" },
      "origenes_aereos": [2, 3, 7],
      "destinos_aereos": [623, 624, 632]
    }
  ]
}

Campos de cada agencia

Los campos de texto, números y listas sin dato no vienen en el JSON (no llegan como null): trata la ausencia como "sin dato". Siempre vienen id, abrebiatura, nombre, departamento, provincia y los booleanos.

CampoTipoDescripción
idintID de la agencia. Es el que va en origin_terminal_id / destiny_terminal_id al crear la orden.
nombrestringRuta completa DEPARTAMENTO / PROVINCIA / DISTRITO / LOCAL. Para mostrar sólo el local, usa lugar_over.
abrebiaturastringCódigo corto de Shalom ("BGACHCA"). Escrito así, con la errata, tal como lo manda Shalom.
lugar_overstringNombre del local a secas ("BAGUA CAPITAL"). Presente en todas.
zonastringZona comercial de Shalom. Presente en todas.
departamento
provincia
stringNombres en mayúsculas. Son los mismos valores que aceptan los filtros del buscador.
dep_id
prov_id
dist_id
intIDs de Shalom que cruzan con /v1/locations. No son ubigeos.
ubi_idintUbigeo INEI del distrito de la agencia. Presente en todas.
direccionstringDirección de calle, normalmente con referencia.
telefonostringSuele ser la central (01) 500 7878, no un número del local. Falta en torno al 7% (el campo no viene).
latitud
longitud
floatalrededor de 20 agencias no las tienen — esas quedan fuera del modo near. Si necesitas el catálogo completo, no filtres por cercanía.
horarioobjectEn la práctica sólo trae hora_atencion, un texto libre ("LUNES A VIERNES - 8:00 AM A 8:00 PM") presente en todas. Los demás subcampos (lunes_inicio, hora_entrega, domingo_fin…) vienen null en todas: Shalom los declara pero no los llena. No parsees el texto — cámbialo de formato sin aviso.
estadostringTexto de Shalom ("ATENDIENDO EN ESTE MOMENTO"). Prácticamente todas dicen lo mismo: no sirve para saber si está abierta ahora.
categoriastringTamaño del local: MICRO, MINI-MICRO, PEQUEÑA, MEDIANA, GRANDE / CO. Falta en algunas (el campo no viene).
aereoboolAdemás despacha carga aérea (cerca del 90%). Todas hacen terrestre.
reparto_habilitadoboolHace reparto a domicilio además de entrega en local (alrededor de un tercio).
habilitadoboolEl flag de habilitación para órdenes que Shalom asigna a la agencia (alrededor de 60 vienen en false). El listado y el buscador devuelven todas por defecto; para quedarte solo con las habilitadas pasa ?incluir_deshabilitadas=false. No uses estado para esto (casi todas dicen "ATENDIENDO EN ESTE MOMENTO").
internacionalboolOpera envíos internacionales (unas 40).
principalboolAgencia principal de su zona (unas 40).
origenes_aereos
destinos_aereos
int[]IDs de agencias con las que tiene ruta aérea. Sirven para saber si un par origen→destino se puede mandar por avión antes de cotizar.
GET /v1/agencies/{id}

Detalle completo de una agencia por ID numérico.

GET /v1/locations/departments

Todos los departamentos del Perú.

{
  "items": [
    { "id": 1, "name": "AMAZONAS" }
  ]
}
GET /v1/locations/departments/{depId}/provinces

Provincias de un departamento.

PathDescripción
depIdID del departamento
GET /v1/locations/departments/{depId}/provinces/{provId}/districts

Distritos de una provincia.

PathDescripción
depIdID del departamento
provIdID de la provincia

Es el único nivel que trae ubi_id: el ubigeo INEI de 6 dígitos del distrito (150201 = Barranca). Departamentos y provincias devuelven sólo id y name — su id es el de Shalom, no un ubigeo.

200 OK
{
  "items": [
    { "id": 1, "name": "BARRANCA", "ubi_id": 150201 },
    { "id": 2, "name": "PARAMONGA", "ubi_id": 150202 }
  ]
}

Crear pedido en Shalom Pro

Crea una guía de envío real en la cuenta de Shalom Pro del cliente y devuelve los identificadores para tracking, descarga de PDF y operaciones posteriores. El remitente es la cuenta autenticada; la resolución del destinatario, la validación del servicio de cobranza y la cotización se hacen automáticamente en una sola llamada.

Cada POST /v1/orders exitoso crea una preguía real en la cuenta de Shalom Pro, visible en el panel de pro.shalom.pe. Si la creaste por error, bórrala con DELETE /v1/orders/{id} mientras no haya sido despachada.

Estos endpoints necesitan X-API-Key + credenciales de Shalom Pro: ver Autenticación.

Latencia y reintentos

La primera llamada de cada cuenta hace un login real contra Shalom y tarda ~90 s, hasta 2 min. Las siguientes son rápidas: la sesión queda caliente y se reutiliza. Aplica a crear pedido, tracking, sesiones, bulk y productos.

Configura el timeout de tu cliente HTTP en al menos 150 s para estos endpoints. Con un timeout corto (20–30 s) la primera llamada se corta antes de terminar y te devuelve un error aunque la operación siga en curso del lado del servidor.

Si una llamada se corta por timeout

En las lecturas (tracking, productos, tarifa, listar órdenes, crear sesión) reintenta sin miedo: la sesión termina de calentarse en segundo plano y el reintento sale rápido.

No reintentes a ciegas POST /v1/orders ni /v1/orders/bulk. Un timeout de tu cliente no significa que la orden no se creó: puede haberse creado igual. Como la API no tiene clave de idempotencia, un reintento crearía una segunda guía real y cobrable. Ante un timeout al crear, consulta primero GET /v1/orders y revisa si la guía ya está ahí; reintenta solo si no aparece.

Usar la opción A concentra el arranque lento en una sola llamada: pides el token con POST /v1/shalom/sessions (una lectura, reintentable sin riesgo) y las órdenes posteriores ya salen rápidas.

POST /v1/shalom/sessions

Valida las credenciales contra pro.shalom.pe y emite un token efímero (formato ssk_<64-hex>) con TTL de 2 horas.

El password viaja en el body (no en headers, que suelen acabar en logs) y no se persiste: se usa para emitir el token y se descarta.

Request
curl -X POST https://api.shalom-api-peru.com/v1/shalom/sessions \
  -H "X-API-Key: tu-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "cliente@empresa.com",
    "password": "tu-password-de-shalom-pro"
  }'
200 OK
{
  "session_token": "ssk_a1b2c3d4e5f6...",
  "expires_at": "2026-05-08T18:23:01Z"
}

Body

CampoTipoDescripción
emailstringEmail de la cuenta en pro.shalom.pe.
passwordstringPassword de esa cuenta.

Cuándo expira el token

Dura 2 horas desde su emisión. Un token vencido devuelve 401 shalom_auth_failed: pide uno nuevo. El token solo sirve con la X-API-Key que lo emitió; usado con otra key, también devuelve 401 shalom_auth_failed. El token sobrevive a reinicios y despliegues del servicio dentro de esas 2 horas, así que no necesitas re-emitirlo tras un deploy.

DELETE /v1/shalom/sessions

Revoca un token antes de que expire y cierra la sesión viva que la API mantiene abierta contra Shalom para esa cuenta. Idempotente: si el token ya estaba revocado o nunca existió, igual responde 200. Útil para forzar logout cuando el cliente cambia de cuenta o detecta actividad sospechosa.

Request
curl -X DELETE https://api.shalom-api-peru.com/v1/shalom/sessions \
  -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Session: ssk_a1b2c3d4e5f6..."
200 OK
{ "revoked": true, "session_closed": true }

El token a revocar va en el header X-Shalom-Session, no en el path ni en el body. También puedes identificar la cuenta con X-Shalom-Email + X-Shalom-Password para cerrar su sesión viva, pero solo si no mandas X-Shalom-Session: si viene el token, se usa siempre ese. Si no mandas ni lo uno ni lo otro, devuelve 400. session_closed indica si se cerró la sesión viva contra Shalom (es false si el token ya no era válido).

GET /v1/products

Lista los productos disponibles en la cuenta de Shalom Pro del cliente: Sobre, Caja Paquete XXS/XS/S/M/L y Otra Medida. Devuelve el id que usas en el body de POST /v1/orders y las dimensiones default de cada uno.

Mismo auth Shalom que el resto (ver Autenticación). La conexión con Shalom se reutiliza entre todos los endpoints: si ya creaste una orden con esas credenciales, esta llamada no vuelve a pagar el login.

Request
curl https://api.shalom-api-peru.com/v1/products \
  -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Email: cliente@empresa.com" \
  -H "X-Shalom-Password: tu-password-de-shalom-pro"
200 OK
{
  "products": [
    {
      "id": 3,
      "title": "Sobre",
      "img": "https://pro.shalom.pe/img/products/sobre.png",
      "content": "Hasta 0.5 kg",
      "sub_content": "Documentos y similares",
      "measurements": {
        "weight": 0.5,
        "width": 0.3,
        "height": 0.02,
        "length": 0.4
      }
    },
    {
      "id": 1098,
      "title": "Otra Medida",
      "img": "https://pro.shalom.pe/img/products/custom.png",
      "content": "Define las dimensiones",
      "sub_content": "Para bultos fuera de catálogo",
      "measurements": { "weight": 0, "width": 0, "height": 0, "length": 0 }
    }
  ]
}

Campos del producto

CampoTipoDescripción
idintID del producto. Pásalo en product_id al crear la orden.
titlestringNombre legible: "Sobre", "Caja Paquete XS", "Otra Medida", etc.
contentstringDescripción corta del límite de peso.
sub_contentstringTexto auxiliar: ejemplos de uso.
imgstringURL del thumbnail para mostrar en tu UI.
measurementsobjectMedidas default { weight, width, height, length } en kg/m. Para Otra Medida vienen en cero — defínelas tú en el body de la orden.

Otra Medida — bulto fuera de catálogo

Si tu envío no entra en los tamaños estándar, usa el producto cuyo title sea "Otra Medida" y manda el campo opcional dimensions en el body de POST /v1/orders con { weight_kg, height_m, length_m, width_m }. Para los demás productos puedes omitirlo y se usan las medidas default.

POST /v1/tariff/calculate

Calcula el precio de un envío entre dos terminales sin comprometer la orden. Devuelve el precio para cada tipo de producto del catálogo (sobre, cajas XXS/XS/S/M/L y otra medida) para que puedas mostrar la cotización antes del checkout.

Request
curl -X POST https://api.shalom-api-peru.com/v1/tariff/calculate \
  -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Email: cliente@empresa.com" \
  -H "X-Shalom-Password: tu-password-de-shalom-pro" \
  -H "Content-Type: application/json" \
  -d '{
    "origin_terminal_id": 66,
    "destiny_terminal_id": 7,
    "dimensions": {
      "weight_kg": 1.0,
      "height_m": 0.1,
      "length_m": 0.2,
      "width_m": 0.15
    }
  }'
200 OK
{
  "currency": "PEN",
  "aereo": false,
  "breakdown": {
    "sobre": 8.00,
    "caja_paquete_xxs": 8.00,
    "caja_paquete_xs": 10.00,
    "caja_paquete_s": 12.00,
    "caja_paquete_m": 15.00,
    "caja_paquete_l": 20.00,
    "otra_medida": 25.00
  }
}

Body

CampoTipoDescripción
origin_terminal_idintegerID del terminal de origen (lo obtienes de /v1/agencies) (requerido)
destiny_terminal_idintegerID del terminal de destino (requerido)
dimensionsobjectPeso y medidas del paquete (weight_kg, height_m, length_m, width_m). Afectan SOLO el precio de otra_medida. Si no se pasan, se cotiza con todas las medidas en cero.
aereoboolCon true cotiza con la tarifa aérea (la misma que usa POST /v1/orders con aereo: true). La respuesta lo refleja en el campo aereo. Este endpoint no valida si la ruta admite envío aéreo; eso lo hace la creación de la orden.
product_idintegerSi lo pasas, la respuesta incluye un campo product con el precio puntual para mostrar al usuario. Si el id no existe en el catálogo, el campo se omite (la respuesta sigue siendo 200 con el breakdown).

Con product_id — precio puntual

Si ya sabes qué producto va a comprar el usuario, manda product_id y te ahorras el mapeo manual del breakdown:

Request
curl -X POST https://api.shalom-api-peru.com/v1/tariff/calculate \
  -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Email: cliente@empresa.com" \
  -H "X-Shalom-Password: tu-password-de-shalom-pro" \
  -H "Content-Type: application/json" \
  -d '{
    "origin_terminal_id": 66,
    "destiny_terminal_id": 7,
    "product_id": 3
  }'
200 OK
{
  "currency": "PEN",
  "aereo": false,
  "breakdown": {
    "sobre": 8.00,
    "caja_paquete_xxs": 8.00,
    "caja_paquete_xs": 10.00,
    "caja_paquete_s": 12.00,
    "caja_paquete_m": 15.00,
    "caja_paquete_l": 20.00,
    "otra_medida": 25.00
  },
  "product": {
    "id": 3,
    "title": "Sobre",
    "price": 8.00
  }
}

Errores: los generales. El 400 se da si faltan origin_terminal_id o destiny_terminal_id, Si Shalom no cotiza la ruta o el producto, responde 422 upstream_rejected con su motivo (prefijo "Shalom Pro: "); lo mismo pasa al crear un pedido.

POST /v1/orders

Crea una guía de envío. Devuelve guia, serie, codigo y ose_id. Para trackear usa guia + codigo (o el ose_id); para borrar necesitas el id que aparece en GET /v1/orders, que no viene en esta respuesta.

Request
curl -X POST https://api.shalom-api-peru.com/v1/orders \
  -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Email: cliente@empresa.com" \
  -H "X-Shalom-Password: tu-password-de-shalom-pro" \
  -H "Content-Type: application/json" \
  -d '{
    "origin_terminal_id": 404,
    "destiny_terminal_id": 7,
    "product_id": 3,
    "quantity": 1,
    "payer": "sender",
    "declaracion_jurada": "docs",
    "receiver": {
      "document_type": "DNI",
      "document": "87654321",
      "name": "MARIA",
      "last_name": "GOMEZ",
      "sur_name": "TORRES",
      "phone": 998765432
    },
    "pickup_code": "2415"
  }'
200 OK
{
  "guia": "80574902",
  "serie": "v872",
  "codigo": "CJTW",
  "ose_id": 584210
}

Guarda el ose_id: lo necesitas para descargar el rótulo y el comprobante. Qué es cada identificador de la respuesta, en Identificadores.

Con GET /v1/orders/{ose_id} recuperas el registro local completo de una orden (el body que mandaste y la respuesta), tal como se guardó al crearla. Solo existe para órdenes creadas con POST /v1/orders por tu misma API key; cualquier otra devuelve 404.

Todos los campos del body — requeridos y opcionales — están en Campos del body. Ojo con declaracion_jurada: la API lo exige ("docs", "ropa", "art" o "electro"); si lo omites, responde 400.

Errores propios de crear pedido

HTTPcodeCuándo se da
400bad_requestpickup_code inválido (no son 4 dígitos, repetidos, consecutivos o con forma de año). Se rechaza antes de llegar a Shalom.
400bad_requestDestinatario nuevo (no registrado en Shalom) sin teléfono que se pueda autocompletar. El mensaje lista los campos que faltan en receiver.
400bad_requestCon aereo: true: la ruta no admite aéreo ("envío aéreo no disponible: <motivo de Shalom>"), o Shalom no devuelve tarifa aérea para esa ruta y producto (precio 0).
422upstream_rejectedShalom rechazó la orden o el alta del destinatario por una regla de negocio (cuenta sin servicio de cobranza, datos que Shalom no acepta, etc.). El message trae el texto literal de Shalom con el prefijo "Shalom Pro: ".

El resto, en Errores.

401 — credenciales rechazadas
{
  "error": {
    "code": "shalom_auth_failed",
    "message": "X-Shalom-Email/Password rechazados por Shalom Pro",
    "request_id": "01KQ..."
  }
}
POST /v1/orders/bulk

Carga masiva de envíos desde la plantilla Excel de Shalom Pro (Formato-Pro-Masivo.xlsx). El wrapper importa el archivo, lo valida (todo-o-nada), asigna la misma clave de retiro a todo el lote y lo emite — en una sola llamada. Para crear un solo envío usa POST /v1/orders.

Es multipart/form-data, no JSON. Envía dos campos: file (el .xlsx) y pickup_code (clave de retiro de 4 dígitos aplicada a todos los envíos). Mismo auth que POST /v1/orders: X-API-Key + sesión Shalom (header X-Shalom-Session o X-Shalom-Email/X-Shalom-Password).

Campos (form-data)

CampoTipoDescripción
filefile (.xlsx)La plantilla Formato-Pro-Masivo con una fila por envío. Máximo 5 MB. (requerido)
pickup_codestringClave de retiro de 4 dígitos que se asigna a todos los envíos del archivo (mismas reglas que en la orden única). (requerido)
Request
curl -X POST https://api.shalom-api-peru.com/v1/orders/bulk \
  -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Email: cliente@empresa.com" \
  -H "X-Shalom-Password: tu-password-de-shalom-pro" \
  -F "file=@Formato-Pro-Masivo.xlsx" \
  -F "pickup_code=2415"
200 OK
{
  "count": 12,
  "message": "Envíos registrados",
  "created": []
}

created normalmente viene vacío: Shalom confirma la emisión pero no devuelve las guías. Para obtenerlas, consulta GET /v1/orders justo después — las del lote son las más recientes.

Validación todo-o-nada

Si cualquier fila tiene errores, no se crea ningún envío: la respuesta es 422 con el detalle por fila en rows (row, el id del borrador que se descartó y los errors de Shalom) para que corrijas el Excel y reintentes. Si Shalom rechaza el archivo entero, el message trae su motivo y rows puede venir vacío.

422 — filas con errores
{
  "error": {
    "code": "upstream_rejected",
    "message": "Shalom Pro: el archivo tiene filas con errores; corrige y vuelve a subirlo",
    "request_id": "01KQ..."
  },
  "rows": [
    {
      "row": 3,
      "id": 5512001,
      "errors": ["El destinatario no tiene documento válido"]
    },
    {
      "row": 7,
      "id": 5512005,
      "errors": ["Terminal de destino no encontrado", "Peso requerido"]
    }
  ]
}

Errores propios de bulk

HTTPcodeCuándo se da
413payload_too_largeEl archivo supera los 5 MB.
415unsupported_media_typeEl body no es multipart/form-data.
422upstream_rejectedHay filas con errores (detalle en rows) o Shalom rechazó el archivo. No se creó ningún envío.

El resto, en Errores.

GET /v1/orders

Lista las órdenes de la cuenta: preguías y envíos despachados. Sin query params devuelve todas en una sola respuesta, sin campo meta. Al mandar page, per_page o state se activa la paginación y se agrega meta.

Cuidado con cuentas grandes. Cada orden pesa ~3 KB: una cuenta con 5.000 envíos devuelve ~15 MB si no paginas. Usa per_page.

Hay dos caminos. Rápido (paginado por Shalom): si mandas state, o from junto con page/per_page, y no mandas status, la API pide a Shalom solo lo que necesita. Lento (lista completa): en cualquier otro caso (incluido usar status) la API trae todas las órdenes de la cuenta y filtra y pagina en memoria: recorta la respuesta, pero no la carga contra Shalom.

Query params (opcionales)

ParamDefaultDescripción
page1Número de página. Mandar page o per_page activa la paginación y agrega meta. Una página fuera de rango devuelve "orders": [] con el meta correcto.
per_page20Ítems por página. Tope máximo 200 (valores mayores se recortan a 200); con state el tope es 100.
state—Estado: pending (pendientes de aprobación), in_transit (en tránsito) o delivered (entregados). Acepta alias como pendiente, transito, entregado o history. Usa el camino rápido y siempre devuelve meta, aunque no mandes page.
guia—Filtra por match exacto del campo guia.
status—Filtra por match exacto del código numérico de status (ej. 1). Fuerza el camino lento; para filtrar por estado usa state.
from—Fecha mínima (YYYY-MM-DD), inclusive. Filtra por la fecha de created_at.
to—Fecha máxima (YYYY-MM-DD), inclusive. Filtra por la fecha de created_at.
document_type / document_number / destination—Filtros que se pasan tal cual a Shalom. Solo aplican en el camino rápido; en el lento se ignoran.

Los filtros se combinan con AND y se aplican antes de paginar, así que meta.total refleja el conteo ya filtrado. Filtrar sin paginar (y sin state) devuelve el mismo shape de siempre (sin meta). Con from + paginación y sin state, la API combina los tres estados; si alguno no responde en Shalom, sirve el resto y marca meta.partial: true (puede faltar órdenes de ese estado).

Request — sin params (lista completa)
curl https://api.shalom-api-peru.com/v1/orders \
  -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Email: cliente@empresa.com" \
  -H "X-Shalom-Password: tu-password-de-shalom-pro"
200 OK
{
  "orders": [
    {
      "id": 87654321,
      "internal_id": 4567890,
      "guia": "12345678",
      "serie": "s001",
      "codigo": "ABCD",
      "pickup_code": "2415",
      "created_at": "2026-05-08 10:11:12",
      "updated_at": "2026-05-08 10:11:13",
      "shipping_date": "2026-05-08",
      "status": 1,
      "payer": "receiver",
      "aereo": false,
      "delivered": false,
      "paid": false,
      "origin": {
        "ter_id": 404,
        "name": "SALAS ICA",
        "address": "SUB LOTE 01 ZONA PANAMERICANA SUR KM. 293.350...",
        "abbreviation": "sls",
        "phone": "(01) 500 7878",
        "ubigeo": "ICA - ICA - SALAS"
      },
      "destination": {
        "ter_id": 7,
        "name": "AV PARRA 379 CO",
        "address": "AV. PARRA 379",
        "abbreviation": "AQP",
        "phone": "0",
        "ubigeo": "AREQUIPA - AREQUIPA - AREQUIPA"
      },
      "sender": {
        "id": 0,
        "document": "12345678",
        "name": "",
        "last_name": "",
        "sur_name": "",
        "full_name": "JUAN PEREZ GARCIA",
        "phone": 0
      },
      "receiver": {
        "id": 0,
        "document": "87654321",
        "name": "MARIA",
        "last_name": "GOMEZ",
        "sur_name": "TORRES",
        "full_name": "MARIA GOMEZ TORRES",
        "phone": 998765432
      },
      "items": [
        {
          "id": 11223344,
          "quantity": 1,
          "weight": 0,
          "width": 0,
          "length": 0,
          "height": 0,
          "status": 1,
          "product_name": "Sobre",
          "product_detail": "Documentos simples en sobre manila / Tamaño A4",
          "product_price": 20
        }
      ],
      "warranty": { "active": false, "amount": 0, "total_cost": 0, "percent": 0.01 }
    }
  ]
}
Request — paginado + filtrado
# Paginado por Shalom (rápido): estado + rango de fechas, con "meta"
curl "https://api.shalom-api-peru.com/v1/orders?state=delivered&page=1&per_page=20&from=2026-05-01&to=2026-05-31" \
  -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Email: cliente@empresa.com" \
  -H "X-Shalom-Password: tu-password-de-shalom-pro"
200 OK (con meta)
{
  "orders": [
    { "id": 87654321, "guia": "12345678", "status": 1, "delivered": true, "created_at": "2026-05-08 10:11:12" }
  ],
  "meta": {
    "total": 142,
    "page": 1,
    "per_page": 20,
    "last_page": 8
  }
}

Campos de la orden

CampoTipoDescripción
id / internal_id / guia / serie / codigo / pickup_code—Identificadores del envío. Ver Identificadores.
created_at / updated_at / shipping_datestringTimestamps en formato Lima (YYYY-MM-DD HH:MM:SS y YYYY-MM-DD).
statusintCódigo numérico de estado de la orden (1 = creada, etc.). El significado lo define el flujo operativo de Shalom Pro. En el camino rápido Shalom no lo manda y viene como 1: para saber si se entregó usa delivered.
payerstring"sender" o "receiver" (mapeado de REMITENTE/DESTINATARIO).
aereo / delivered / paidboolBooleanos que indican si el envío es aéreo, ya fue entregado y/o ya fue pagado.
origin / destinationobjectAgencia con ter_id, name, address, abbreviation, phone, ubigeo.
sender / receiverobjectPersona con id, document, name, last_name, sur_name, full_name, phone (y email / address solo si hay dato). Qué tan completos vienen depende del camino: en el rápido Shalom manda document, el nombre completo (en full_name y también en name) y phone. En el lento Shalom los manda vacíos y la API los rellena: con lo que mandaste en POST /v1/orders si la orden se creó por esta API, y si no, solo document + full_name desde el rastreo público (sin teléfono; best-effort, con tope por request). Lo que no se pudo rellenar queda vacío.
items[]arrayDetalle por bulto: quantity, weight, width, length, height, product_name, product_detail, product_price.
warrantyobject{ active, amount, total_cost, percent }. Si la orden no tiene garantía, active es false.
DELETE /v1/orders/{id}

Borra una orden de la cuenta del cliente. Funciona tanto para borradores (preguías sin guía empresarial real) como para guías ya emitidas que aún no fueron recibidas en agencia.

{id} es el campo id de GET /v1/orders, no el ose_id ni la guia (ver Identificadores).

Operación destructiva. La orden no se puede recuperar una vez borrada.

Es lento en cuentas grandes. Para ubicar la orden, la API trae la lista completa de la cuenta (el camino lento de GET /v1/orders) antes de borrar. Usa el mismo timeout largo que para crear (al menos 150 s).

Request
curl -X DELETE https://api.shalom-api-peru.com/v1/orders/87654321 \
  -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Email: cliente@empresa.com" \
  -H "X-Shalom-Password: tu-password-de-shalom-pro"
200 OK
{
  "deleted": true,
  "id": 87654321
}

El error propio de este endpoint es el 422: Shalom rechaza el borrado cuando la guía ya fue recibida en agencia o despachada. El resto, en Errores.

422 — Shalom rechaza el borrado
{
  "error": {
    "code": "upstream_rejected",
    "message": "Shalom Pro rechazó el borrado: shalompro: delete /delete-guia: La guia ya fue recibida en agencia",
    "request_id": "01KQ..."
  }
}
GET /v1/orders/{ose_id}/label

Descarga el rótulo (la etiqueta que se pega en el paquete) como PDF binario: Content-Type: application/pdf. El ose_id lo devuelve POST /v1/orders (ver Identificadores).

Request
curl https://api.shalom-api-peru.com/v1/orders/1234567890/label \
  -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Email: cliente@empresa.com" \
  -H "X-Shalom-Password: tu-password-de-shalom-pro" \
  --output rotulo.pdf

Devuelve 400 si el ose_id no es un entero positivo, y 404 cuando Shalom responde 404 al descargar el PDF. Si Shalom rechaza el ose_id al generar el enlace de descarga o devuelve algo que no es un PDF, la respuesta es 502. Ver Errores.

GET /v1/orders/{ose_id}/voucher

Descarga el comprobante de la orden (resumen con datos de remitente, destinatario e items) como PDF. Mismo contrato que /label: Content-Type: application/pdf binario y mismo path param {ose_id}.

Request
curl https://api.shalom-api-peru.com/v1/orders/1234567890/voucher \
  -H "X-API-Key: tu-api-key" \
  -H "X-Shalom-Email: cliente@empresa.com" \
  -H "X-Shalom-Password: tu-password-de-shalom-pro" \
  --output comprobante.pdf

Errores específicos

Mismos códigos que /label (400, 404, 502).

Campos del body

El remitente lo toma Shalom de la cuenta autenticada (X-Shalom-Email / la sesión que uses). El body solo lleva al destinatario en receiver, y un campo sender suelto se ignora. Única excepción: si mandas shipment_type: "empresarial", la guía se emite a nombre de una empresa remitente y ahí sí el sender es obligatorio (ver más abajo).

Requeridos

CampoTipoDescripción
origin_terminal_idintID de la agencia de origen (campo id de GET /v1/agencies)
destiny_terminal_idintID de la agencia de destino (campo id de GET /v1/agencies)
product_idintID del producto (Sobre, Caja XXS/XS/S/M/L, Otra Medida). Listalo con GET /v1/products.
receiverobjectDestinatario. Siempre debe incluir document_type + document; sin id, también name (y last_name salvo RUC). Ver subcampos abajo.
pickup_codestringClave de retiro de 4 dígitos. Shalom rechaza tres formas: repetidos (1111..9999), consecutivos (1234..6789) y cualquier año entre 1900 y 2100 (2024, 1985…). Si mandas uno de esos, la respuesta es 400 antes de llegar a Shalom. Shalom puede aplicar reglas propias además de estas; si rechaza la clave, la respuesta es 422 con su mensaje.
declaracion_juradastringTipo de contenido declarado — la API lo exige en toda orden. Si Shalom no la admite para esa orden (por ejemplo, cuenta o destinatario con RUC), la API la quita y reintenta automáticamente: la orden se crea sin declaración jurada. Acepta 4 alias cortos: "docs", "ropa", "art" (artículos de uso personal), "electro" (electrodomésticos); o los 4 textos literales que Shalom imprime en la guía ("Documentos", "Ropa", "Articulos de uso personal" sin tilde, "Electrodomésticos" con tilde). Si lo omites, la API responde 400 con los valores aceptados. (requerido)

Opcionales

CampoTipoDescripción
quantityintDefault 1.
shipment_typestringSi lo omites, la guía se crea como siempre (remitente = la cuenta autenticada). Con "empresarial" se emite una guía empresarial a nombre de una empresa remitente, y pasa a ser obligatorio el campo sender. Ver Guía empresarial.
senderobjectSolo se usa con shipment_type: "empresarial". Identifica a la empresa remitente: { "document_type": "RUC", "document": "20xxxxxxxxx" }. En el flujo normal se ignora.
payerstring"sender" (default) o "receiver" (contra entrega).
dimensionsobjectDimensiones custom { weight_kg, height_m, length_m, width_m } — valores numéricos (peso en kg, medidas en metros, ej. { "weight_kg": 2.5, "height_m": 0.3, "length_m": 0.4, "width_m": 0.2 }). Solo para producto "Otra Medida".
aereoboolServicio aéreo. La API primero valida la ruta con Shalom: si no admite aéreo responde 400 "envío aéreo no disponible: <motivo de Shalom>", sin crear nada. Si la ruta lo admite, cotiza y emite con la tarifa aérea. Puedes cotizarla antes con aereo: true en POST /v1/tariff/calculate.
warrantyobject{ "amount": "100.00" } activa garantía con monto declarado.
collection_serviceobjectCobro contra entrega. Objeto { cost, data } — ver subcampos abajo. Requiere cuenta bancaria registrada en Shalom Pro previamente.
documentationarrayGuías adicionales: [{ "guide": "G123", "series": "S01" }].
contacto_docstringDocumento de la persona que recoge.
trackboolCon true, la guía recién creada queda suscrita al tracking por webhook automáticamente — recibes un POST en cada cambio de estado (ver Suscribir envíos). Best-effort: si no tienes webhook configurado o llegaste al cupo, la orden se crea igual.

Guía empresarial (remitente empresa)

Por defecto la guía se emite a nombre del titular de la cuenta (una persona), y eso no se puede cambiar: el flujo estándar de Shalom no acepta otro remitente. Si necesitas que salga a nombre de tu empresa, agrega estos dos campos al body — el resto queda igual:

"shipment_type": "empresarial",
"sender": { "document_type": "RUC", "document": "20xxxxxxxxx" }

El document es el RUC de la empresa que quieres que figure como remitente. Sin shipment_type, todo se comporta exactamente como antes.

Requisito previo: la empresa tiene que estar registrada y aprobada en tu cuenta de Shalom Pro (panel: Tu Negocio → Crear negocio — RUC, representante legal y documentos de respaldo; Shalom la aprueba manualmente). Si el RUC no está registrado, la API responde 404 con el mensaje "la empresa (RUC) no está registrada en la cuenta Shalom Pro".

Ten en cuenta que una guía empresarial es un producto distinto dentro de Shalom, no la misma guía con otro nombre impreso: tiene su propia tarifa y su propio flujo de emisión. El remitente empresa aparece en el comprobante (/voucher); el rótulo solo muestra destinatario y destino.

Limitaciones actuales del flujo empresarial:

  • Siempre emite el producto SOBRE: product_id se ignora.
  • El destinatario tiene que ser una persona (DNI/CE) ya registrada en Shalom: con RUC responde 400, y si no está registrada, 404.
  • payer, warranty, collection_service y declaracion_jurada no se aplican.
  • Con aereo: true no se valida la ruta ni se usa la tarifa aérea.
  • La respuesta solo trae ose_id (guia, serie y codigo vienen vacíos): la guía emitida se ve después en GET /v1/orders.

receiver (destinatario)

El destinatario (persona natural o empresa) siempre se identifica por documento: document_type + document son obligatorios, aunque pases también el id. Según lo que envíes:

  1. Sin id: además de document_type + document, la API exige siempre name y — salvo RUC — last_name, aunque el destinatario ya exista (si falta, 400). Si ya existe en Shalom, se reusa. Si aún no existe, para darlo de alta Shalom necesita también phone (y sur_name para DNI/CE): la API intenta completar lo que falte desde la búsqueda de personas de Shalom; si no consigue el teléfono, responde 400 listando los campos que faltan. Opcionales: email, address.
  2. Con id: name y last_name pasan a ser opcionales, pero document_type + document siguen siendo obligatorios: el destinatario se resuelve por documento.

El remitente no va en el body: Shalom lo toma de la cuenta autenticada.

Empresas (RUC): la razón social va completa en name. last_name y sur_name no aplican.

Ejemplo: persona (DNI)

{
  "document_type": "DNI",
  "document": "12345678",
  "name": "JUAN",
  "last_name": "PEREZ",
  "sur_name": "GARCIA",
  "phone": 999888777
}

Ejemplo: empresa (RUC)

{
  "document_type": "RUC",
  "document": "20123456789",
  "name": "MI EMPRESA S.A.C.",
  "phone": 999888777
}
CampoTipoDescripción
idintperson_id de Shalom Pro. Si lo pasas, name y last_name dejan de ser obligatorios; document_type + document siguen siéndolo.
document_typestring"DNI" (8 dígitos), "RUC" (11 dígitos, empresas) o "CE" (carnet de extranjería). En mayúsculas: "dni" devuelve 400.
documentstringSolo dígitos para DNI/RUC; alfanumérico ≥4 chars para CE.
namestringNombre (persona) o razón social completa (empresa). Obligatorio si no pasas id.
last_namestringPrimer apellido. Obligatorio para DNI/CE si no pasas id; no aplica a RUC.
sur_namestringSegundo apellido. No aplica a RUC.
phoneintTeléfono como número, sin comillas (sin código de país, ej. 999888777); si lo mandas como texto, la API responde 400. Necesario para dar de alta un destinatario nuevo.
emailstringOpcional.
addressstringOpcional.

Servicio de cobranza (collection_service)

Cobro contra entrega: Shalom cobra al destinatario y deposita en la cuenta bancaria que indiques. Es un objeto con cost (el monto a cobrar) y data (los datos de la cuenta donde se deposita).

Requiere que la cuenta tenga el servicio de cobranza habilitado en pro.shalom.pe. Si no lo está, la orden falla con 422 upstream_rejected.

CampoTipoDescripción
costnumberMonto a cobrar al destinatario (soles). Numérico, sin comillas.
data.documentstringDocumento del titular de la cuenta.
data.namestringNombre del titular de la cuenta.
data.bankstringBanco: "BCP", "BBVA", "Interbank" o "Scotiabank".
data.type_accountstringTipo de cuenta: "Ahorro" o "Corriente".
data.account_numberstringNúmero de cuenta.
data.ccistringCódigo de Cuenta Interbancario (CCI), 20 dígitos.

Ejemplo

{
  "cost": 150.00,
  "data": {
    "document": "12345678",
    "name": "JUAN PEREZ",
    "bank": "BCP",
    "type_account": "Ahorro",
    "account_number": "1234567890",
    "cci": "00212312345678901234"
  }
}

Errores

Todos los errores de /v1 usan la misma estructura. Las únicas excepciones son las rutas que no existen y los métodos HTTP no soportados: devuelven el 404 o 405 por defecto, en texto plano y sin este envelope.

{
  "error": {
    "code": "not_found",
    "message": "orden no existe",
    "request_id": "01JXZ..."
  }
}
HTTPcodeDescripción
400bad_requestParámetros faltantes o inválidos
401unauthorizedAPI key inválida o ausente
401key_revokedLa API key fue revocada
401key_expiredLa API key expiró; renuévala
401shalom_auth_failedCredenciales de Shalom Pro ausentes, inválidas o rechazadas (/v1/shalom/sessions, /v1/orders, /v1/orders/bulk, DELETE /v1/orders/{id}, label y voucher, /v1/products, /v1/persons/search, /v1/tariff/calculate; no aplica a GET /v1/orders/{ose_id}, que solo pide la API key). El tracking nunca devuelve 401: sin credenciales válidas responde solo con estado e indica el motivo en degraded_reason.
403key_disabledLa API key está deshabilitada
404not_foundRecurso no existe
409conflictConflicto de estado
413payload_too_largeEl body JSON supera el límite del endpoint (todos los que reciben JSON, salvo POST /v1/tracking/batch, que acota por cantidad de items), o el archivo de /v1/orders/bulk supera el tamaño máximo
415unsupported_media_typeContent-Type no soportado: los POST/PUT que reciben JSON exigen Content-Type: application/json (salvo POST /v1/tracking/batch), y /v1/orders/bulk espera multipart/form-data
422upstream_rejectedShalom Pro rechazó la solicitud por una regla de negocio
429rate_limitedRate limit excedido
429quota_exceededTope de suscripciones de webhook activas alcanzado (solo POST /v1/tracking/subscriptions)
502upstream_unavailableShalom no respondió o devolvió algo inesperado
502shalom_unavailableShalom Pro no disponible o respondió algo inesperado (endpoints de Shalom Pro: sesiones, órdenes, productos, personas, tarifa). Reintenta.
503shalom_login_unavailableNo se pudo autenticar contra Shalom Pro en ese momento (login saturado o rechazado por su verificación de seguridad). Transitorio: reintenta respetando el header Retry-After.
504upstream_timeoutShalom no respondió dentro del tiempo de espera
500internalError interno

Vencimiento de la API key

Si tu key tiene fecha de vencimiento, toda respuesta autenticada trae el header X-API-Key-Expires-At con esa fecha en RFC3339 (ej. 2026-11-01T05:00:00Z). Úsalo para renovar antes de empezar a recibir 401 key_expired.

Rate Limits

Límite por API key según tu plan: 60–600 requests por minuto (60 por defecto).

Es un token bucket: se recarga al ritmo de tu plan y admite una ráfaga de min(rpm, max(10, rpm/4)) requests seguidos. Los endpoints de /v1/me/* no consumen cupo.

Requests / minRáfaga
6015
12030
24060
600150

Rastreo en lote. POST /v1/tracking/batch cuesta un request del límite general y, además, cada guía del lote descuenta de un presupuesto de rastreo aparte (mismo ritmo por minuto que tu plan; un lote de hasta 50 guías siempre entra de una vez). Si lo agotas, el batch responde 429 rate_limited con Retry-After, pero el resto de endpoints sigue funcionando. Este presupuesto no se refleja en los headers X-RateLimit-*.

Toda respuesta (no solo el 429) incluye los headers X-RateLimit-*, así sabes tu límite y cuánto te queda sin tener que chocar contra el error. Los exponemos vía Access-Control-Expose-Headers, así que también son legibles desde el browser.

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 12
X-RateLimit-Reset: 1713200060

Headers

HeaderDescripción
X-RateLimit-LimitRequests por minuto de tu plan (60–600).
X-RateLimit-RemainingRequests disponibles ahora mismo (capacidad de ráfaga restante, como máximo la ráfaga de tu plan). Llega a 0 cuando estás topeado.
X-RateLimit-ResetEpoch en segundos (UTC). Es aproximado y siempre apunta al futuro, incluso con el cupo lleno: no indica que el cupo esté lleno. Para saber cuánto te queda usa X-RateLimit-Remaining, y para reintentar, Retry-After.
Retry-AfterEn el 429 y en el 503 shalom_login_unavailable: segundos a esperar antes de reintentar (mínimo 1).

Health Checks

Públicos, sin autenticación (no llevan X-API-Key).

GET /healthz

Liveness. 200 si el servidor está vivo.

GET /readyz

Readiness. 200 cuando la base de datos responde, la última sincronización de agencias tiene menos de 24 h y el último chequeo contra Shalom salió bien. Si algo falla, 503 con un JSON que indica qué componente está mal (db, sync, upstream).

GET /status

Estado del servicio. Devuelve el último snapshot público del monitoreo, con el estado general en overall (unknown si todavía no hay datos). Es lo que muestra la página Estado.