v1Resumen/Firma y payloads

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...a91d

Recalcula 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.

No analices el cuerpo antes de verificar

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.

Secretowhsec_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=6204bc359e5eabdf2060a37634581c0276a9c99221d54a6063cb4373f43b2b04

Playground de firmas

Playground 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.