EnruTek API 1.0.0
Creá y rastreá guías en EnruTek desde cualquier sistema.
Dirección base: https://api.enrutek.com/v1. Contrato: https://api.enrutek.com/openapi.json y https://api.enrutek.com/openapi.yaml.
Empezá en 5 pasos
Todo va a https://api.enrutek.com/v1, con JSON y con tu llave en la cabecera Authorization.
1. Pedí tu llave. La crea tu operador logístico en EnruTek y se muestra una sola vez. Guardala en el servidor de tu sistema, nunca en una página web, una app ni un repositorio.
2. Mirá qué podés usar. /catalog devuelve tus tipos de carga con su precio y tus sucursales. El code de cada tipo de carga es lo que va en cargo_type.
curl https://api.enrutek.com/v1/catalog \
-H "Authorization: Bearer $ENRUTEK_KEY"
3. Creá una guía. Responde 201 con el número de guía y el total calculado con tu tarifa.
curl -X POST https://api.enrutek.com/v1/shipments \
-H "Authorization: Bearer $ENRUTEK_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: factura-379671" \
-d '{
"external_reference": "379671",
"recipient": { "name": "Farmacia Plaza Flamingo", "phone": "+50626545524" },
"destination": {
"country": "CR",
"admin_area_1": "Guanacaste",
"admin_area_2": "Santa Cruz",
"address_line": "Playa Potrero, 200 metros sur del Hotel Bahía Esmeralda"
},
"packages": [{ "cargo_type": "seco", "quantity": 2 }]
}'
4. Consultala. Trae el estado, el historial y, una vez entregada, la prueba de entrega. Es para cuando alguien la necesita ver, no para preguntar cada rato si cambió: para eso está el paso 5.
curl https://api.enrutek.com/v1/shipments/TF-261024ZXWPH2S \
-H "Authorization: Bearer $ENRUTEK_KEY"
5. Enterate de los cambios. Suscribí una URL de tu sistema con POST /webhooks para no tener que estar preguntando.
Probar en Sandbox
Antes de conectar tu sistema a la operación real, probá en Sandbox, una empresa de pruebas dentro de EnruTek. Lo que creés ahí no le llega a ningún operador ni sale a la calle.
- Pedí acceso a tu operador logístico o a Alfa Tek. Te dan una llave de prueba para un cliente de pruebas propio: nadie más ve tus guías.
- Es la misma dirección y el mismo contrato:
https://api.enrutek.com/v1, con los mismos campos, errores y límites. Solo cambia la llave. - Las guías nacen con el prefijo
SB-, así se distinguen de las reales. - Nadie las recolecta ni las entrega: se quedan en
createdsalvo que las canceles. Para probar tu receptor de webhooks usáPOST /webhooks/{id}/test. - Para pasar a producción, tu operador te crea la llave real y la cambiás en tu sistema. No hay que cambiar nada más.
Rigen las mismas reglas de uso responsable que en producción.
Uso responsable
El API comparte la base de datos con la operación de bodega y de los choferes. Por eso:
- Crear guías no tiene horario. Mandalas en el momento en que nacen en tu sistema; si son varias, en un lote con
/shipments/batch. - Para saber el estado, usá webhooks. EnruTek te avisa cada cambio y no tenés que preguntar.
- Si no podés recibir webhooks, consultá a lo sumo una vez por hora, con una sola llamada a
GET /shipments?updated_since=<fecha>que trae todo lo que cambió desde la última vez. - No consultés guía por guía en un ciclo, ni cada pocos segundos o minutos. Consultar una guía suelta está bien cuando una persona lo pide.
El límite técnico por llave (RateLimit-Limit) existe para absorber lotes y reintentos; no es una invitación a consultar todo el tiempo. EnruTek revisa el registro de llamadas de cada llave: un uso que afecte la operación puede llevar a que se baje el límite de la llave o se revoque, con aviso previo salvo que el daño sea inmediato.
Crear varias guías de una vez
Si un botón o un proceso de tu sistema manda varias guías juntas, usá POST /shipments/batch: una sola llamada, hasta 500 guías y una sola Idempotency-Key para todo el lote.
curl -X POST https://api.enrutek.com/v1/shipments/batch \
-H "Authorization: Bearer $ENRUTEK_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: despacho-2026-11-10-tarde" \
-d '{ "shipments": [ { "external_reference": "379671", "...": "..." },
{ "external_reference": "379672", "...": "..." } ] }'
Cada guía se procesa por aparte: una inválida no detiene a las demás.
El lote responde 200 aunque alguna guía haya fallado. Revisá siempre failed y recorré results, que viene en el mismo orden en que mandaste las guías:
{
"created": 1,
"failed": 1,
"results": [
{ "index": 0, "ok": true, "shipment": { "tracking_number": "TF-261024ZXWPH2S" } },
{ "index": 1, "ok": false, "error": { "code": "unknown_cargo_type", "message": "…", "field": "packages.0.cargo_type" } }
]
}
Si failed es mayor que cero, corregí esas guías y mandá solamente esas en un lote nuevo, con otra Idempotency-Key.
Reintentar sin duplicar
Las redes fallan. Hay dos protecciones y conviene usar las dos:
| Situación | Qué mandar | Qué pasa |
|---|---|---|
| Se cortó la conexión y no sabés si llegó | Lo mismo, con la misma Idempotency-Key |
Recibís el resultado original y no se crea nada nuevo |
| Alguien vuelve a tocar el botón más tarde | Otra Idempotency-Key, la misma external_reference |
409 duplicate_reference, con el número de la guía que ya existe en details |
Misma Idempotency-Key, cuerpo distinto |
No lo hagás | 409 idempotency_conflict |
Las Idempotency-Key se guardan 24 horas. La external_reference es única por cliente entre las guías activas: al cancelar una guía, su referencia se puede volver a usar.
Recibir webhooks
EnruTek le manda un POST a tu URL cada vez que una guía cambia. Para probar tu receptor sin mover ninguna guía, usá POST /webhooks/{id}/test: el evento llega con "test": true.
Tu receptor tiene que:
- Validar la firma con el cuerpo crudo, tal como llegó, antes de convertirlo a JSON.
- Rechazar eventos con más de 5 minutos.
- Responder cualquier
2xxen menos de 10 segundos y hacer el trabajo pesado después. - Descartar repetidos con
event.id: un evento puede llegar más de una vez. - Ordenar con
occurred_at, porque pueden llegar en desorden.
Si tu sistema no responde 2xx, EnruTek reintenta con espera creciente hasta por 24 horas. Una suscripción que lleva 24 horas fallando se apaga sola.
La cabecera EnruTek-Signature trae t=<segundos unix>,v1=<firma>. La firma es HMAC-SHA256 en hexadecimal de <t>.<cuerpo crudo>, con el secreto whsec_… que recibiste al crear la suscripción.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto"
export function firmaValida(cuerpoCrudo, cabecera, secreto) {
const partes = Object.fromEntries(cabecera.split(",").map((p) => p.split("=")))
if (!partes.t || !partes.v1) return false
if (Math.abs(Date.now() / 1000 - Number(partes.t)) > 300) return false
const esperada = createHmac("sha256", secreto).update(`${partes.t}.${cuerpoCrudo}`).digest()
const recibida = Buffer.from(partes.v1, "hex")
return recibida.length === esperada.length && timingSafeEqual(recibida, esperada)
}
Python
import hashlib, hmac, time
def firma_valida(cuerpo_crudo: bytes, cabecera: str, secreto: str) -> bool:
partes = dict(p.split("=", 1) for p in cabecera.split(","))
if "t" not in partes or "v1" not in partes:
return False
if abs(time.time() - int(partes["t"])) > 300:
return False
mensaje = partes["t"].encode() + b"." + cuerpo_crudo
esperada = hmac.new(secreto.encode(), mensaje, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperada, partes["v1"])
PHP
function firma_valida(string $cuerpoCrudo, string $cabecera, string $secreto): bool {
parse_str(str_replace(',', '&', $cabecera), $partes);
if (empty($partes['t']) || empty($partes['v1'])) return false;
if (abs(time() - (int) $partes['t']) > 300) return false;
$esperada = hash_hmac('sha256', $partes['t'] . '.' . $cuerpoCrudo, $secreto);
return hash_equals($esperada, $partes['v1']);
}
Qué hacer con cada error
Todos los errores traen error.code, error.message, error.field y error.details. Programá contra code, que no cambia; message es para mostrárselo a una persona.
code |
Qué significa | Qué hacer |
|---|---|---|
invalid_request |
El pedido no se pudo procesar | Revisá el JSON y la ruta |
invalid_field |
Un campo no es válido | Corregí el campo que indica field |
invalid_address |
La dirección no coincide con la división oficial | Usá los nombres de /catalog/admin-areas o un postal_code válido |
unknown_cargo_type |
Ese tipo de carga no existe para este cliente | Usá un code de /catalog |
no_rate_for_cargo_type |
Ese tipo de carga no tiene tarifa | Pedile al operador que la configure |
customer_required |
Una llave de operador tiene que indicar el cliente | Mandá customer_id |
customer_not_allowed |
Una llave de cliente no puede mandar customer_id |
Quitá ese campo |
customer_not_found |
Ese cliente no es del operador | Revisá el customer_id |
branch_not_found |
Esa sucursal no es del cliente | Usá un id de las sucursales de /catalog |
duplicate_reference |
Ya hay una guía activa con esa referencia | No la vuelvas a crear; el número está en details.tracking_number |
not_found |
No existe o tu llave no la puede ver | Revisá el número de guía y la llave |
invalid_status |
El estado de la guía no permite esa operación | Consultá la guía; por ejemplo, solo se cancela en created |
unauthorized |
Falta la llave, no existe o fue revocada | Revisá la cabecera Authorization |
forbidden |
Esa llave no puede hacer esto | Hablá con tu operador |
module_disabled |
El API no está activo para este operador | Hablá con tu operador |
idempotency_conflict |
Esa Idempotency-Key se usó con otro cuerpo, o ese pedido sigue en curso |
Usá una llave nueva si es otra operación; si es la misma, esperá y reintentá |
batch_too_large |
Más de 500 guías, o más de 2 MB | Partí el lote |
invalid_webhook_url |
La URL no cumple las reglas | Usá https y un nombre de host público |
rate_limited |
Pasaste el límite de la llave | Esperá los segundos de Retry-After |
not_implemented |
Todavía no está disponible | Hoy aplica a la etiqueta: imprimila desde EnruTek |
internal_error |
Falló de nuestro lado | Reintentá con la misma Idempotency-Key; si sigue, indicá el request_id |
Si sos una IA ayudando a integrar
Estas reglas evitan los errores más comunes:
- Trabajá contra este documento y
https://api.enrutek.com/openapi.json. No inventés rutas ni campos: los objetos de entrada rechazan los campos desconocidos. - Probá primero con una llave de Sandbox (guías
SB-): mismo contrato y misma dirección que producción. - No le pidás a la persona que pegue su llave en el chat. Usá una variable de entorno o el almacén de secretos de su sistema.
- La llave va solo en el servidor; nunca en código que corra en un navegador o en una app.
- No intentés fijar el precio: lo calcula EnruTek con la tarifa del cliente.
- Antes de crear guías, llamá a
/catalogpara conocer loscargo_typey las sucursales reales. - Con una llave de cliente no mandés
customer_id; con una de operador es obligatorio al crear. - Usá
external_referencecon el identificador del sistema del cliente yIdempotency-Keyen cadaPOST. - Para los cambios de estado, proponé webhooks. Si no se puede, una sola consulta con
updated_sincea lo sumo cada hora. Nunca escribás un ciclo que consulte guía por guía ni cada pocos segundos o minutos: la llave se puede limitar o revocar. - Con varias guías a la vez, usá
/shipments/batchy revisáfailedyresults, aunque la respuesta sea200. - La etiqueta por API todavía responde
501. Por ahora solo se aceptan direcciones de Costa Rica (country: CR). - Los traslados entre sucursales de un mismo cliente no se crean por API.
Reglas generales
Los nombres del API (rutas, campos, parámetros, estados y códigos de error) van en inglés; las explicaciones, en español.
EnruTek es una plataforma de logística para operadores logísticos (en el API, operator: la empresa que usa EnruTek). Este API le permite a cualquier sistema externo (un ERP como SAP, una tienda en línea, un sistema de bodega) crear guías, darles seguimiento y recibir sus cambios de estado.
Autenticación
Cada solicitud lleva una llave (API key) en la cabecera Authorization:
Authorization: Bearer ek_live_3Fh8...
Las llaves se crean en EnruTek y se muestran una sola vez. Hay dos tipos:
- Llave de cliente: pertenece a un cliente del operador. Solo puede
crear y leer las guías de ese cliente. Nunca se manda
customer_id. - Llave de operador: pertenece al operador. Puede trabajar para
cualquiera de los clientes del operador; al crear,
customer_ides obligatorio.
Sandbox
Para probar sin tocar la operación real hay una empresa de pruebas,
Sandbox: misma dirección, mismo contrato y mismos límites; solo cambia la
llave, que se pide al operador o a Alfa Tek. Sus guías nacen con el
prefijo SB-, nadie las recolecta ni las entrega, y para probar los
webhooks está POST /webhooks/{id}/test.
Convenciones
- JSON de entrada y de salida, en UTF-8. Nombres de campos en
snake_case. - Fechas y horas en ISO 8601 con desfase (
2026-10-24T14:05:00-06:00). - Teléfonos en E.164 (
+50688887777). - Montos como
{ "amount": "3000.00", "currency": "CRC" }(ISO 4217; el monto es un texto decimal para evitar redondeos). - Las direcciones no dependen del país:
country(ISO 3166-1 alfa-2) y hasta tres niveles administrativos. En Costa Rica:admin_area_1= provincia,admin_area_2= cantón,admin_area_3= distrito. - El precio siempre lo calcula EnruTek con la tarifa del cliente; no se puede fijar por el API.
Idempotencia
Mandá una cabecera Idempotency-Key (cualquier texto único de hasta 100
caracteres) en las solicitudes POST. Si reintentás con la misma llave,
recibís el resultado original en vez de crear una segunda guía. Las llaves
se guardan 24 horas. Además, external_reference es única por cliente
entre las guías activas: una segunda guía con la misma referencia se
rechaza con duplicate_reference y el número de guía que ya existe.
Errores
Los errores traen un code estable, para máquinas (en inglés), y un
message para personas. El mensaje viene en español por defecto; mandá
Accept-Language: en para recibirlo en inglés.
Límites de uso
Cada llave tiene un máximo de solicitudes por minuto (rate limit). Cada
respuesta trae RateLimit-Limit, RateLimit-Remaining y
RateLimit-Reset. Si se pasa del límite, el API responde 429 con
Retry-After.
Uso responsable
El API comparte la base con la operación. Crear guías no tiene horario
(varias juntas, en /shipments/batch). Para los cambios de estado usá
webhooks; si no podés recibirlos, consultá a lo sumo una vez por hora con
una sola llamada a GET /shipments?updated_since=…. No consultés guía
por guía en un ciclo ni cada pocos segundos o minutos. El límite por
llave existe para lotes y reintentos, no para consultar todo el tiempo:
un uso que afecte la operación puede llevar a que se baje el límite de la
llave o se revoque, con aviso previo salvo que el daño sea inmediato.
Webhooks
EnruTek puede avisarle a tu sistema cuando una guía cambia de estado (por
webhook). Cada envío es un POST con un WebhookEvent en JSON y la
cabecera EnruTek-Signature: t=<segundos unix>,v1=<HMAC-SHA256 en hex>,
calculada sobre <t>.<cuerpo tal cual> con el secreto de la suscripción.
Rechazá los eventos de hace más de 5 minutos. Respondé cualquier 2xx en
menos de 10 segundos; si no, EnruTek reintenta con espera exponencial
durante hasta 24 horas. Un evento puede llegar más de una vez y en
desorden: usá event.id para descartar repetidos y occurred_at para
ordenarlos.
Referencia de rutas
Guías
Crear, consultar y cancelar guías.
GET /shipments
Listar guías.
Las más recientes primero, paginadas por cursor. Para sincronizar, usá
updated_since con el updated_at de la última guía que viste.
Sin webhooks, sincronizá a lo sumo una vez por hora (ver "Uso
responsable").
Parámetros:
| Nombre | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
Accept-Language |
header | es, en |
no | Idioma de los mensajes de error y de los nombres de estado. es (por defecto) o en. |
status |
query | string | no | Uno o varios estados, separados por coma. |
external_reference |
query | string | no | Tu referencia de la guía (external_reference). |
customer_id |
query | string (uuid) | no | Solo con llaves de operador. |
created_since |
query | string (date-time) | no | Solo guías creadas desde esta fecha y hora. |
updated_since |
query | string (date-time) | no | Solo guías que cambiaron desde esta fecha y hora. |
limit |
query | integer | no | Cantidad de guías por página. |
cursor |
query | string | no | El next_cursor de la página anterior. |
Respuestas:
200: Una página de guías (sinevents; pedí la guía individual para ver su historial).400: El cuerpo o un parámetro no es válido (invalid_request,invalid_field).401: Falta la llave, no existe o fue revocada (unauthorized).429: Se pasó del límite de uso (rate_limited). VerRetry-After.
POST /shipments
Crear una guía.
Crea una guía. Nace en created (un chofer la recoge en la sucursal de
origen). La respuesta trae el número de guía, el precio calculado con
la tarifa y el enlace a la etiqueta 100×100.
Parámetros:
| Nombre | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
Idempotency-Key |
header | string | no | Texto único por operación; un reintento con la misma llave devuelve el resultado original (24 horas). |
Accept-Language |
header | es, en |
no | Idioma de los mensajes de error y de los nombres de estado. es (por defecto) o en. |
Cuerpo (JSON):
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
customer_id |
string (uuid) | no | Obligatorio con llaves de operador; no se permite con llaves de cliente. |
origin_branch_id |
string (uuid) | no | Sucursal del cliente donde se recoge. Por defecto, la sucursal principal. |
external_reference |
string | no | Tu identificador de esta guía (factura, número de entrega). Única por cliente entre las guías activas. Si el cliente escanea sus propias etiquetas, es el código de barras. |
recipient |
object | sí | |
recipient.name |
string | sí | Nombre del destinatario. |
recipient.phone |
string | no | E.164. |
recipient.email |
string (email) | no | Para avisarle de la entrega al destinatario. |
destination |
object | sí | Los niveles administrativos se comparan sin tildes ni mayúsculas contra la lista oficial (/catalog/admin-areas). En Costa Rica, admin_area_1 y admin_area_2 son obligatorios salvo que postal_code los resuelva; admin_area_3 es opcional. |
destination.country |
string | sí | ISO 3166-1 alfa-2. Por ahora solo CR. |
destination.admin_area_1 |
string | no | CR: provincia. |
destination.admin_area_2 |
string | no | CR: cantón. |
destination.admin_area_3 |
string | no | CR: distrito. |
destination.postal_code |
string | no | CR: 5 dígitos; si viene y es válido, llena los niveles administrativos. |
destination.address_line |
string | sí | Otras señas. |
destination.lat |
number | no | Latitud del punto exacto (opcional). |
destination.lng |
number | no | Longitud del punto exacto (opcional). |
notes |
string | no | Indicaciones para el chofer (horario, por quién preguntar). El chofer las ve. |
packages |
lista de object | sí | Los bultos, agrupados por tipo de carga. |
packages[].cargo_type |
string | sí | Un code de /catalog (p. ej. seco, frio). |
packages[].quantity |
integer | sí | Cantidad de bultos de este tipo de carga. |
Ejemplo:
{
"external_reference": "379671",
"recipient": {
"name": "Farmacia Plaza Flamingo",
"phone": "+50626545524"
},
"destination": {
"country": "CR",
"admin_area_1": "Guanacaste",
"admin_area_2": "Santa Cruz",
"admin_area_3": "Tempate",
"postal_code": "50304",
"address_line": "Playa Potrero, 200 metros sur del Hotel Bahía Esmeralda"
},
"notes": "Entregar en horario de oficina",
"packages": [
{
"cargo_type": "seco",
"quantity": 1
}
]
}
Respuestas:
200: MismaIdempotency-Keyque una solicitud anterior; se devuelve la guía original.201: Creada.400: El cuerpo o un parámetro no es válido (invalid_request,invalid_field).401: Falta la llave, no existe o fue revocada (unauthorized).403: La llave no puede hacer esto (forbidden,customer_not_allowed,module_disabled).409:duplicate_reference,invalid_statusoidempotency_conflict(misma llave, cuerpo distinto).422: JSON válido que EnruTek no puede usar (invalid_address,unknown_cargo_type,no_rate_for_cargo_type,branch_not_found,invalid_webhook_url).429: Se pasó del límite de uso (rate_limited). VerRetry-After.
POST /shipments/batch
Crear hasta 500 guías.
Cada elemento se procesa por aparte: uno inválido no detiene a los
demás. La respuesta trae un resultado por elemento, en el mismo orden.
Usá una Idempotency-Key para todo el lote; al reintentar recibís los
mismos resultados.
Parámetros:
| Nombre | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
Idempotency-Key |
header | string | no | Texto único por operación; un reintento con la misma llave devuelve el resultado original (24 horas). |
Accept-Language |
header | es, en |
no | Idioma de los mensajes de error y de los nombres de estado. es (por defecto) o en. |
Cuerpo (JSON):
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
shipments |
lista de object | sí | |
shipments[].customer_id |
string (uuid) | no | Obligatorio con llaves de operador; no se permite con llaves de cliente. |
shipments[].origin_branch_id |
string (uuid) | no | Sucursal del cliente donde se recoge. Por defecto, la sucursal principal. |
shipments[].external_reference |
string | no | Tu identificador de esta guía (factura, número de entrega). Única por cliente entre las guías activas. Si el cliente escanea sus propias etiquetas, es el código de barras. |
shipments[].recipient |
object | sí | |
shipments[].recipient.name |
string | sí | Nombre del destinatario. |
shipments[].recipient.phone |
string | no | E.164. |
shipments[].recipient.email |
string (email) | no | Para avisarle de la entrega al destinatario. |
shipments[].destination |
object | sí | Los niveles administrativos se comparan sin tildes ni mayúsculas contra la lista oficial (/catalog/admin-areas). En Costa Rica, admin_area_1 y admin_area_2 son obligatorios salvo que postal_code los resuelva; admin_area_3 es opcional. |
shipments[].destination.country |
string | sí | ISO 3166-1 alfa-2. Por ahora solo CR. |
shipments[].destination.admin_area_1 |
string | no | CR: provincia. |
shipments[].destination.admin_area_2 |
string | no | CR: cantón. |
shipments[].destination.admin_area_3 |
string | no | CR: distrito. |
shipments[].destination.postal_code |
string | no | CR: 5 dígitos; si viene y es válido, llena los niveles administrativos. |
shipments[].destination.address_line |
string | sí | Otras señas. |
shipments[].destination.lat |
number | no | Latitud del punto exacto (opcional). |
shipments[].destination.lng |
number | no | Longitud del punto exacto (opcional). |
shipments[].notes |
string | no | Indicaciones para el chofer (horario, por quién preguntar). El chofer las ve. |
shipments[].packages |
lista de object | sí | Los bultos, agrupados por tipo de carga. |
shipments[].packages[].cargo_type |
string | sí | Un code de /catalog (p. ej. seco, frio). |
shipments[].packages[].quantity |
integer | sí | Cantidad de bultos de este tipo de carga. |
Respuestas:
200: Un resultado por elemento.400: El cuerpo o un parámetro no es válido (invalid_request,invalid_field).401: Falta la llave, no existe o fue revocada (unauthorized).413: Más de 500 elementos.429: Se pasó del límite de uso (rate_limited). VerRetry-After.
GET /shipments/{tracking_number}
Consultar una guía con su historial.
Parámetros:
| Nombre | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
tracking_number |
path | string | sí | El número de guía de EnruTek, p. ej. TF-261024ZXWPH2S. |
Accept-Language |
header | es, en |
no | Idioma de los mensajes de error y de los nombres de estado. es (por defecto) o en. |
Respuestas:
200: La guía, coneventsy, una vez entregada,proof_of_delivery(prueba de entrega).401: Falta la llave, no existe o fue revocada (unauthorized).404: No existe, o esta llave no lo puede ver (not_found).429: Se pasó del límite de uso (rate_limited). VerRetry-After.
POST /shipments/{tracking_number}/cancel
Cancelar una guía.
Solo mientras esté en created (nadie la ha recogido).
Parámetros:
| Nombre | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
tracking_number |
path | string | sí | El número de guía de EnruTek, p. ej. TF-261024ZXWPH2S. |
Idempotency-Key |
header | string | no | Texto único por operación; un reintento con la misma llave devuelve el resultado original (24 horas). |
Accept-Language |
header | es, en |
no | Idioma de los mensajes de error y de los nombres de estado. es (por defecto) o en. |
Cuerpo (JSON):
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
reason |
string | no | Motivo de la cancelación. |
Respuestas:
200: Cancelada (o ya estaba cancelada).401: Falta la llave, no existe o fue revocada (unauthorized).404: No existe, o esta llave no lo puede ver (not_found).409:duplicate_reference,invalid_statusoidempotency_conflict(misma llave, cuerpo distinto).429: Se pasó del límite de uso (rate_limited). VerRetry-After.
GET /shipments/{tracking_number}/label
Obtener la etiqueta 100×100.
Una página por bulto. Por ahora en PDF; más adelante, ZPL para
impresoras térmicas. Todavía no está disponible: responde
501 not_implemented (después de revisar que la llave puede ver la
guía). Mientras tanto, imprimí las etiquetas desde EnruTek.
Parámetros:
| Nombre | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
tracking_number |
path | string | sí | El número de guía de EnruTek, p. ej. TF-261024ZXWPH2S. |
format |
query | pdf |
no | Formato de la etiqueta. |
Respuestas:
200: La etiqueta.401: Falta la llave, no existe o fue revocada (unauthorized).404: No existe, o esta llave no lo puede ver (not_found).501: Todavía no está disponible (not_implemented).
Catálogo
Tipos de carga, sucursales y división territorial que puede usar una guía.
GET /catalog
Lo que puede usar una guía.
Los tipos de carga con el precio del cliente y las sucursales del
cliente (orígenes). Con llaves de operador, customer_id es
obligatorio.
Parámetros:
| Nombre | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
customer_id |
query | string (uuid) | no | Solo con llaves de operador (y ahí es obligatorio). |
Accept-Language |
header | es, en |
no | Idioma de los mensajes de error y de los nombres de estado. es (por defecto) o en. |
Respuestas:
200: El catálogo.401: Falta la llave, no existe o fue revocada (unauthorized).
GET /catalog/admin-areas
División territorial de un país.
Los valores válidos de admin_area_1..3, en forma de árbol.
Parámetros:
| Nombre | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
country |
query | string | sí | País, en ISO 3166-1 alfa-2. |
Respuestas:
200: El árbol.
Webhooks
Suscripciones para recibir los cambios de estado de las guías.
GET /webhooks
Listar las suscripciones de webhook de esta llave.
Respuestas:
200: Las suscripciones (sin el secreto).401: Falta la llave, no existe o fue revocada (unauthorized).
POST /webhooks
Suscribir una URL a los eventos de las guías.
El secret se devuelve solo en esta respuesta.
Alcance. Una llave de cliente recibe solo los eventos de sus propias
guías. Una llave de operador puede indicar customer_id para recibir
solo los eventos de ese cliente; sin él, recibe los eventos de todas
las guías del operador. customer_id tiene que ser un cliente del
operador de la llave (si no, 422 customer_not_found), y si una llave
de cliente lo manda se rechaza (403 customer_not_allowed): nunca
amplía lo que una llave puede ver. El alcance no se puede cambiar
después.
Reglas de la URL (se revisan al crear y otra vez en cada envío):
solo https, sin usuario ni contraseña en la URL, un nombre de host
(no una IP literal) que resuelva solo a direcciones públicas (se
rechazan loopback, privadas RFC 1918, link-local 169.254.0.0/16, CGNAT
100.64.0.0/10, loopback/ULA/link-local de IPv6 y las direcciones de
metadatos de la nube). No se siguen redirecciones. Una URL que no
cumple estas reglas se rechaza con invalid_webhook_url.
Cuerpo (JSON):
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
url |
string (uri) | sí | Ver las reglas de la URL arriba. |
customer_id |
string (uuid) | no | Solo con llaves de operador: limita la suscripción a un cliente. |
events |
lista de shipment.created, shipment.picked_up, shipment.received_at_cedi, shipment.assigned, shipment.out_for_delivery, shipment.kept_on_route, shipment.back_to_cedi, shipment.delivered, shipment.rejected, shipment.cancelled |
sí | |
description |
string | no |
Respuestas:
201: Creada.400: El cuerpo o un parámetro no es válido (invalid_request,invalid_field).401: Falta la llave, no existe o fue revocada (unauthorized).403: La llave no puede hacer esto (forbidden,customer_not_allowed,module_disabled).422: JSON válido que EnruTek no puede usar (invalid_address,unknown_cargo_type,no_rate_for_cargo_type,branch_not_found,invalid_webhook_url).
DELETE /webhooks/{id}
Detener una suscripción.
Parámetros:
| Nombre | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id |
path | string (uuid) | sí |
Respuestas:
204: Detenida.401: Falta la llave, no existe o fue revocada (unauthorized).404: No existe, o esta llave no lo puede ver (not_found).
POST /webhooks/{id}/test
Enviar un evento de prueba.
Parámetros:
| Nombre | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id |
path | string (uuid) | sí |
Respuestas:
202: En cola.401: Falta la llave, no existe o fue revocada (unauthorized).404: No existe, o esta llave no lo puede ver (not_found).
Evento de webhook
Lo que EnruTek le manda a la URL suscrita.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id |
string (uuid) | sí | Único por evento; usalo para descartar repetidos. |
type |
shipment.created, shipment.picked_up, shipment.received_at_cedi, shipment.assigned, shipment.out_for_delivery, shipment.kept_on_route, shipment.back_to_cedi, shipment.delivered, shipment.rejected, shipment.cancelled |
sí | |
occurred_at |
string (date-time) | sí | |
shipment |
object | sí | La guía como estaba en el momento del evento. proof_of_delivery viene sin photo_url ni signature_url (los enlaces de corta vida no deben quedar en bitácoras durante los reintentos): pedí la guía para obtenerlos. |
shipment.id |
string (uuid) | sí | |
shipment.tracking_number |
string | sí | |
shipment.tracking_url |
string (uri) | no | Página pública de rastreo. |
shipment.label_url |
string (uri) | no | /shipments/{tracking_number}/label. |
shipment.status |
created, at_reception, picked_up, received_at_cedi, loaded, out_for_delivery, kept_on_route, back_to_cedi, delivered, rejected, cancelled |
sí | Dónde está físicamente el bulto. Finales: delivered, rejected, cancelled. |
shipment.status_label |
string | sí | Nombre del estado para personas, en el idioma pedido; p. ej. "En CEDI". |
shipment.external_reference |
string | no | |
shipment.customer |
object | sí | |
shipment.customer.id |
string (uuid) | sí | |
shipment.customer.name |
string | sí | |
shipment.origin |
object | sí | |
shipment.origin.branch_id |
string (uuid) | no | |
shipment.origin.name |
string | no | |
shipment.origin.address |
object | no | Los niveles administrativos se comparan sin tildes ni mayúsculas contra la lista oficial (/catalog/admin-areas). En Costa Rica, admin_area_1 y admin_area_2 son obligatorios salvo que postal_code los resuelva; admin_area_3 es opcional. |
shipment.origin.address.country |
string | sí | ISO 3166-1 alfa-2. Por ahora solo CR. |
shipment.origin.address.admin_area_1 |
string | no | CR: provincia. |
shipment.origin.address.admin_area_2 |
string | no | CR: cantón. |
shipment.origin.address.admin_area_3 |
string | no | CR: distrito. |
shipment.origin.address.postal_code |
string | no | CR: 5 dígitos; si viene y es válido, llena los niveles administrativos. |
shipment.origin.address.address_line |
string | sí | Otras señas. |
shipment.origin.address.lat |
number | no | Latitud del punto exacto (opcional). |
shipment.origin.address.lng |
number | no | Longitud del punto exacto (opcional). |
shipment.recipient |
object | sí | |
shipment.recipient.name |
string | sí | Nombre del destinatario. |
shipment.recipient.phone |
string | no | E.164. |
shipment.recipient.email |
string (email) | no | Para avisarle de la entrega al destinatario. |
shipment.destination |
object | sí | Los niveles administrativos se comparan sin tildes ni mayúsculas contra la lista oficial (/catalog/admin-areas). En Costa Rica, admin_area_1 y admin_area_2 son obligatorios salvo que postal_code los resuelva; admin_area_3 es opcional. |
shipment.destination.country |
string | sí | ISO 3166-1 alfa-2. Por ahora solo CR. |
shipment.destination.admin_area_1 |
string | no | CR: provincia. |
shipment.destination.admin_area_2 |
string | no | CR: cantón. |
shipment.destination.admin_area_3 |
string | no | CR: distrito. |
shipment.destination.postal_code |
string | no | CR: 5 dígitos; si viene y es válido, llena los niveles administrativos. |
shipment.destination.address_line |
string | sí | Otras señas. |
shipment.destination.lat |
number | no | Latitud del punto exacto (opcional). |
shipment.destination.lng |
number | no | Longitud del punto exacto (opcional). |
shipment.notes |
string | no | |
shipment.packages |
lista de object | sí | |
shipment.packages[].cargo_type |
string | sí | |
shipment.packages[].cargo_type_name |
string | no | |
shipment.packages[].quantity |
integer | sí | |
shipment.packages[].unit_price |
object | sí | |
shipment.packages[].unit_price.amount |
string | sí | |
shipment.packages[].unit_price.currency |
string | sí | |
shipment.total |
object | sí | Total con IVA, tal como se va a facturar. |
shipment.total.amount |
string | sí | |
shipment.total.currency |
string | sí | |
shipment.scheduled |
object | no | El despacho al que está asignada, si lo hay. |
shipment.scheduled.date |
string (date) | no | |
shipment.scheduled.route_code |
string | no | |
shipment.proof_of_delivery |
object | no | |
shipment.proof_of_delivery.delivered_at |
string (date-time) | no | |
shipment.proof_of_delivery.received_by |
string | no | |
shipment.proof_of_delivery.lat |
number | no | |
shipment.proof_of_delivery.lng |
number | no | |
shipment.events |
lista de object | no | Historial, de lo más viejo a lo más nuevo. Solo en GET /shipments/{tracking_number} y en los webhooks. |
shipment.events[].type |
created, edited, picked_up, at_reception, received_at_cedi, assigned, unassigned, loaded, out_for_delivery, kept_on_route, back_to_cedi, delivered, rejected, cancelled |
sí | |
shipment.events[].label |
string | no | |
shipment.events[].occurred_at |
string (date-time) | sí | |
shipment.events[].facility |
string | no | Nombre del CEDI (centro de distribución), p. ej. "GAM". |
shipment.events[].comment |
string | no | |
shipment.created_at |
string (date-time) | sí | |
shipment.updated_at |
string (date-time) | sí |