Webhooks — avisos en tiempo real
En vez de consultar la API cada rato, deja que R2 le avise a tu sistema en el momento en que ocurre un evento: una reserva creada, un pago cobrado.
Un webhook es una URL de tu sistema que R2 llama con un POST cada vez que ocurre algo que te interesa. El dueño del negocio registra el endpoint desde su panel, elige qué eventos escuchar, y R2 empieza a avisar. Es la forma eficiente de mantenerte sincronizado: sin consultar en ciclo, sin retraso.
Eventos disponibles#
| Evento | Cuándo se dispara | Permiso |
|---|---|---|
reservation.created | Se creó una reserva de hospedaje (motor, recepción o canal). | read:reservations |
order.created | Se creó cualquier orden: restaurante, tienda, experiencias, hospedaje. | read:orders |
payment.succeeded | Un pago se confirmó como cobrado. | read:payments |
Latencia
R2 detecta y despacha los eventos en cuestión de un minuto. Los webhooks son para reaccionar rápido, no como reloj de precisión: para totales contables exactos, usa la API de lectura como fuente de verdad.
Forma del payload#
Cada aviso llega como JSON. Trae el tipo de evento, cuándo ocurrió, y un objeto data con los campos clave. Para el detalle completo, pide el recurso por la API con el order_number o el id.
{
"event": "payment.succeeded",
"occurred_at": "2026-07-16T10:20:14.907Z",
"data": {
"object": "payment",
"id": "n47dk3mzq81ba5tv",
"order_id": "p97cbaneya88az5ra",
"order_number": "ORD-MRNCRMX2-3BAW",
"amount": 2088,
"currency": "MXN",
"status": "succeeded",
"method": "card",
"succeeded_at": "2026-07-16T10:20:14.907Z"
}
}Encabezados de cada POST#
| Encabezado | Contenido |
|---|---|
R2-Signature | Firma HMAC + timestamp: t=<unix>,v1=<hex>. |
R2-Event | El tipo de evento, ej. payment.succeeded. |
R2-Delivery-Id | Identificador único de esta entrega (para deduplicar). |
R2-Webhook-Id | Identificador del webhook que recibió el aviso. |
Verificar la firma (importante)#
Al registrar el webhook, el dueño recibe un secreto de firma (empieza con whsec_) que solo se muestra una vez. Con él verificas que cada aviso viene de R2 y no de un impostor. El procedimiento es idéntico al de Stripe:
- Toma el encabezado
R2-Signaturey separat(timestamp) yv1(firma). - Construye la cadena firmada:
{t}.{cuerpo_crudo}— el cuerpo EXACTO que recibiste, sin re-serializar. - Calcula
HMAC-SHA256(secreto, cadena_firmada)en hexadecimal. - Compáralo con
v1usando comparación de tiempo constante. - Rechaza si no coincide, o si
ttiene más de 5 minutos (protección anti-repetición).
// Node.js / Express — verificación del webhook
import crypto from "node:crypto";
const SECRET = process.env.R2_WEBHOOK_SECRET; // whsec_...
// OJO: necesitas el cuerpo CRUDO (raw), no el ya parseado.
app.post("/webhooks/r2", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("R2-Signature") || "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
const body = req.body.toString("utf8");
// 1) Anti-repetición: rechaza timestamps viejos
if (Math.abs(Date.now() / 1000 - t) > 300) return res.status(400).end();
// 2) Recalcula la firma
const expected = crypto.createHmac("sha256", SECRET)
.update(`${t}.${body}`).digest("hex");
// 3) Comparación de tiempo constante
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
if (!ok) return res.status(400).end();
// 4) Responde 2xx RÁPIDO y procesa después
const evento = JSON.parse(body);
encolarParaProcesar(evento); // no bloquees la respuesta
res.status(200).end();
});# Python / Flask — verificación del webhook
import hmac, hashlib, time, os
from flask import request, abort
SECRET = os.environ["R2_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/r2")
def r2_webhook():
header = dict(p.split("=") for p in request.headers.get("R2-Signature", "").split(","))
t = int(header.get("t", "0"))
body = request.get_data() # crudo, en bytes
if abs(time.time() - t) > 300:
abort(400) # anti-repetición
expected = hmac.new(SECRET, f"{t}.".encode() + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, header.get("v1", "")):
abort(400)
evento = request.get_json()
encolar_para_procesar(evento)
return "", 200Reintentos y confiabilidad#
- Responde con cualquier código
2xxpara confirmar la recepción. Cualquier otra cosa cuenta como fallo. - Si tu endpoint falla o no responde, R2 reintenta con espera creciente: hasta 6 intentos a lo largo de varias horas.
- Un endpoint que falla muchas veces seguidas se desactiva solo; el dueño lo reactiva desde su panel cuando lo arregla.
- Responde rápido (menos de 5 segundos): confirma la recepción y procesa en segundo plano. Si tardas, R2 lo toma como timeout y reintenta.
- Puede haber entregas duplicadas en casos límite: usa
R2-Delivery-Idpara deduplicar y haz tu procesamiento idempotente.
Probar sin esperar un evento real#
Desde el panel del negocio (Configuración → R2 Connect API → Webhooks) hay un botón “Enviar evento de prueba” que dispara un aviso firmado, idéntico a uno real pero con datos ficticios, para que verifiques tu endpoint de punta a punta. El registro de entregas muestra el resultado de cada intento con su código de respuesta.