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.
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:
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:
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).
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.
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ón | Headers | Cuá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 — directo | X-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 674Identificadores
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.
| Campo | Qué es | Dónde lo obtienes | Dónde lo usas |
|---|---|---|---|
| guia | 8–10 dígitos. Va impresa en el comprobante físico. | POST /v1/orders | GET /v1/tracking?numero= |
| codigo | Alfanumérico de 4 caracteres. Lo asigna Shalom. | POST /v1/orders | GET /v1/tracking?codigo= |
| ose_id | ID 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 |
| id | ID de la orden dentro de la cuenta empresarial. | GET /v1/orders | DELETE /v1/orders/{id} |
| pickup_code | Clave de 4 dígitos para retirar en agencia. La eliges tú. | La defines al crear la orden | La presenta el destinatario en agencia |
| serie | Prefijo del talonario (ej. "v872"). | POST /v1/orders | Informativo. No se usa en esta API. |
| internal_id | ID interno del panel de Shalom Pro. | GET /v1/orders | Informativo. 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)
Param Tipo Descripción numero string 8 a 10 dígitos (ej. "12345678"). Es la guia. codigo string Alfanumérico de 4 caracteres (ej. "CJTW"). ose_id string ID 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.
Valor Qué significa sin_credenciales No mandaste credenciales Shalom, o mandaste un X-Shalom-Session inválido, expirado o de otra API key. Es el modo estado normal. shalom_auth_failed Shalom rechazó la cuenta. Reintentar no ayuda: revisa las credenciales. shalom_login_unavailable El 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_disponible Rastreo 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.
Hito Descripción registrado La orden se registró en el sistema. origen Recibido en la agencia de origen. transito En 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. demora Incidencia que retrasó el envío. Normalmente null. destino Llegó a la agencia de destino. reparto Salió a reparto a domicilio. null si la entrega es en agencia. entregado Entregado 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
Campo Tipo Descripción items[] array Lista de guías a rastrear. Requerido, de 1 a 50 elementos. items[].custom_id string Etiqueta 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_id string Identificadores 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
Campo Tipo Descripción results[].ok bool true si la guía se rastreó; false si falló. results[].tracking object Presente cuando ok: true. Mismo shape que la respuesta de GET /v1/tracking (detailed, order, status y, si degrada, degraded_reason). results[].error object Presente cuando ok: false. { code, message }, con el mismo code que devolvería el tracking individual. summary object { 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.
Path Tipo Descripción ose_id string ID 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.
Param En Descripción ose_id path ID interno de Shalom del envío — el campo order.ose_id de la respuesta de GET /v1/tracking. cap_id query ID 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étodo Ruta Descripción POST /v1/webhooks/rotate Genera un signing_secret nuevo (el anterior deja de valer) y lo devuelve una vez. 404 si no tienes webhook configurado. DELETE /v1/webhooks Deshabilita 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étodo Ruta Descripción GET /v1/tracking/subscriptions Lista 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
event Cuándo tracking.updated Cambió el estado del envío (apareció un hito). También el primer evento al suscribir. tracking.delivered Entregado — estado final. La suscripción se cierra sola. tracking.expired Pasaron ~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.ping Verificació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.
Param Default Descripción page 1 Página per_page 100 Í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_deshabilitadas true Por 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.
Campo Tipo Descripción id int ID de la agencia. Es el que va en origin_terminal_id / destiny_terminal_id al crear la orden. nombre string Ruta completa DEPARTAMENTO / PROVINCIA / DISTRITO / LOCAL. Para mostrar sólo el local, usa lugar_over. abrebiatura string Código corto de Shalom ("BGACHCA"). Escrito así, con la errata, tal como lo manda Shalom. lugar_over string Nombre del local a secas ("BAGUA CAPITAL"). Presente en todas. zona string Zona comercial de Shalom. Presente en todas. departamento
provincia string Nombres en mayúsculas. Son los mismos valores que aceptan los filtros del buscador. dep_id
prov_id
dist_id int IDs de Shalom que cruzan con /v1/locations. No son ubigeos. ubi_id int Ubigeo INEI del distrito de la agencia. Presente en todas. direccion string Dirección de calle, normalmente con referencia. telefono string Suele ser la central (01) 500 7878, no un número del local. Falta en torno al 7% (el campo no viene). latitud
longitud float alrededor de 20 agencias no las tienen — esas quedan fuera del modo near. Si necesitas el catálogo completo, no filtres por cercanía. horario object En 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. estado string Texto de Shalom ("ATENDIENDO EN ESTE MOMENTO"). Prácticamente todas dicen lo mismo: no sirve para saber si está abierta ahora. categoria string Tamaño del local: MICRO, MINI-MICRO, PEQUEÑA, MEDIANA, GRANDE / CO. Falta en algunas (el campo no viene). aereo bool Además despacha carga aérea (cerca del 90%). Todas hacen terrestre. reparto_habilitado bool Hace reparto a domicilio además de entrega en local (alrededor de un tercio). habilitado bool El 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"). internacional bool Opera envíos internacionales (unas 40). principal bool Agencia 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/search Busca por texto o por filtros de ubicación. No pagina: devuelve hasta per_page resultados (page se ignora).
Param Tipo Descripción q string Texto libre. Coincidencia parcial, sin distinguir mayúsculas, contra nombre o abrebiatura. departamento string Filtrar por departamento (coincidencia parcial, sin distinguir mayúsculas) provincia string Filtrar por provincia (coincidencia parcial, sin distinguir mayúsculas) aereo bool Además despacha carga aérea. No es el tipo de agencia: todas operan envío terrestre, y este flag marca las que también hacen aéreo (cerca del 90%). Filtrar por aereo=false devuelve las que solo hacen terrestre, no "las terrestres". Solo true o 1 cuentan como verdadero: cualquier otro valor filtra como false. near lat,lng Ordena por cercanía a ese punto y agrega distancia_km. Equivalente: lat= + lng= por separado. radius_km number Sólo con near. Acota el resultado a ese radio en kilómetros. incluir_deshabilitadas bool Default true: la búsqueda incluye todas, cada una con su flag habilitado. false (o 0) devuelve solo las habilitadas. per_page int (default 100, máx 500) Cuántas devolver. Un valor mayor a 500 se recorta a 500.
Agencia más cercana
Con near el catálogo se devuelve ordenado de la agencia más cercana a la más lejana, y cada una trae distancia_km. Útil para elegir el punto de recojo automáticamente a partir de la dirección del cliente, sin traerte el catálogo entero en cada llamada.
curl "https://api.shalom-api-peru.com/v1/agencies/search?near=-12.1211,-77.0300&radius_km=5&per_page=3" \
-H "X-API-Key: tu-api-key"
200 OK {
"items": [
{
"id": 190,
"nombre": "LIMA / LIMA / MIRAFLORES / AV. JOSE PARDO",
"departamento": "LIMA",
"provincia": "LIMA",
"direccion": "AV. JOSE PARDO N°533",
"latitud": -12.119319117645,
"longitud": -77.034353944233,
"aereo": false,
"distancia_km": 0.51
},
{
"id": 435,
"nombre": "LIMA / LIMA / MIRAFLORES / AV. COMANDANTE ESPINAR",
"distancia_km": 0.91
}
]
}
El per_page se aplica después de ordenar por distancia, así que per_page=1 devuelve la más cercana. Se combina con q, departamento, provincia y aereo — por ejemplo, la agencia aérea más cercana. La veintena de agencias sin coordenadas queda fuera de este modo.
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.
Path Descripción depId ID del departamento
GET /v1/locations/departments/{depId}/provinces/{provId}/districts Distritos de una provincia.
Path Descripción depId ID del departamento provId ID 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
Campo Tipo Descripción email string Email de la cuenta en pro.shalom.pe. password string Password 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
Campo Tipo Descripción id int ID del producto. Pásalo en product_id al crear la orden. title string Nombre legible: "Sobre", "Caja Paquete XS", "Otra Medida", etc. content string Descripción corta del límite de peso. sub_content string Texto auxiliar: ejemplos de uso. img string URL del thumbnail para mostrar en tu UI. measurements object Medidas 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.
GET /v1/persons/search?document={document}&type={type}
Resuelve un documento (DNI/RUC/CE) a una persona registrada en pro.shalom.pe. Útil para autocomplete en formularios — el usuario tipea su DNI y tú pre-cargas nombre, apellidos y teléfono antes del submit.
No es obligatorio antes de crear una orden. POST /v1/orders ya resuelve la persona automáticamente cuando le pasas el documento. Este endpoint sirve cuando quieres mostrar los datos al usuario antes del submit. Pasar el id en receiver solo te exime de mandar name / last_name: el documento sigue siendo obligatorio.
Request curl "https://api.shalom-api-peru.com/v1/persons/search?document=70123456&type=DNI" \
-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 {
"id": 1234567,
"document_type": "DNI",
"document": "70123456",
"name": "JUANA",
"last_name": "RAMOS",
"sur_name": "LOPEZ",
"full_name": "JUANA RAMOS LOPEZ",
"phone": 999111222
}
Query params
Param Tipo Descripción document string Documento a buscar (8 dígitos para DNI, 11 para RUC, alfanumérico ≥4 para CE) (requerido) type string "DNI", "RUC" o "CE" (case-insensitive) (requerido)
Errores específicos
HTTP code Cuándo se da 400 bad_request Falta document o type, type fuera de DNI/RUC/CE, o longitud inválida (DNI≠8, RUC≠11, etc.) 404 not_found El DNI/RUC/CE no figura en Shalom Pro.
404 — persona no encontrada {
"error": {
"code": "not_found",
"message": "persona no registrada en Shalom Pro",
"request_id": "01KS..."
}
}
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
Campo Tipo Descripción origin_terminal_id integer ID del terminal de origen (lo obtienes de /v1/agencies) (requerido) destiny_terminal_id integer ID del terminal de destino (requerido) dimensions object Peso 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. aereo bool Con 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_id integer Si 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
HTTP code Cuándo se da 400 bad_request pickup_code inválido (no son 4 dígitos, repetidos, consecutivos o con forma de año). Se rechaza antes de llegar a Shalom. 400 bad_request Destinatario nuevo (no registrado en Shalom) sin teléfono que se pueda autocompletar. El mensaje lista los campos que faltan en receiver. 400 bad_request Con 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). 422 upstream_rejected Shalom 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)
Campo Tipo Descripción file file (.xlsx) La plantilla Formato-Pro-Masivo con una fila por envío. Máximo 5 MB. (requerido) pickup_code string Clave 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
HTTP code Cuándo se da 413 payload_too_large El archivo supera los 5 MB. 415 unsupported_media_type El body no es multipart/form-data. 422 upstream_rejected Hay 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)
Param Default Descripción page 1 Nú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_page 20 Í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
Campo Tipo Descripción id / internal_id / guia / serie / codigo / pickup_code — Identificadores del envío. Ver Identificadores. created_at / updated_at / shipping_date string Timestamps en formato Lima (YYYY-MM-DD HH:MM:SS y YYYY-MM-DD). status int Có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. payer string "sender" o "receiver" (mapeado de REMITENTE/DESTINATARIO). aereo / delivered / paid bool Booleanos que indican si el envío es aéreo, ya fue entregado y/o ya fue pagado. origin / destination object Agencia con ter_id, name, address, abbreviation, phone, ubigeo. sender / receiver object Persona 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[] array Detalle por bulto: quantity, weight, width, length, height, product_name, product_detail, product_price. warranty object { 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
Campo Tipo Descripción origin_terminal_id int ID de la agencia de origen (campo id de GET /v1/agencies) destiny_terminal_id int ID de la agencia de destino (campo id de GET /v1/agencies) product_id int ID del producto (Sobre, Caja XXS/XS/S/M/L, Otra Medida). Listalo con GET /v1/products. receiver object Destinatario. Siempre debe incluir document_type + document; sin id, también name (y last_name salvo RUC). Ver subcampos abajo. pickup_code string Clave 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_jurada string Tipo 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
Campo Tipo Descripción quantity int Default 1. shipment_type string Si 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. sender object Solo se usa con shipment_type: "empresarial". Identifica a la empresa remitente: { "document_type": "RUC", "document": "20xxxxxxxxx" }. En el flujo normal se ignora. payer string "sender" (default) o "receiver" (contra entrega). dimensions object Dimensiones 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". aereo bool Servicio 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. warranty object { "amount": "100.00" } activa garantía con monto declarado. collection_service object Cobro contra entrega. Objeto { cost, data } — ver subcampos abajo. Requiere cuenta bancaria registrada en Shalom Pro previamente. documentation array Guías adicionales: [{ "guide": "G123", "series": "S01" }]. contacto_doc string Documento de la persona que recoge. track bool Con 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:
- 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. - 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
}
Campo Tipo Descripción id int person_id de Shalom Pro. Si lo pasas, name y last_name dejan de ser obligatorios; document_type + document siguen siéndolo. document_type string "DNI" (8 dígitos), "RUC" (11 dígitos, empresas) o "CE" (carnet de extranjería). En mayúsculas: "dni" devuelve 400. document string Solo dígitos para DNI/RUC; alfanumérico ≥4 chars para CE. name string Nombre (persona) o razón social completa (empresa). Obligatorio si no pasas id. last_name string Primer apellido. Obligatorio para DNI/CE si no pasas id; no aplica a RUC. sur_name string Segundo apellido. No aplica a RUC. phone int Telé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. email string Opcional. address string Opcional.
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.
Campo Tipo Descripción cost number Monto a cobrar al destinatario (soles). Numérico, sin comillas. data.document string Documento del titular de la cuenta. data.name string Nombre del titular de la cuenta. data.bank string Banco: "BCP", "BBVA", "Interbank" o "Scotiabank". data.type_account string Tipo de cuenta: "Ahorro" o "Corriente". data.account_number string Número de cuenta. data.cci string Có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..."
}
}
HTTP code Descripción 400 bad_request Parámetros faltantes o inválidos 401 unauthorized API key inválida o ausente 401 key_revoked La API key fue revocada 401 key_expired La API key expiró; renuévala 401 shalom_auth_failed Credenciales 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. 403 key_disabled La API key está deshabilitada 404 not_found Recurso no existe 409 conflict Conflicto de estado 413 payload_too_large El 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 415 unsupported_media_type Content-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 422 upstream_rejected Shalom Pro rechazó la solicitud por una regla de negocio 429 rate_limited Rate limit excedido 429 quota_exceeded Tope de suscripciones de webhook activas alcanzado (solo POST /v1/tracking/subscriptions) 502 upstream_unavailable Shalom no respondió o devolvió algo inesperado 502 shalom_unavailable Shalom Pro no disponible o respondió algo inesperado (endpoints de Shalom Pro: sesiones, órdenes, productos, personas, tarifa). Reintenta. 503 shalom_login_unavailable No 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. 504 upstream_timeout Shalom no respondió dentro del tiempo de espera 500 internal Error 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 / min Ráfaga 60 15 120 30 240 60 600 150
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
Header Descripción X-RateLimit-Limit Requests por minuto de tu plan (60–600). X-RateLimit-Remaining Requests 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-Reset Epoch 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-After En 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.