Webhooks
Que Tickeep avise a tu servidor cuando pasa algo, en vez de preguntar en bucle. Eventos, firma, reintentos y los tres fallos que se cometen siempre.
Última actualización: 23 de agosto de 2026
Un webhook es una petición que nosotros hacemos a tu servidor cuando ocurre algo. Es lo contrario de preguntar cada pocos segundos si un pedido ya está pagado: llega solo, en el momento, y no consume tu límite de uso.
Si estás vendiendo con enlace de pago, los webhooks no son opcionales: son la única forma de enterarte de que el comprador ha pagado.
Los eventos
| Evento | Cuándo se dispara | data |
|---|---|---|
order.paid | El pedido se ha cobrado y las entradas se están emitiendo | { order } |
order.cancelled | Una reserva pendiente se ha anulado y su stock está libre | { order } |
order.refunded | Se ha devuelto el importe de un pedido pagado | { order } |
ticket.checkedIn | Una entrada se ha marcado como usada en puerta | { ticket } |
ticket.voided | Una entrada ha dejado de ser válida | { ticket } |
event.published | Un evento ha pasado a estar visible | { event } |
performance.cancelled | Se ha cancelado una fecha concreta | { performance } |
Cada evento nace en un punto donde la transición ya está confirmada. Nunca antes: un order.paid de un pedido que todavía podría fallar sería peor que no mandar nada, porque ya habrías dado la entrada por buena.
El cuerpo
{
"id": "evt_mt5ml84xjszvc9",
"type": "order.paid",
"createdAt": "2026-08-23T20:00:00.000Z",
"data": {
"order": { "…": "el objeto Order completo" }
}
}
El objeto que viene en data es exactamente el mismo que devuelve la API. Un order.paid lleva el mismo Order que GET /orders/{reference}, con los mismos campos y los mismos nombres.
Es intencionado: escribes un deserializador y te sirve para las dos cosas, en vez de dos que se te desincronizan en la siguiente versión.
Las cabeceras
| Cabecera | Contenido |
|---|---|
Tickeep-Signature | t=<unix>,v1=<hmac-sha256 hex> |
Tickeep-Event | El tipo de evento (order.paid) |
Tickeep-Delivery-Id | Identificador de esta entrega |
Content-Type | application/json |
User-Agent | Tickeep-Webhooks/1 |
Tickeep-Delivery-Id identifica el intento; el id del cuerpo identifica el evento. Para deduplicar usa el del cuerpo: dos reintentos del mismo evento llevan id iguales y Tickeep-Delivery-Id distintos.
Dar de alta un endpoint
En el panel, en Configuración → Desarrolladores, con el rol de propietario o administrador. Eliges la URL, qué eventos quieres recibir y si escucha todas las salas o solo algunas.
Al crearlo recibes un secreto de firma (whsec_…). Guárdalo como guardas la clave de API.
Requisitos de la URL
- HTTPS obligatorio. El cuerpo lleva datos de compradores, y la firma garantiza autenticidad, no confidencialidad.
- Sin credenciales en la URL. Nada de
https://usuario:clave@…: acabarían en el registro de entregas. - Sin direcciones internas.
10.x,192.168.x,172.16–31.x,169.254.x,.internaly.localestán bloqueadas.
Lo último no es burocracia. La URL la eliges tú y la petición la hace nuestro servidor desde dentro de nuestra red: sin ese filtro, cualquiera podría apuntar un webhook a un servicio interno y usar Tickeep como puente para alcanzarlo.
Verificar la firma
Tu endpoint es una URL pública. Sin verificar la firma, cualquiera que la descubra puede mandarte un order.paid inventado y hacer que emitas lo que sea. La firma es lo único que distingue una entrega nuestra de una falsificada.
Tickeep-Signature: t=1756000000,v1=5257a869e7bcd0c3…
El HMAC-SHA256 se calcula sobre la cadena ${t}.${cuerpoExacto} con el secreto de tu suscripción.
Con el cliente oficial
import { verificarFirmaWebhook } from '@tickeep/client'
const ok = await verificarFirmaWebhook({
rawBody, // el cuerpo CRUDO
signature: req.headers['tickeep-signature'],
secret: process.env.TICKEEP_WEBHOOK_SECRET!,
toleranceSeconds: 300, // opcional, 300 por defecto
})
No tiene dependencias y usa WebCrypto, así que funciona en Node 18+, Deno, Bun y Cloudflare Workers.
En Express
import express from 'express'
import { verificarFirmaWebhook } from '@tickeep/client'
app.post(
'/webhooks/tickeep',
express.raw({ type: 'application/json' }), // OJO: raw, no json
async (req, res) => {
const ok = await verificarFirmaWebhook({
rawBody: req.body.toString('utf8'),
signature: req.headers['tickeep-signature'] as string,
secret: process.env.TICKEEP_WEBHOOK_SECRET!,
})
if (!ok) return res.sendStatus(401)
const evento = JSON.parse(req.body.toString('utf8'))
// Responde YA. Procesa después.
res.sendStatus(200)
void encolar(evento)
},
)
Los tres fallos que se cometen siempre
Si la firma no te cuadra, es casi seguro uno de estos tres.
1. Estás firmando el cuerpo reserializado
La firma se calculó sobre los bytes exactos que viajaron por el cable. Si parseas el JSON y lo vuelves a serializar para verificar, cambias el orden de las claves y los espacios — y la firma deja de cuadrar, aunque el contenido sea idéntico.
En Express es express.raw(), no express.json(). En Next.js, await req.text() antes de cualquier JSON.parse. Si tu framework parsea el cuerpo por ti, busca cómo conservar el original.
Es, con diferencia, la causa número uno de «la firma no me cuadra».
2. Estás validando solo el HMAC
El t forma parte de la firma, y hay que comprobarlo aparte. Si solo compruebas el HMAC, cualquiera que capture una entrega válida puede reenviarla mil veces a tu endpoint y las mil verificarán.
La ventana recomendada son 5 minutos, y en los dos sentidos: un timestamp del futuro tampoco vale. verificarFirmaWebhook ya lo hace.
3. Estás comparando con ===
Comparar cadenas con === sale antes cuando fallan los primeros caracteres, y esa diferencia de tiempo filtra cuántos se acertaron. Es suficiente para reconstruir una firma válida a base de intentos.
Usa una comparación en tiempo constante: crypto.timingSafeEqual en Node, o el cliente oficial, que ya la trae.
Tu receptor tiene que ser idempotente
Un reintento entrega el mismo id de evento. Si tu endpoint tardó en responder pero llegó a procesar, recibirás ese evento otra vez. Tickeep garantiza que el evento llega; no que llegue una sola vez.
Deduplica por el id del cuerpo antes de actuar:
async function procesar(evento: WebhookEvent) {
const nuevo = await db.eventos.insertarSiNoExiste(evento.id)
if (!nuevo) return // ya lo procesamos
switch (evento.type) {
case 'order.paid':
await miSistema.registrarVenta(evento.data.order)
break
default:
// Aparecerán eventos nuevos. No falles por ellos.
break
}
}
Ese default importa más de lo que parece: añadiremos tipos de evento con el tiempo, y un switch sin rama por defecto que lance una excepción convertiría un evento nuevo en una cadena de reintentos fallidos.
Entrega y reintentos
Solo un 2xx cuenta como entregado. Un 3xx no se sigue —seguir redirecciones de una URL que elige el cliente es un agujero de seguridad, y además la firma se calculó para el destino original—.
| Respuesta tuya | Qué hacemos |
|---|---|
| 2xx | Entregado. Fin |
| 5xx | Reintentamos |
| 429 | Reintentamos |
| 4xx | No reintentamos. Damos por hecho que tu endpoint está mal configurado |
| Timeout o red caída | Reintentamos |
5 intentos con retroceso exponencial desde 10 segundos, lo que da unas dos horas de margen. Si tu servidor se cae un rato, no pierdes nada.
El timeout de cada intento es de 10 segundos. Por eso conviene responder 200 antes de procesar y hacer el trabajo en segundo plano: si tu manejador tarda quince segundos, la entrega se marca como fallida aunque hayas hecho el trabajo.
Si tu endpoint deja de responder
Tras 20 fallos consecutivos la suscripción se desactiva sola. El contador se pone a cero con cada entrega correcta, así que un endpoint que falla una vez al mes y se recupera no acaba desactivado nunca.
En el panel verás la suscripción marcada como inactiva, con la fecha en que se desactivó y el último error registrado. Una vez arreglado tu endpoint, se reactiva desde ahí.
La desactivación no manda ninguna notificación: queda reflejada en el panel y nada más. Hasta que la haya, monta tú una alarma: si tu receptor lleva un rato sin recibir nada de una suscripción que debería estar activa, alguien tiene que enterarse. La sincronización periódica con updatedSince es la otra mitad de esa red — ver más abajo.
Reenviar a mano
Cada entrega queda registrada, con el código que devolviste y los primeros 2 KB de tu respuesta. Desde el panel puedes reenviar cualquiera: es lo que se usa cuando has arreglado un fallo y quieres recuperar lo que se perdió mientras tanto.
Ámbito de salas
Igual que las claves de API, una suscripción escucha todas las salas o una lista concreta. Si tu organización tiene varias salas y cada una trabaja con un proveedor distinto, cada suscripción recibe solo lo suyo.
Un receptor completo en Next.js
// app/api/webhooks/tickeep/route.ts
import { verificarFirmaWebhook } from '@tickeep/client'
export async function POST(req: Request) {
const rawBody = await req.text() // OJO: antes de cualquier parseo
const ok = await verificarFirmaWebhook({
rawBody,
signature: req.headers.get('tickeep-signature'),
secret: process.env.TICKEEP_WEBHOOK_SECRET!,
})
if (!ok) return new Response('firma inválida', { status: 401 })
const evento = JSON.parse(rawBody)
const nuevo = await db.eventos.insertarSiNoExiste(evento.id)
if (nuevo) await encolar(evento) // trabajo pesado, fuera del manejador
return new Response('ok', { status: 200 })
}
Webhooks o sincronización
No es una u otra: las dos.
- Los webhooks te dan la reacción inmediata. Son lo que necesitas para confirmar una venta o abrir un torno.
- La sincronización con
updatedSincees la red de seguridad. Recoge lo que se perdiera si tu servidor estuvo caído más de dos horas, o si una suscripción se desactivó sin que te dieras cuenta.
Una integración seria tiene las dos cosas. La primera hace el trabajo; la segunda te permite dormir.
Seguir por aquí
El circuito completo de una venta hecha desde tu propia web: disponibilidad, reserva, cobro y confirmación por webhook.
Cuántas peticiones por minuto admite una clave, cómo leer las cabeceras del contador y cómo diseñar una integración que no lo roce.
El paquete oficial: tipos de todo el contrato, reintentos automáticos y verificación de webhooks, sin dependencias.