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.
Alcance de los endpoints de la API
Los endpoints /api/v1/webhooks respetan el alcance del token que los llama:
| Token | Puede ver y administrar |
|---|---|
| Token de empresa (Matriz) | Todos los webhooks de la empresa, incluidos los de cada sucursal |
| Token de sucursal | Únicamente los webhooks de su propia sucursal |
Un webhook creado con un token de sucursal queda asociado a esa sucursal. Pedir,
modificar o eliminar el webhook de otra sucursal responde 404 NOT_FOUND — igual
que cualquier otro recurso fuera de tu alcance, para no revelar si existe.
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) y no incluir usuario ni contraseña. - Debe apuntar a un dominio o IP pública: se rechazan
localhost, nombres internos (.local,.internal, sin punto), IPs privadas o reservadas (RFC 1918,127.x,169.254.x,100.64.x, IPv6 local…) y dominios cuyo DNS resuelva a una de ellas. - La dirección se verifica otra vez en cada entrega: si tu dominio pasa a resolver a una IP privada, la entrega falla.
- No se siguen redirecciones. Una respuesta
3xxcuenta como entrega fallida — configura la URL final. - Máximo 10 webhooks activos por empresa o sucursal. Un token de sucursal crea y gestiona solo los webhooks de su sucursal.
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, incluye credenciales, o apunta (directamente o por DNS) a una dirección no pública |
400 | LIMIT_EXCEEDED | Ya tienes 10 webhooks activos en la empresa o sucursal |
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}`)
}
}