Developers
R2 Connect API

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#

EventoCuándo se disparaPermiso
reservation.createdSe creó una reserva de hospedaje (motor, recepción o canal).read:reservations
order.createdSe creó cualquier orden: restaurante, tienda, experiencias, hospedaje.read:orders
payment.succeededUn 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.

json
{
  "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#

EncabezadoContenido
R2-SignatureFirma HMAC + timestamp: t=<unix>,v1=<hex>.
R2-EventEl tipo de evento, ej. payment.succeeded.
R2-Delivery-IdIdentificador único de esta entrega (para deduplicar).
R2-Webhook-IdIdentificador 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:

  1. Toma el encabezado R2-Signature y separa t (timestamp) y v1 (firma).
  2. Construye la cadena firmada: {t}.{cuerpo_crudo} — el cuerpo EXACTO que recibiste, sin re-serializar.
  3. Calcula HMAC-SHA256(secreto, cadena_firmada) en hexadecimal.
  4. Compáralo con v1 usando comparación de tiempo constante.
  5. Rechaza si no coincide, o si t tiene más de 5 minutos (protección anti-repetición).
javascript
// 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
# 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 "", 200

Reintentos y confiabilidad#

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.