Firma y payloads
El sobre del payload
Cada entrega comparte el mismo sobre. El objeto data tiene la forma que dicta el tipo de evento.
{
"id": "msg_3Ge4EysPsTMIbms5w4bM45ZkR5z",
"type": "review.received",
"created_at": "2026-08-30T14:12:04Z",
"api_version": "2026-06-01",
"data": {
"review_id": "rev_8kQ2mT",
"location_id": "loc_4419",
"client_id": "cli_221",
"directory": "google",
"rating": 2,
"author": "M. Ferraro",
"text": "Waited 40 minutes past my appointment.",
"language": "en",
"url": "https://maps.google.com/.../rev_8kQ2mT"
}
}api_version queda fijado en tu endpoint en el momento de crearlo (actualmente 2026-06-01): un futuro cambio incompatible en la forma de data solo afecta a los endpoints creados después de ese cambio de versión.
Request headers
Cada entrega incluye estos encabezados junto con el cuerpo firmado:
Synup-SignatureLa firma a verificar — ver más abajo.Synup-DeliveryEl id propio del sobre. Útil para deduplicar una entrega reintentada.Synup-EventEl tipo de evento del sobre — permite enrutar sin analizar el cuerpo primero.Verificar firmas
Cada solicitud incluye una cabecera Synup-Signature para que puedas confirmar que realmente proviene de Synup:
Synup-Signature: t=1788112324,v1=8f4c...a91dRecalcula HMAC-SHA256 sobre t + "." + rawBody usando el secreto de firma de tu endpoint, y compáralo con v1 en tiempo constante. Rechaza cualquier firma en la que t tenga más de cinco minutos de antigüedad, para limitar el riesgo de ataques de repetición.
La verificación necesita los bytes exactos y sin procesar de la solicitud. Si el analizador de cuerpo de tu framework se ejecuta primero y vuelve a serializar el JSON —aunque solo sea reformateando espacios—, la firma no coincidirá. Lee el cuerpo sin procesar de esta ruta antes de que cualquier middleware de análisis de JSON lo toque.
Ejemplo
const crypto = require("crypto");
function verifyWebhookSignature(header, rawBody, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const t = Number(parts.t);
const v1 = parts.v1;
if (!t || !v1) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const signedPayload = `${t}.${rawBody}`;
const expected = crypto.createHmac("sha256", secret).update(signedPayload).digest("hex");
const a = Buffer.from(v1, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
// Express example
app.post("/synup/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.header("Synup-Signature");
if (!verifyWebhookSignature(signature, req.body.toString("utf8"), process.env.SYNUP_WEBHOOK_SECRET)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body);
// ... handle event.type / event.data
res.status(200).send({ received: true });
});Prueba tu implementación
Ejecuta tu verificador con estos valores fijos: si tu resultado coincide con la firma esperada de abajo, tu implementación es correcta.
whsec_test_secret_keyMarca de tiempo1700000000Payload{"id":"evt_test123","type":"review.received","created_at":"2023-11-14T22:13:20Z","api_version":"2026-06-01","data":{"review_id":"rev_test","rating":5}}Firma esperadat=1700000000,v1=6204bc359e5eabdf2060a37634581c0276a9c99221d54a6063cb4373f43b2b04Playground de firmas
Calcula el HMAC completamente en tu navegador; nada de esto se envía a ningún sitio. Pega tu propio secreto y payload para ver la cabecera exacta que enviaríamos.