Con los webhooks, Sarao avisa a tu servidor cuando pasa algo, sin que tengas que preguntar. Da de alta tus URLs en Ajustes → Developer Portal → Webhooks o con POST /v1/webhook-endpoints. Las URLs tienen que ser https y públicas (nunca direcciones privadas ni internas).
Avisos
| Tipo | Cuándo | data.object |
|---|
order.paid | Pedido pagado | order |
order.refunded | Pedido devuelto | order |
ticket.validated | Entrada validada en la puerta | ticket |
reservation.created | Reserva creada | reservation |
reservation.updated | Reserva cambiada | reservation |
Llegan por POST con este cuerpo (el objeto tiene la misma forma que en la API, congelado en el momento del primer envío):
json
{
"id": "77665544-ffff-4a3b-8c2d-1e0f9a8b7c6d",
"object": "event",
"type": "order.paid",
"created_at": "2026-11-08T23:10:00.000Z",
"data": {
"object": {
"object": "order",
"id": "9b1e4c7a-2222-4d3e-8f90-a1b2c3d4e5f6",
"event_id": "3f0c6a2e-5b1d-4c8e-9a77-2d4e8f1b6c10",
"status": "paid",
"channel": "online",
"customer": {
"id": "c0ffee00-3333-4abc-9def-001122334455",
"email": "laura@ejemplo.com",
"name": "Laura Gómez",
"phone": "+34600000000"
},
"currency": "eur",
"subtotal_cents": 3000,
"discount_cents": 0,
"fee_cents": 180,
"total_cents": 3180,
"refunded_cents": 0,
"net_cents": 3180,
"items": [
{
"ticket_type_id": "7d2f9c1a-1111-4a2b-9c3d-4e5f6a7b8c9d",
"quantity": 2,
"unit_price_cents": 1500,
"unit_discount_cents": 0,
"unit_fee_cents": 90,
"extra_cents": 0
}
],
"promoter_id": null,
"api_key_id": "5e6f7a8b-4444-4c5d-8e9f-0a1b2c3d4e5f",
"checkout_id": "e1d2c3b4-5555-4a6b-8c7d-9e0f1a2b3c4d",
"paid_at": "2026-11-08T23:10:00.000Z",
"created_at": "2026-11-08T23:08:41.000Z"
}
}
}
Cabeceras: Sarao-Signature (la firma), Sarao-Event-Id, Sarao-Event-Type y Sarao-Delivery-Id.
Verificar la firma
Cada URL tiene su secreto (whsec_…), que solo se enseña al crearla o al cambiarlo. La cabecera es t=1762643400,v1=5257a8…: t es la hora del envío (segundos Unix) y v1 el HMAC-SHA256 en hexadecimal, con tu secreto, del texto <t>.<cuerpo tal cual>. Compara en tiempo constante y rechaza las firmas de hace más de 5 minutos.
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.SARAO_WEBHOOK_SECRET; // whsec_…
// Ojo: la firma va sobre el cuerpo TAL CUAL llega (sin volver a serializar el JSON)
app.post("/sarao", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("Sarao-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${req.body}`).digest("hex");
const ok =
parts.v1 && parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)) &&
Math.abs(Date.now() / 1000 - t) < 300;
if (!ok) return res.status(400).send("Firma no válida");
const event = JSON.parse(req.body);
// Responde rápido (2xx) y procesa después; ignora los event.id que ya hayas procesado
res.sendStatus(200);
});
PHP
<?php
$secret = getenv('SARAO_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_SARAO_SIGNATURE'] ?? ''), $parts);
$expected = hash_hmac('sha256', $parts['t'] . '.' . $body, $secret);
if (!hash_equals($expected, $parts['v1'] ?? '') || abs(time() - (int) $parts['t']) > 300) {
http_response_code(400);
exit('Firma no válida');
}
$event = json_decode($body, true);
http_response_code(200);
Reintentos y registro
Responde con un 2xx en menos de 10 segundos (procesa después). Cualquier otra respuesta, una redirección o no contestar es un fallo: se reintenta a los 1, 5 y 30 minutos, y a las 2, 6, 12 y 24 horas (8 intentos en total). El mismo aviso puede llegar más de una vez: usa id para no procesarlo dos veces. Cada entrega queda en el registro (GET /v1/webhook-endpoints/{id}/deliveries y el Developer Portal), donde puedes reenviarla. El registro se guarda 30 días.