Skip to Content
Webhooks

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 sucursalEventos 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

EventoDescripción
GUIA_CREADAGuía creada exitosamente
GUIA_RECOLECTADAGuía recolectada por TPC
GUIA_EN_BODEGAGuía ingresó a bodega TPC
GUIA_EN_RUTAAsignada a mensajero y en camino
GUIA_ENTREGADAEntregada al destinatario
GUIA_DEVOLUCIONEn proceso de devolución
GUIA_ANULADAGuía anulada
GUIA_ACTUALIZADADatos de la guía actualizados
GUIA_INTENTO_FALLIDOIntento de entrega sin éxito
PINGTest 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:

EventoCampos en data
GUIA_CREADAnumeroGuia, estado
GUIA_ENTREGADAnumeroGuia, estado, fechaEntrega
GUIA_DEVOLUCIONnumeroGuia, estado, motivo
GUIA_RECOLECTADA, GUIA_EN_BODEGA, GUIA_EN_RUTA, GUIA_ANULADA, GUIA_ACTUALIZADA, GUIA_INTENTO_FALLIDOnumeroGuia, estado
PING(vacío)

Headers enviados por TPC

HeaderDescripción
X-TPC-Signaturesha256=<hmac> — firma HMAC-SHA256 del body raw
X-TPC-EventNombre del evento (ej. GUIA_ENTREGADA)
X-TPC-TimestampISO 8601 del momento del envío
X-Request-IdUUID único del delivery para diagnóstico
User-AgentTPC-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:

IntentoDelay
1Inmediato
21 segundo
32 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 -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

CampoTipoReq.Descripción
urlstring (URL HTTPS)RequeridoURL pública de tu endpoint receptor
eventosstring[]OpcionalEventos a recibir. Vacío = todos
descripcionstring (máx 200)OpcionalEtiqueta 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 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 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 -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

CampoTipoDescripción
urlstring (URL HTTPS)Nueva URL del endpoint
eventosstring[]Nueva lista de eventos (reemplaza la lista anterior)
descripcionstring (máx 200) | nullNueva 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 -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 -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

HTTPCódigoCausa
400INVALID_URLURL usa HTTP, es localhost, o es IP privada
400LIMIT_EXCEEDEDYa tienes 10 webhooks activos
404NOT_FOUNDWebhook no existe o pertenece a otra empresa
409ALREADY_INACTIVEEl webhook ya estaba desactivado (DELETE)
409WEBHOOK_INACTIVETest 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}`) } }
Last updated on