Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintfax.com/docs/llms.txt

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

Webhook signing and verification

Outcome

After following these steps you will have a webhook receiver that verifies HMAC-SHA256 signatures, rejects replayed requests, and captures the raw body correctly (the step most people get wrong). Code samples cover Node.js, Python, PHP, .NET, and Go.

Prerequisites

  • A mintfax account with a webhook endpoint configured. Your endpoint’s signing secret is shown once when you create or rotate it.
  • Your signing secret stored as an environment variable. The examples below use MINTFAX_WEBHOOK_SECRET. In the sandbox, this looks like whsec_test_3JzE9rYNm2VbQ8P6KxLf1WdGa4Tc.
  • A publicly reachable HTTPS URL that can receive POST requests.

How mintfax signs webhooks

Every webhook request includes two headers:
HeaderValue
X-Mintfax-SignatureHex-encoded HMAC-SHA256 of {timestamp}.{raw_body}
X-Mintfax-TimestampUnix timestamp (seconds) when mintfax generated the payload
The signed content is the timestamp, a literal ., and the raw request body concatenated together. Including the timestamp prevents replay attacks because the signature is only valid for that specific moment and payload.

Step 1: Capture the raw request body

This is where most verification failures happen. If your framework parses JSON before your code runs, it may alter whitespace or key order. That changes the byte sequence and breaks the signature check.
// Use express.raw() instead of express.json() for the webhook route
app.post(
  '/webhooks/mintfax',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const rawBody = req.body.toString('utf8');
    // rawBody is the unmodified request body
  }
);
Verify: Log the first 50 bytes of rawBody and confirm they match the original JSON, including whitespace.

Step 2: Reject stale timestamps

Compare the X-Mintfax-Timestamp header to the current time. Reject requests older than five minutes to block replay attacks.
const timestamp = req.headers['x-mintfax-timestamp'];
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
  return res.status(403).send('Timestamp too old');
}
Verify: Send a request with a timestamp from ten minutes ago and confirm your endpoint returns 403.

Step 3: Verify the signature

Compute HMAC-SHA256 over {timestamp}.{rawBody} using your signing secret, then compare it to the X-Mintfax-Signature header. Use a constant-time comparison function. A naive == check leaks information through response timing, which an attacker can use to guess the correct signature byte by byte.
const crypto = require('crypto');

const secret = process.env.MINTFAX_WEBHOOK_SECRET;
const signature = req.headers['x-mintfax-signature'];
const expected = crypto
  .createHmac('sha256', secret)
  .update(`${timestamp}.${rawBody}`)
  .digest('hex');

const ok = crypto.timingSafeEqual(
  Buffer.from(expected),
  Buffer.from(signature)
);
if (!ok) {
  return res.status(401).send('Invalid signature');
}
Verify: Tamper with one byte of the request body and confirm your endpoint returns 401.

Step 4: Handle key rotation

When you rotate your signing secret (via POST /account/webhooks/{id}/rotate-secret), in-flight requests may still carry the old signature. To avoid dropping those:
  1. Store both the old and new secrets.
  2. Attempt verification with the new secret first.
  3. If that fails, try the old secret.
  4. After five minutes (the replay window), remove the old secret.

Verify

Send a test webhook from the sandbox. Use the sandbox signing secret (whsec_test_3JzE9rYNm2VbQ8P6KxLf1WdGa4Tc) and send a fax to +15005550001 (the success magic number). Your endpoint should:
  1. Accept the fax.queued event with a 200 response.
  2. Reject a replayed copy of the same request (timestamp too old or duplicate event_id).
  3. Reject a request with a modified body (signature mismatch).
Use the event_id field in the payload for deduplication. Store each event_id you process and skip any you have already seen.

What to do next

  • Events schema - full list of event types and payload shapes.
  • Error catalog - machine-readable error codes and next actions.
  • Idempotency keys - make retries safe on your outbound API calls.
  • Rate limits - request limits and Retry-After headers.
  • Sandbox - magic numbers and simulated failure scenarios for integration testing.