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 / min
Formato
JSON
Base URL: https://api.shalom-api-peru.com
Quickstart
De cero a tu primera guía creada y rastreada, en cuatro llamadas. 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, tracking) pide credenciales de Shalom Pro. 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.
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). 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. Los endpoints que tocan la cuenta del cliente en Shalom Pro (tracking, órdenes, productos, personas, tarifas) exigen además sus credenciales. Agencias, ubicaciones y health solo necesitan 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. |
| 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 las credenciales fallan o Shalom rechaza la orden, la respuesta degrada al modo estado en vez de romper. 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: mi-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
}
}
Request — modo detallado (numero + codigo + credenciales) curl -H "X-API-Key: mi-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: mi-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": 0, "nombre": "", "abrebiatura": "", "ubigeo": 0, "departamento": "", "provincia": "", "distrito": "" },
"destino": { "id": 0, "nombre": "", "abrebiatura": "", "ubigeo": 0, "departamento": "", "provincia": "", "distrito": "" },
"remitente": { "documento": "", "nombre": "" },
"destinatario": { "documento": "", "nombre": "" },
"comprobante": { "cop_id": 0, "serie": "", "numero": "", "tipo": "", "tipo_pago": "", "estado_pago": "", "fecha": "", "hora": "" }
},
"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, pero sus bloques origen, destino, remitente, destinatario y comprobante llegan vacíos: desde julio de 2026 Shalom dejó de incluirlos en su respuesta de rastreo, y los devolvemos en cero para no romper el shape de quienes ya los leen. No construyas sobre esos campos. De order lo que sí llega: los identificadores, las fechas, contenido, monto, tipo_pago, estado_pago y los flags (entregado, reparto, aereo). El objeto status completo llega en ambos modos.
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. entregado Entregado al destinatario. reparto Salió a reparto a domicilio. null si la entrega es en agencia.
La fecha incluye la hora. Cada hito trae un solo campo fecha con formato "YYYY-MM-DD HH:MM:SS" — no hay un campo hora separado.
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 u ose_id — al menos uno) más un custom_id opcional. 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. Al menos uno por item, igual que en GET /v1/tracking.
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: mi-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" }
]
}'
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": false,
"status": { "entregado": { "fecha": "2026-04-16 11:40:45" } }
}
},
{
"custom_id": "pedido-003",
"numero": "00000000",
"ok": false,
"error": { "code": "not_found", "message": "orden no existe" }
}
],
"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 (order + status). 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 del sistema OSE (SUNAT) — 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 del sistema OSE (SUNAT) — 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" \
-H "X-Shalom-Email: cliente@empresa.com" \
-H "X-Shalom-Password: tu-password-de-shalom-pro" \
"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: mi-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 controlás la URL — evita que alguien apunte el webhook a un servidor ajeno.
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.
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
}
// ... acá procesás 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.
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. DELETE /v1/webhooks Deshabilita el webhook (deja de entregar). 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 primero. 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: mi-key" \
-H "Content-Type: application/json" \
-d '{ "numero": "80574902", "codigo": "CJTW" }'
201 Created — outcome: created | reactivated | already {
"subscription": {
"id": 812,
"numero": "80574902",
"codigo": "CJTW",
"status": "pending_resolve",
"created_at": "2026-07-30T12:00:00Z"
},
"outcome": "created"
}
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 o llegaste al cupo, la orden se crea igual.
Request curl -X POST https://api.shalom-api-peru.com/v1/orders \
-H "X-API-Key: mi-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 (por defecto 50, ajustable a tu plan). 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 50 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 con fecha, hora y descripción — lo mismo que el status de GET /v1/tracking.
POST a tu servidor — body {
"id": "evt_812_tracking.updated_destino",
"event": "tracking.updated",
"occurred_at": "2026-07-30T15:04:05Z",
"data": {
"numero": "80574902",
"ose_id": 84048736,
"status": "destino",
"previous_status": "transito",
"delivered": false,
"timeline": [
{ "milestone": "registrado", "fecha": "2026-07-28", "hora": "09:12", "descripcion": "Registrado en agencia origen", "completo": true },
{ "milestone": "transito", "fecha": "2026-07-29", "hora": "14:30", "descripcion": "En tránsito", "completo": true },
{ "milestone": "destino", "fecha": "2026-07-30", "hora": "10:05", "descripcion": "En agencia destino", "completo": false }
]
}
}
Headers
X-Shalom-Event: tracking.updated
X-Shalom-Event-Id: evt_812_tracking.updated_destino
X-Shalom-Signature: t=1782140645,v1=8f3a9c...hmac-sha256-en-hex...
Tipos de evento
event Cuándo tracking.updated El envío avanzó de hito. 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í).
Los hitos de status / timeline[].milestone son: registrado, origen, transito, demora, destino, entregado, reparto.
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).
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).
Node.js (Express) const crypto = require('crypto');
function verify(rawBody, header, secret) {
const sig = Object.fromEntries(header.split(',').map(kv => kv.split('=')));
const expected = crypto.createHmac('sha256', secret)
.update(sig.t + '.' + rawBody).digest('hex');
const firmaOk = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1));
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) => {
if (!verify(req.body, req.header('X-Shalom-Signature'), SECRET)) return res.status(400).end();
const event = JSON.parse(req.body);
// ... procesa event.data ... (deduplica por event.id)
res.status(200).end();
});
Reintentos e idempotencia
- Responde 2xx rápido. Si tu servidor responde algo distinto de 2xx (o no responde), reintentamos el mismo cambio en el próximo ciclo de poll. Procesa en background si tu lógica es lenta.
- 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 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). Las agencias habilitadas (unas 485) entran en una sola llamada con per_page=1000; con incluir_deshabilitadas=true es el catálogo completo (unas 546). El número exacto lo da total en la respuesta: Shalom abre y cierra locales, así que no lo fijes en tu código.
200 OK {
"page": 1,
"per_page": 100,
"total": 485,
"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
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. Viene vacío en torno al 7%. 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. Viene null en algunas. 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 Si la agencia opera para crear/despachar órdenes. El listado solo devuelve habilitadas por defecto: Shalom marca alrededor de 60 como no habilitadas y una orden contra ellas falla. No uses estado para esto (casi todas dicen "ATENDIENDO EN ESTE MOMENTO" aunque no operen). Para ver también las deshabilitadas pasa ?incluir_deshabilitadas=true. 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.
Param Tipo Descripción q string Texto libre departamento string Filtrar por departamento provincia string Filtrar por provincia 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". 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 Por defecto la búsqueda omite las agencias no habilitadas para órdenes. true las incluye también. per_page 100 Cuántas devolver (máx 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. La resolución de remitente y 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 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. 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 }
El token a revocar va en el header X-Shalom-Session, no en el path ni en el body. Si el header falta, devuelve 400.
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, o conseguir el id para reusarlo en envíos siguientes.
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,
"email": null,
"address": null
}
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",
"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 usan defaults mínimos. 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",
"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.
POST /v1/orders Crea una guía de envío. Devuelve guia, serie y codigo — los identificadores con los que después puedes trackear o borrar la orden.
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.
Todos los campos del body — requeridos y opcionales — están en Campos del body. Ojo con declaracion_jurada: es requerido ("docs", "ropa", "art" o "electro"); si lo omites, la API responde 400.
Errores propios de crear pedido
HTTP code Cuándo se da 409 conflict Ya hay una persona registrada con ese documento: pasa su id en vez de los datos. 422 upstream_rejected Regla de negocio de Shalom: cuenta sin servicio de cobranza, producto restringido para esa ruta, etc.
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 siempre 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 para que corrijas el Excel y reintentes.
422 — filas con errores {
"error": {
"code": "upstream_rejected",
"message": "el archivo tiene filas con errores de validación",
"request_id": "01KQ..."
},
"rows": [
{
"row": 3,
"errors": ["El destinatario no tiene documento válido"]
},
{
"row": 7,
"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). 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 o per_page 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.
La paginación y los filtros se aplican del lado de la API, no de Shalom: recortan la respuesta, pero no reducen la carga contra el upstream.
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). guia — Filtra por match exacto del campo guia. status — Filtra por match exacto del código numérico de status (ej. 1). 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.
Los filtros (guia, status, from, to) se combinan con AND y se aplican antes de paginar, así que meta.total refleja el conteo ya filtrado. Filtrar sin paginar devuelve el mismo shape de siempre (sin meta).
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": 1234567,
"document": "12345678",
"name": "JUAN",
"last_name": "PEREZ",
"sur_name": "GARCIA",
"full_name": "JUAN PEREZ GARCIA",
"phone": 999888777
},
"receiver": { "id": 7654321, "document": "87654321", "...": "..." },
"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 # Paginar (agrega "meta") + filtrar por estado y rango de fechas
curl "https://api.shalom-api-peru.com/v1/orders?page=1&per_page=20&status=1&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, "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. 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, email, address. 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.
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 404 si el ose_id no existe en la cuenta. 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 (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. declaracion_jurada string Tipo de contenido declarado — Shalom lo exige en toda orden. 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, paga al despachar) 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. Solo si origen y destino lo soportan. 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.
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:
- Solo
document_type + document: si el destinatario ya existe en la cuenta, se reusa. Si aún no existe, para registrarlo Shalom exige también name, phone y — solo para DNI/CE — last_name + sur_name. Opcionales: email, address. - Con
id: si ya conoces su id (de GET /v1/persons/search o de un envío anterior), pásalo junto con document_type + document para evitar la búsqueda.
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, los demás campos no son necesarios. document_type string "DNI" (8 dígitos), "RUC" (11 dígitos, empresas) o "CE" (carnet de extranjería). 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. last_name string Primer apellido. Obligatorio para DNI/CE, no aplica a RUC. sur_name string Segundo apellido. No aplica a RUC. phone int Teléfono numérico (sin código de país, ej. 999888777). 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
Estructura consistente en todos los errores:
{
"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 shalom_auth_failed Credenciales de Shalom Pro ausentes o rechazadas (endpoints de tracking y /v1/orders) 403 forbidden Sin permisos 404 not_found Recurso no existe 409 conflict Conflicto de estado (ej. persona duplicada en /v1/orders) 413 payload_too_large Archivo demasiado grande (solo /v1/orders/bulk) 415 unsupported_media_type Content-Type no soportado (ej. /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/webhooks/subscriptions) 502 upstream_unavailable Shalom Pro no respondió o devolvió algo inesperado 502 shalom_unavailable Shalom Pro no disponible (solo /v1/orders) 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 Pro no respondió dentro del tiempo de espera 500 internal Error interno
Rate Limits
Limite por defecto: 60 requests por minuto por API key.
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: 55
X-RateLimit-Reset: 1713200060
Headers
Header Descripción X-RateLimit-Limit Requests por minuto de tu plan. X-RateLimit-Remaining Requests disponibles ahora mismo (capacidad de ráfaga restante). Llega a 0 cuando estás topeado. X-RateLimit-Reset Epoch en segundos (UTC) en que el cupo vuelve a estar lleno. Retry-After Solo en el 429: segundos a esperar antes de reintentar (mínimo 1).
Health Checks
Públicos, sin autenticación.
GET /healthz Liveness. 200 si el servidor está vivo.
GET /readyz Readiness. 200 cuando está listo para recibir tráfico.