Autenticación
En el formato v2, Rapid firma cada webhook con HMAC-SHA256 usando tu clave de firma (el secret). Rapid genera esa clave cuando registras el webhook y la muestra una sola vez en el panel, en Configuración › Webhook; guárdala en el momento. No hay forma de recuperarla después: si la pierdes, genera otra en Configuración › API (la anterior deja de valer en ese mismo instante). Tu aplicación debe validar esta firma antes de procesar el payload; así confirmas que el POST vino de Rapid.
Las cuentas en formato
legacy(migradas del sistema anterior) no reciben firma: la autenticación se hace por los headersclientid/clientkey, como antes. Mira cuál es tu formato en Payload.
Header de firma
Sección titulada «Header de firma»X-Webhook-Signature: sha256=<hmac_hex>Donde <hmac_hex> es el HMAC-SHA256, en hexadecimal, de la cadena ${timestamp}.${cuerpo_crudo}, es decir, el valor del header X-Webhook-Timestamp (segundos desde la epoch), un punto y el cuerpo crudo de la solicitud (el JSON recibido, byte a byte), usando tu clave de firma.
El timestamp forma parte de la firma para dar protección contra replay: sin él, un POST válido capturado podría reenviarse indefinidamente con una firma correcta. Rechaza las solicitudes cuyo X-Webhook-Timestamp esté fuera de una ventana de tolerancia (recomendamos 5 minutos).
Cómo validar
Sección titulada «Cómo validar»- Lee el cuerpo crudo de la solicitud. No lo parsees como JSON antes de validar: la firma se calcula sobre los bytes exactos recibidos.
- Arma
signed = timestamp + "." + raw_body, calculaHMAC-SHA256(secret, signed)y codifícalo en hexadecimal - Compáralo con el valor de
X-Webhook-Signature(después del prefijosha256=) - Usa una comparación timing-safe para evitar timing attacks
Ejemplos
Sección titulada «Ejemplos»Un endpoint completo en cada lenguaje: lee el cuerpo crudo, valida la firma y la ventana de tiempo, y solo entonces lee el JSON. Responde 401 a cualquier entrega que no pase, incluso sin los headers. La clave viene de la variable de entorno RAPID_WEBHOOK_SECRET.
import crypto from 'node:crypto'import express from 'express'
const TOLERANCIA_SEGUNDOS = 5 * 60
function validarFirma(cuerpoCrudo, firma, timestamp, secret) { // 1. Ventana de tolerancia (anti-replay). Sin esto, un POST antiguo capturado // sigue pasando para siempre. const antiguedad = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) if (!Number.isFinite(antiguedad) || antiguedad > TOLERANCIA_SEGUNDOS) return false
// 2. Firma `timestamp.cuerpo_crudo` (el cuerpo recibido, sin reserializar). const esperada = 'sha256=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${cuerpoCrudo}`).digest('hex')
const a = Buffer.from(esperada) const b = Buffer.from(firma ?? '') return a.length === b.length && crypto.timingSafeEqual(a, b)}
const app = express()
// express.raw, y no express.json: la firma se calcula sobre el cuerpo crudo.app.post('/webhooks/rapid', express.raw({ type: 'application/json' }), (req, res) => { const cuerpoCrudo = req.body.toString('utf8') const ok = validarFirma( cuerpoCrudo, req.get('X-Webhook-Signature'), req.get('X-Webhook-Timestamp'), process.env.RAPID_WEBHOOK_SECRET, ) if (!ok) return res.status(401).end()
const evento = JSON.parse(cuerpoCrudo) // Procesa el `evento` (preferentemente en una cola) y responde rápido. res.status(200).end()})
app.listen(process.env.PORT ?? 3000)import hashlibimport hmacimport osimport time
from flask import Flask, request
TOLERANCIA_SEGUNDOS = 5 * 60
def validar_firma(cuerpo_crudo: bytes, firma: str, timestamp: str, secret: str) -> bool: # 1. Ventana de tolerancia (anti-replay). try: antiguedad = abs(int(time.time()) - int(timestamp)) except (TypeError, ValueError): return False if antiguedad > TOLERANCIA_SEGUNDOS or not firma: return False
# 2. Firma `timestamp.cuerpo_crudo` (los bytes recibidos, sin reserializar). firmado = timestamp.encode() + b"." + cuerpo_crudo esperada = "sha256=" + hmac.new(secret.encode(), firmado, hashlib.sha256).hexdigest() return hmac.compare_digest(esperada.encode(), firma.encode())
app = Flask(__name__)
@app.post("/webhooks/rapid")def webhook_rapid(): # get_data(), y no get_json(): la firma se calcula sobre el cuerpo crudo. cuerpo_crudo = request.get_data() ok = validar_firma( cuerpo_crudo, request.headers.get("X-Webhook-Signature", ""), request.headers.get("X-Webhook-Timestamp", ""), os.environ["RAPID_WEBHOOK_SECRET"], ) if not ok: return "", 401
evento = request.get_json() # Procesa el `evento` (preferentemente en una cola) y responde rápido. return "", 200<?phpconst TOLERANCIA_SEGUNDOS = 5 * 60;
function validarFirma(string $cuerpoCrudo, string $firma, string $timestamp, string $secret): bool{ // 1. Ventana de tolerancia (anti-replay). if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > TOLERANCIA_SEGUNDOS) { return false; }
// 2. Firma `timestamp.cuerpo_crudo` (los bytes recibidos, sin reserializar). $esperada = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $cuerpoCrudo, $secret); return hash_equals($esperada, $firma);}
// php://input, y no json_decode antes: la firma se calcula sobre el cuerpo crudo.$cuerpoCrudo = file_get_contents('php://input');$ok = validarFirma( $cuerpoCrudo, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '', $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '', getenv('RAPID_WEBHOOK_SECRET'));if (!$ok) { http_response_code(401); exit;}
$evento = json_decode($cuerpoCrudo, true);// Procesa el $evento (preferentemente en una cola) y responde rápido.http_response_code(200);Verificador de firma
Sección titulada «Verificador de firma»¿Tu validación no coincide? Pega aquí lo que recibiste y compáralo con la firma esperada.
El cálculo se hace en tu navegador: nada de lo que pegues aquí sale de esta página. Aun así, prefiere probar con la clave de un webhook de prueba.
Rotación del secret
Sección titulada «Rotación del secret»Tú mismo generas una clave nueva en el panel, en Configuración › API. Aparece una sola vez, y a partir de ahí los webhooks ya salen firmados con ella, sin período de superposición: quien valide con la anterior empieza a rechazar. Cambia la clave en tus servidores enseguida, de preferencia en una ventana de poco tráfico.
Guardar una URL nueva no cambia la clave.
Headers heredados
Sección titulada «Headers heredados»En el formato legacy, Rapid envía los headers clientid y clientkey y no envía firma, exactamente como el sistema anterior.
clientid: <client_id>clientkey: <client_secret>En el formato v2 estos headers no existen: validar por clientid/clientkey significaría recibir la credencial de acceso a la API en cada solicitud (menos seguro que firmar el payload), así que la autenticación de la entrega es solo X-Webhook-Signature. Quien está en legacy sigue recibiendo los headers por tiempo indefinido, para no romper integraciones existentes.