Webhooks
Recibe notificaciones en tiempo real cuando el estado de tus guías cambia. Registra una URL HTTPS y TPC enviará un POST automático cada vez que ocurra un evento.
Ventaja vs polling: en lugar de consultar /guias cada N minutos, TPC te avisa en segundos cuando cambia el estado — sin consumir rate limit.
Webhooks por sucursal
Si tu empresa tiene sucursales habilitadas, los webhooks pueden configurarse por separado para cada sucursal.
| Tipo de webhook | ¿Quién lo recibe? |
|---|---|
| Webhook de empresa (sin sucursal) | Eventos generados por token empresa (Matriz) |
| Webhook de sucursal | Eventos generados por el token de esa sucursal + eventos de empresa que afectan a la sucursal |
Cuando una sucursal genera un evento (por ejemplo, crea una guía con su token), se disparan los webhooks de empresa y los webhooks de esa sucursal. El webhook de empresa actúa como receptor global; el de sucursal permite escuchar eventos específicos de cada sede.
Los webhooks de sucursal se administran desde el portal en Integraciones → pestaña de la sucursal. Solo la cuenta Matriz puede crearlos.
Configuración rápida
Registra tu endpoint
curl -X POST https://api.tpcxpress.com/api/v1/webhooks \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://mi-app.com/api/webhook/tpc",
"eventos": ["GUIA_ENTREGADA", "GUIA_ANULADA"],
"descripcion": "Producción — actualizaciones de estado"
}'Guarda el secret
La respuesta incluye un secret que solo se muestra una vez. Guárdalo en tus variables de entorno inmediatamente:
TPC_WEBHOOK_SECRET=a3f8b2c1d4e5...Implementa el receptor
Recibe el POST, verifica la firma y procesa el evento:
// app/api/webhook/tpc/route.ts
import crypto from "crypto"
export async function POST(req: Request) {
const body = await req.text()
const sig = req.headers.get("x-tpc-signature") ?? ""
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.TPC_WEBHOOK_SECRET!)
.update(body)
.digest("hex")
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return new Response("Unauthorized", { status: 401 })
}
const payload = JSON.parse(body)
await procesarEvento(payload)
return new Response("OK", { status: 200 })
}Prueba la conexión
curl -X POST https://api.tpcxpress.com/api/v1/webhooks/wh_uuid/test \
-H "Authorization: Bearer TOKEN"Eventos disponibles
| Evento | Descripción |
|---|---|
GUIA_CREADA | Guía creada exitosamente |
GUIA_RECOLECTADA | Guía recolectada por TPC |
GUIA_EN_BODEGA | Guía ingresó a bodega TPC |
GUIA_EN_RUTA | Asignada a mensajero y en camino |
GUIA_ENTREGADA | Entregada al destinatario |
GUIA_DEVOLUCION | En proceso de devolución |
GUIA_ANULADA | Guía anulada |
GUIA_ACTUALIZADA | Datos de la guía actualizados |
GUIA_INTENTO_FALLIDO | Intento de entrega sin éxito |
PING | Test de conectividad |
Suscríbete a eventos específicos o envía "eventos": [] para recibir todos los eventos.
Formato del payload
TPC envía un POST con Content-Type: application/json:
{
"event": "GUIA_ENTREGADA",
"timestamp": "2026-07-06T14:30:00.000Z",
"webhookId": "uuid-del-delivery",
"data": {
"numeroGuia": "TPCP123456789",
"estado": "ENTREGADO"
}
}El campo data varía por evento:
| Evento | Campos en data |
|---|---|
GUIA_CREADA | numeroGuia, estado |
GUIA_ENTREGADA | numeroGuia, estado, fechaEntrega |
GUIA_DEVOLUCION | numeroGuia, estado, motivo |
GUIA_RECOLECTADA, GUIA_EN_BODEGA, GUIA_EN_RUTA, GUIA_ANULADA, GUIA_ACTUALIZADA, GUIA_INTENTO_FALLIDO | numeroGuia, estado |
PING | (vacío) |
Headers enviados por TPC
| Header | Descripción |
|---|---|
X-TPC-Signature | sha256=<hmac> — firma HMAC-SHA256 del body raw |
X-TPC-Event | Nombre del evento (ej. GUIA_ENTREGADA) |
X-TPC-Timestamp | ISO 8601 del momento del envío |
X-Request-Id | UUID único del delivery para diagnóstico |
User-Agent | TPC-Webhooks/1.0 |
Verificar la firma (CRÍTICO)
Siempre verifica X-TPC-Signature para confirmar que el request viene de TPC.
import crypto from "crypto"
function verificarFirma(
rawBody: string, // body como string ANTES de JSON.parse
secret: string, // TPC_WEBHOOK_SECRET
signature: string, // header X-TPC-Signature
): boolean {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex")
// timingSafeEqual previene ataques de timing
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
)
}Leer el body como string raw antes de JSON.parse. El HMAC se calcula sobre los bytes exactos recibidos. Si parseas primero, el cálculo falla.
Política de reintentos
Si tu endpoint no responde 2xx en 5 segundos, TPC reintenta automáticamente:
| Intento | Delay |
|---|---|
| 1 | Inmediato |
| 2 | 1 segundo |
| 3 | 2 segundos |
Tras 3 fallos, el delivery se registra como fallido. No hay reintentos adicionales. Usa el endpoint de tracking para recuperar el historial si necesitas el evento.
Tu endpoint debe responder rápido (< 5s). Si tu lógica es lenta, responde 200 OK de inmediato y procesa de forma asíncrona (colas, background jobs).
Endpoints
POST/api/v1/webhooksRegistrar
curl
curl -X POST https://api.tpcxpress.com/api/v1/webhooks \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://mi-app.com/api/webhook/tpc",
"eventos": ["GUIA_ENTREGADA", "GUIA_ANULADA"],
"descripcion": "Producción"
}'Body
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
url | string (URL HTTPS) | Requerido | URL pública de tu endpoint receptor |
eventos | string[] | Opcional | Eventos a recibir. Vacío = todos |
descripcion | string (máx 200) | Opcional | Etiqueta para identificar el webhook |
Restricciones de URL:
- Debe usar
https://(HTTP rechazado) - No puede ser
localhost,127.x.x.x, ni rangos RFC 1918 (10.x,172.16-31.x,192.168.x) - Máximo 10 webhooks activos por empresa
Respuesta 201
{
"success": true,
"data": {
"id": "wh_uuid",
"url": "https://mi-app.com/api/webhook/tpc",
"secret": "a3f8b2c1d4e5f6...",
"eventos": ["GUIA_ENTREGADA", "GUIA_ANULADA"],
"descripcion": "Producción",
"createdAt": "2026-07-06T14:00:00.000Z",
"nota": "Guarda el secret — no se mostrará nuevamente."
}
}El secret solo aparece en esta respuesta. Guárdalo en una variable de entorno inmediatamente. No hay forma de recuperarlo después — tendrías que eliminar el webhook y crear uno nuevo.
GET/api/v1/webhooksListar
curl
curl https://api.tpcxpress.com/api/v1/webhooks \
-H "Authorization: Bearer TOKEN"Respuesta 200
{
"success": true,
"data": [
{
"id": "wh_uuid",
"url": "https://mi-app.com/api/webhook/tpc",
"eventos": ["GUIA_ENTREGADA"],
"descripcion": "Producción",
"createdAt": "2026-07-06T14:00:00.000Z",
"ultimoDelivery": {
"exitoso": true,
"statusCode": 200,
"fecha": "2026-07-06T15:32:00.000Z"
}
}
]
}El campo secret nunca se retorna en listados ni en GET por ID.
GET/api/v1/webhooks/{id}Detalle
Incluye los últimos 20 deliveries del webhook, ordenados del más reciente al más antiguo.
curl
curl https://api.tpcxpress.com/api/v1/webhooks/wh_uuid \
-H "Authorization: Bearer TOKEN"Respuesta 200
{
"success": true,
"data": {
"id": "wh_uuid",
"url": "https://mi-app.com/api/webhook/tpc",
"eventos": ["GUIA_ENTREGADA"],
"descripcion": "Producción",
"activo": true,
"createdAt": "2026-07-06T14:00:00.000Z",
"entregas": [
{
"id": "delivery_uuid",
"evento": "GUIA_ENTREGADA",
"exitoso": true,
"statusCode": 200,
"intentos": 1,
"ultimoIntentoEn": "2026-07-06T15:32:00.000Z",
"createdAt": "2026-07-06T15:32:00.000Z"
},
{
"id": "delivery_uuid_2",
"evento": "GUIA_CREADA",
"exitoso": false,
"statusCode": 500,
"intentos": 3,
"ultimoIntentoEn": "2026-07-06T12:00:05.000Z",
"createdAt": "2026-07-06T12:00:00.000Z"
}
]
}
}PATCH/api/v1/webhooks/{id}Actualizar
Actualiza la URL, eventos suscritos o descripción de un webhook existente. Al menos un campo es requerido.
curl
curl -X PATCH https://api.tpcxpress.com/api/v1/webhooks/wh_uuid \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://mi-app.com/api/webhook/tpc-v2",
"eventos": ["GUIA_ENTREGADA", "GUIA_DEVOLUCION", "GUIA_ANULADA"]
}'Body
| Campo | Tipo | Descripción |
|---|---|---|
url | string (URL HTTPS) | Nueva URL del endpoint |
eventos | string[] | Nueva lista de eventos (reemplaza la lista anterior) |
descripcion | string (máx 200) | null | Nueva descripción |
Las mismas restricciones de URL que al crear aplican también aquí.
Respuesta 200
{
"success": true,
"data": {
"id": "wh_uuid",
"url": "https://mi-app.com/api/webhook/tpc-v2",
"eventos": ["GUIA_ENTREGADA", "GUIA_DEVOLUCION", "GUIA_ANULADA"],
"descripcion": "Producción",
"activo": true,
"createdAt": "2026-07-06T14:00:00.000Z"
}
}DELETE/api/v1/webhooks/{id}Desactivar
Desactivación suave — el historial de deliveries se preserva. No se eliminan datos.
curl
curl -X DELETE https://api.tpcxpress.com/api/v1/webhooks/wh_uuid \
-H "Authorization: Bearer TOKEN"Respuesta 200
{
"success": true,
"data": {
"id": "wh_uuid",
"activo": false
}
}POST/api/v1/webhooks/{id}/testProbar
Envía un evento PING de forma síncrona y retorna el resultado inmediato. Útil para verificar conectividad sin esperar un evento real.
curl
curl -X POST https://api.tpcxpress.com/api/v1/webhooks/wh_uuid/test \
-H "Authorization: Bearer TOKEN"Respuesta 200
{
"success": true,
"data": {
"exitoso": true,
"statusCode": 200,
"durationMs": 143
}
}Payload que tu endpoint recibe durante el test:
{
"event": "PING",
"timestamp": "2026-07-06T14:00:00.000Z",
"webhookId": "uuid-del-delivery",
"data": {}
}Errores específicos de webhooks
| HTTP | Código | Causa |
|---|---|---|
400 | INVALID_URL | URL usa HTTP, es localhost, o es IP privada |
400 | LIMIT_EXCEEDED | Ya tienes 10 webhooks activos |
404 | NOT_FOUND | Webhook no existe o pertenece a otra empresa |
409 | ALREADY_INACTIVE | El webhook ya estaba desactivado (DELETE) |
409 | WEBHOOK_INACTIVE | Test en webhook inactivo |
Implementación de referencia completa
// app/api/webhook/tpc/route.ts — Next.js App Router
import crypto from "crypto"
import { NextRequest } from "next/server"
export async function POST(req: NextRequest) {
// 1. Leer body como string raw (antes de JSON.parse)
const body = await req.text()
const sig = req.headers.get("x-tpc-signature") ?? ""
const secret = process.env.TPC_WEBHOOK_SECRET!
// 2. Verificar firma HMAC
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex")
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
console.warn("TPC webhook: firma inválida")
return new Response("Unauthorized", { status: 401 })
}
// 3. Parsear y responder rápido
const payload = JSON.parse(body) as {
event: string
timestamp: string
webhookId: string
data: {
numeroGuia?: string
estado?: string
fechaEntrega?: string
motivo?: string
}
}
// 4. Procesar de forma asíncrona (no bloquear la respuesta)
procesarEventoAsync(payload).catch(err =>
console.error("Error procesando webhook TPC:", err)
)
return new Response("OK", { status: 200 })
}
async function procesarEventoAsync(payload: {
event: string
data: { numeroGuia?: string; estado?: string; fechaEntrega?: string; motivo?: string }
}) {
switch (payload.event) {
case "GUIA_ENTREGADA":
await actualizarEstadoPedido(payload.data.numeroGuia!, "entregado")
await notificarCliente(payload.data.numeroGuia!)
break
case "GUIA_DEVOLUCION":
await actualizarEstadoPedido(payload.data.numeroGuia!, "devolucion")
break
case "GUIA_ANULADA":
await cancelarPedido(payload.data.numeroGuia!)
break
case "PING":
// Solo para test — nada que hacer
break
default:
console.log(`Evento no manejado: ${payload.event}`)
}
}