Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.ryvo.so/llms.txt

Use this file to discover all available pages before exploring further.

Cada evento incluye un header X-Ryvo-Signature con esta estructura:
X-Ryvo-Signature: t=1714502143,v1=5257a86931c0...
  • t — timestamp Unix en segundos cuando firmamos el evento.
  • v1 — hexadecimal del HMAC-SHA-256 de ${t}.${body_raw} usando tu signing secret.

Cómo verificar

1

Lee el header

Parsea X-Ryvo-Signature y extrae t y v1.
2

Reconstruye la firma esperada

Computa HMAC-SHA-256(secret).update(t + '.' + raw_body).digest('hex'). Usa el body crudo (string), no JSON parseado y reserializado.
3

Compara constant-time

Usa una comparación constant-time (no ===) para evitar timing attacks.
4

Verifica el timestamp

Rechaza eventos con t más viejo que 5 minutos para mitigar replay attacks.

Snippets

import express from "express"
import crypto from "crypto"

const app = express()

// IMPORTANTE: necesitas el body crudo. Usa express.raw para esta ruta.
app.post(
  "/webhook/ryvo",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signatureHeader = req.headers["x-ryvo-signature"]
    if (!signatureHeader) return res.status(401).send("missing signature")

    const parts = signatureHeader.split(",").reduce((acc, p) => {
      const [k, v] = p.split("=")
      acc[k] = v
      return acc
    }, {})

    const ts = parts.t
    const sig = parts.v1
    if (!ts || !sig) return res.status(401).send("invalid signature format")

    // Tolerancia de 5 minutos
    if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
      return res.status(401).send("signature too old")
    }

    const expected = crypto
      .createHmac("sha256", process.env.RYVO_WEBHOOK_SECRET)
      .update(`${ts}.${req.body.toString()}`)
      .digest("hex")

    const a = Buffer.from(sig, "hex")
    const b = Buffer.from(expected, "hex")
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.status(401).send("invalid signature")
    }

    const event = JSON.parse(req.body.toString())
    console.log("Verified event:", event.type, event.id)

    res.sendStatus(200)
  }
)

Errores comunes

El HMAC tiene que correrse contra el body crudo (string original). Si parseas y reserializas, el JSON puede salir con otro espaciado y la firma deja de coincidir.
=== o strcmp corta el loop al primer byte distinto, lo que filtra info por timing. Usa crypto.timingSafeEqual (Node), hmac.compare_digest (Python), hmac.Equal (Go).
Sin tolerancia de timestamp, un atacante que capture un evento legítimo puede replayarlo días después. Rechaza eventos con t más viejo que 5 minutos.
Algunos frameworks (Next.js API routes, NestJS) parsean el body antes de pasarlo al handler. Configura tu ruta para acceder al raw body antes del JSON parse.