Retryx Docs

Verify signatures

Anyone can POST to your server. Every request Retryx sends is signed with your destination’s signing secret, so you can reject anything that did not come from us.

The signature header

Retryx-Signature: t=1700000000,v1=5257a869…

  • t is the Unix time (seconds) the request was signed. It is also sent as Retryx-Timestamp.
  • v1 is the lowercase hex HMAC-SHA256 of the string "<t>.<raw request body>", keyed with your signing secret.
  • During secret rotation two v1 values are sent (new secret and old secret). Accept the request if any matches.
Always verify against the raw body bytes exactly as received, not re-serialised JSON. Compare signatures in constant time, and reject timestamps more than 5 minutes from your clock to stop replayed requests.

Verification code

import { createHmac, timingSafeEqual } from 'node:crypto';

/** rawBody: Buffer|string exactly as received. header: value of the Retryx-Signature header. */
export function verifyRetryxSignature(rawBody, header, secret, toleranceSec = 300) {
  const parts = String(header ?? '').split(',').map((p) => p.trim().split('='));
  const t = parts.find(([k]) => k === 't')?.[1];
  const signatures = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
  if (!t || !/^\d+$/.test(t) || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false; // replay protection
  const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
  return signatures.some((s) => {
    const got = Buffer.from(s, 'hex');
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}

Using it in a handler

// Express — capture the RAW body, do not use express.json() on this route
app.post('/webhooks/retryx', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyRetryxSignature(req.body, req.get('Retryx-Signature'), process.env.RETRYX_SIGNING_SECRET)) {
    return res.sendStatus(400);
  }
  // Delivery is at-least-once: de-duplicate on the stable Idempotency-Key header.
  const key = req.get('Idempotency-Key');
  // …process the event once per key…
  res.sendStatus(200);
});

Rotating the secret without downtime

  1. In the dashboard open the destination and choose Rotate secret, with an overlap window (default 24 h).
  2. Deploy the new secret to your server. Until the overlap ends, every request carries signatures for both secrets, so both old and new code verify.
  3. After the overlap only the new secret is used.

Verifying providers on the way in

When you attach Stripe, GitHub, Shopify or a generic signed source to an endpoint, Retryx verifies the provider’s own signature against the raw body before accepting the event (constant-time comparison, secrets encrypted at rest). Each provider uses a different scheme:

SourceHeaderScheme
StripeStripe-SignatureHMAC-SHA256 over t.payload with v1=; timestamp tolerance 5 min (configurable).
GitHubX-Hub-Signature-256HMAC-SHA256 of the raw body, sha256=<hex>.
ShopifyX-Shopify-Hmac-Sha256Base64 HMAC-SHA256 of the raw body.
GenericYour headerConfigurable algorithm (SHA-1/256/512), encoding (hex/base64), optional prefix and signed timestamp with tolerance.

Forwarded events keep the original provider headers (unless you transform the payload, which would invalidate them), so you can still verify the upstream signature at your server if you wish.