Webhooks

Cambios de estado

Beartrack le hace un POST a tu servidor cada vez que uno de tus envíos cambia de estado. No hay que consultar nada: el evento llega solo.

Cómo funciona

Nos das una URL. Cada vez que un envío tuyo se mueve, mandamos un POST con un sobre JSON firmado. Tú respondes 2xx y listo; cualquier otra cosa la reintentamos.

Sólo recibes los envíos que te corresponden: si eres un comercio, los tuyos; si eres un operador logístico, los de tu operación. Puedes además acotar por tipo de pedido o por estado.

Responde rápido y procesa después

Guarda el evento y devuelve 2xx de inmediato. Si tardas más de 10 segundos cortamos la conexión y lo damos por fallido, aunque lo hayas procesado bien.

El sobre

Lo mismo para todos los eventos. Los campos nuevos nacen opcionales y ninguno se renombra.

Cuerpo del POST
{
  "id": "evt_9f2c4b8a1d3e4f5a6b7c8d9e0f1a2b3c",
  "type": "package.status_changed",
  "apiVersion": "2026-09-01",
  "createdAt": "2026-09-06T18:22:31.004Z",
  "data": {
    "packageId": "66f1a3c9e4b0a1d2f3c40011",
    "followingNumber": 268012,
    "orderId": "123456",
    "source": "Shopify",
    "status":         { "code": 4, "name": "delivered" },
    "previousStatus": { "code": 3, "name": "in_transit" },
    "occurredAt": "2026-09-06T18:22:30.881Z",
    "company":          { "id": "66e0aaaaaaaaaaaaaaaaaaaa" },
    "logisticsCompany": { "id": "66c4bbbbbbbbbbbbbbbbbbbb" }
  }
}

Y estas cabeceras:

X-Beartrack-Eventpackage.status_changed

Tipo de evento. Hoy sólo existe este.

X-Beartrack-Deliverydlv_66f1a3c9e4b0a1d2f3c40011

Identifica esta entrega. Es estable entre reintentos, así que te sirve para deduplicar directamente.

X-Beartrack-Subscription66e0aaaaaaaaaaaaaaaaaaaa

Qué suscripción originó la entrega. Útil si tienes más de una apuntando al mismo sitio.

X-Beartrack-Signaturet=1788669751,v1=8f3c…

Firma HMAC-SHA256. Verifícala siempre antes de procesar el cuerpo.

occurredAt es cuándo se movió el envío; createdAt es cuándo armamos el sobre. Si veníamos reintentando, entre uno y otro puede haber minutos. Para ordenar eventos usa occurredAt.

Estados

Un estado que no está en esta tabla no dispara webhook.

codenameQué pasó
0createdPedido creado
1collectedRecolectado en el comercio
2at_warehouseEscaneado en bodega
3in_transitEn ruta de reparto
30in_transitNavegando a la dirección
4deliveredEntregado
5cancelledCancelado
6delivery_failedIntento de entrega fallido
7delivery_failedIntento de entrega fallido
21return_receivedRecibido para devolución
22return_at_warehouseEn bodega para devolución
31return_in_transitEn ruta de devolución
41returnedDevolución completada
Los códigos 6 y 7 comparten el nombre delivery_failed a propósito: el número no distingue el motivo de forma confiable. Lo único cierto en ambos es que hubo un intento y el paquete no se entregó.

Verificar la firma

Hazlo siempre, antes de leer el cuerpo.

La cabecera X-Beartrack-Signature trae t=<unix>,v1=<hmac>. El v1 es el HMAC-SHA256 de "<t>.<cuerpo crudo>" con tu secreto.

import { createHmac, timingSafeEqual } from 'node:crypto'

// OJO: el cuerpo CRUDO, no el objeto ya parseado. Volver a serializar no
// reproduce los mismos bytes y la firma deja de coincidir.
export function verificar(cuerpoCrudo, cabecera, secreto) {
  const partes = Object.fromEntries(
    cabecera.split(',').map((p) => p.split('=')),
  )
  const { t, v1 } = partes
  if (!t || !v1) return false

  // Rechaza lo viejo: sin esto, un request capturado se puede reenviar siempre.
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false

  const esperado = createHmac('sha256', secreto)
    .update(`${t}.${cuerpoCrudo}`)
    .digest('hex')

  const a = Buffer.from(esperado)
  const b = Buffer.from(v1)
  return a.length === b.length && timingSafeEqual(a, b)
}

Dos detalles que rompen la verificación

Verifica sobre el cuerpo crudo, no sobre el JSON re-serializado: dos serializaciones del mismo objeto no producen los mismos bytes. Y compara en tiempo constante (timingSafeEqual, hash_equals, compare_digest), no con ===.

Reintentos

La entrega es at-least-once: prepárate para recibir el mismo evento dos veces.

Sin respuesta, 5xx, 408 o 429

hasta 12 intentos

Se trata como transitorio. Backoff exponencial con jitter, del orden de segundos hasta un tope de 30 minutos.

400, 401, 403 o 422

hasta 4 intentos

Tope bajo: si rechazas el cuerpo o la firma, reintentar no lo arregla. Los pocos intentos cubren el caso de un secreto recién rotado que todavía no configuraste.

404, 410 y demás 4xx

hasta 1 intento

La URL no existe. Reintentar no la crea, así que la entrega se descarta de inmediato.

Cómo deduplicar

Guarda el X-Beartrack-Delivery y descarta el que ya viste. Es estable entre reintentos del mismo evento, así que te sirve tal cual. El id del cuerpo funciona igual.

Pedir un webhook

Todavía no es autogestionado

Escríbenos a [email protected] con la URL que va a recibir los eventos y, si quieres acotarlo, qué estados o qué tipos de pedido te interesan. Te devolvemos el secreto de firma una sola vez.

La URL tiene que ser https y responder en menos de 10 segundos. Puedes registrar más de una.