fluyio.
Webhooks (próximamente)

Verificación de firmas (borrador)

Borrador técnico en inglés de la verificación de firmas prevista para futuros webhooks.

Borrador técnico en inglés · no operativo

Los webhooks salientes todavía no entregan eventos. El cuerpo en inglés que sigue conserva una propuesta técnica: no es un contrato publicado ni una guía para activar una función disponible. No implementes una integración de producción contra estos ejemplos. La API pública actual ofrece suscriptores; consulta la hoja de ruta.

Webhooks are open URLs on the public internet. Anyone who guesses the path can POST to them. To prove that a delivery really came from Fluyio — and that nobody has tampered with the body — every request is signed with an HMAC SHA-256 you can verify with your endpoint secret.

Always verify signatures in production. Treat any request that fails verification as if it never happened — log it, return 400, and move on.

Anatomy of the signature

Every webhook request includes this header:

Fluyio-Signature: t=1715000000,v1=8d5b3c1e0a9f4f2c8e1d2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d

It is a comma-separated list of key=value pairs:

KeyDescription
tUnix timestamp (seconds) when Fluyio signed the request.
v1Hex HMAC SHA-256 of ${t}.${raw_body} using your endpoint secret.

Future schemes will be added with new prefixes (v2, v3...). Always read the v1 value today; new versions will be additive.

The algorithm

Pull the raw body and the header

The body must be the exact bytes Fluyio sent — do not let your framework parse or re-serialize it before you compute the HMAC. Whitespace and key ordering matter.

Split the header

Parse t and v1 from the comma-separated list.

Build the signed payload

Concatenate: signed_payload = timestamp + "." + raw_body. Note the literal dot.

Compute the expected signature

expected = hex(hmac_sha256(secret, signed_payload)).

Compare in constant time

Use a constant-time comparison (crypto.timingSafeEqual, hmac.compare_digest, hash_equals) to avoid timing attacks.

Check the timestamp

Reject any request where |now - t| > 300 seconds. This prevents replay attacks even if a signature leaks.

Code examples

import crypto from 'crypto';
import express from 'express';

const app = express();
const TOLERANCE_SECONDS = 5 * 60;

function verify(rawBody: Buffer, header: string, secret: string) {
  const parts = Object.fromEntries(
    header.split(',').map((kv) => kv.split('=')),
  ) as { t?: string; v1?: string };

  if (!parts.t || !parts.v1) throw new Error('malformed signature');

  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (age > TOLERANCE_SECONDS) throw new Error('timestamp out of tolerance');

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody.toString('utf8')}`)
    .digest('hex');

  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(parts.v1, 'hex');

  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    throw new Error('bad signature');
  }
}

app.post(
  '/webhooks/fluyio',
  // CRITICAL: keep the body raw — do NOT use express.json() here.
  express.raw({ type: 'application/json' }),
  (req, res) => {
    try {
      verify(
        req.body,
        req.header('Fluyio-Signature')!,
        process.env.FLUYIO_WEBHOOK_SECRET!,
      );
    } catch (err) {
      console.warn('rejecting webhook', (err as Error).message);
      return res.status(400).send('bad signature');
    }

    const event = JSON.parse(req.body.toString('utf8'));
    // handle event...
    res.status(200).send('ok');
  },
);

app.listen(3000);

Replay protection

The t timestamp prevents an attacker who captures a valid signature from replaying it hours or days later. Fluyio considers any request older than 300 seconds (5 minutes) invalid.

If your servers and Fluyio's clock drift by more than that, deliveries will be rejected. Sync your servers with NTP — don't widen the tolerance.

Rotating the secret

If you suspect a secret has been exposed:

  1. Open Webhooks in the dashboard and select the endpoint.
  2. Click Roll secret. A new secret is generated.
  3. For the next 24 hours both the new and the previous secret are valid, so in-flight deliveries keep verifying. Update your application during that window.
  4. After 24h the old secret is decommissioned.

Common pitfalls

Body parsed before verification. Frameworks like Express, Fastify and Flask default to parsing JSON, which re-serializes the body and changes the bytes. Always grab the raw request body before any middleware touches it.

Wrong character encoding. Fluyio signs UTF-8 bytes. If you decode and re-encode through latin-1, the HMAC won't match. Stay in bytes.

Non-constant-time comparison. Don't use == to compare HMACs — it leaks information through timing. Use crypto.timingSafeEqual, hmac.compare_digest, or hash_equals.

Reverse proxies stripping headers. Some proxies lowercase or drop X- and custom headers. Make sure Fluyio-Signature reaches your app.

What's next

En esta página