Skip to main content
Getting Started

Verify Webhook Signatures

Every request from XRNotify is signed with your webhook secret. Verifying this signature protects your endpoint from spoofed or tampered requests.

How signatures work

When XRNotify delivers an event, it computes an HMAC-SHA256 digest of the raw request body using your webhook's signing secret. The result is included in the X-XRNotify-Signature request header, prefixed with sha256=.

To verify, you recompute the HMAC using the same secret and compare your result to the header value using a constant-time comparison function to prevent timing attacks.

Webhook request headers

HeaderDescription
X-XRNotify-SignatureHMAC-SHA256 of the raw body, formatted as sha256=<hex_digest>
X-XRNotify-TimestampUnix timestamp (seconds) of when the delivery was initiated
X-XRNotify-Delivery-IdUnique delivery ID — use for idempotency checks in your database
X-XRNotify-Webhook-IdID of the webhook this event was delivered to

Security warning: Always verify the signature before processing any event data. Never trust the payload contents without first confirming the request originated from XRNotify.

Verification examples

const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const expected = `sha256=${crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex')}`;

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Express middleware
app.use('/webhooks', express.raw({ type: 'application/json' }));

app.post('/webhooks/xrpl', (req, res) => {
  const valid = verifySignature(
    req.body,
    req.headers['x-xrnotify-signature'],
    process.env.WEBHOOK_SECRET
  );

  if (!valid) return res.status(401).json({ error: 'Invalid signature' });

  const event = JSON.parse(req.body);
  console.log('Verified event:', event.event_type);

  // process event...
  res.sendStatus(200);
});

Common mistakes

Parsing JSON before verifying

The signature covers the raw bytes of the request body. If you parse the JSON first and re-serialize, even a single whitespace difference will cause verification to fail. Always use the raw body buffer.

Using == for string comparison

Standard equality checks are vulnerable to timing attacks where an attacker can infer the correct signature byte-by-byte by measuring response times. Always use a constant-time comparison function like crypto.timingSafeEqual (Node.js), hmac.compare_digest (Python), or hmac.Equal (Go).

Forgetting the sha256= prefix

The header value is sha256=<hex_digest>, not just the hex digest. Your expected value must include the prefix, otherwise the comparison will always fail.

Security best practices

  • Rotate secrets if compromised

    Use the rotate-secret endpoint (POST /v1/webhooks/:id/rotate-secret) to immediately invalidate the old secret and generate a new one.

  • Reject stale events

    Check the X-XRNotify-Timestamp header and reject events older than 5 minutes. This prevents replay attacks where an attacker re-sends a valid old request.

  • Deduplicate with delivery IDs

    Store X-XRNotify-Delivery-Id values in your database to ensure idempotent processing. XRNotify may retry failed deliveries, so your handler should be safe to call multiple times.

  • Store secrets in environment variables

    Never hardcode webhook secrets in your source code. Use environment variables or a secrets manager like AWS Secrets Manager or HashiCorp Vault.

Next steps