> ## 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.

# Verify webhook signatures

> Confirm that an incoming webhook request was sent by mintfax. Standard Webhooks spec, HMAC-SHA256, reference libraries in eight languages.

Every webhook delivery from mintfax is signed. Verify the signature on your endpoint before doing anything with the payload. Unsigned or invalid requests should return a non-2xx response.

mintfax follows the [Standard Webhooks specification](https://www.standardwebhooks.com/). Use a reference library in your language - you do not need to write the verification code yourself.

## The wire format

Three headers accompany every delivery:

| Header              | Value                                                                 |
| ------------------- | --------------------------------------------------------------------- |
| `webhook-id`        | Event identifier (`evt_`-prefixed). Stable across retry attempts.     |
| `webhook-timestamp` | Unix epoch seconds in ASCII decimal. Re-stamped per delivery attempt. |
| `webhook-signature` | One or more space-separated `v1,<base64-hmac>` tokens.                |

The signature is computed as:

```
HMAC-SHA256(secret, "{webhook-id}.{webhook-timestamp}.{body}")
```

Base64-encoded and emitted as `v1,<base64>`. Multiple tokens can appear separated by spaces (used during signing-secret rotation).

## Verify with a reference library

The Standard Webhooks libraries handle signature comparison, timestamp tolerance, and header parsing for you. Pick your language.

<CodeGroup>
  ```javascript Node.js theme={null}
  import { Webhook } from "standardwebhooks";

  const wh = new Webhook(process.env.MINTFAX_WEBHOOK_SECRET);

  // In your handler:
  const payload = wh.verify(req.rawBody, {
    "webhook-id": req.headers["webhook-id"],
    "webhook-timestamp": req.headers["webhook-timestamp"],
    "webhook-signature": req.headers["webhook-signature"],
  });
  // payload is the parsed JSON body. Process it.
  ```

  ```python Python theme={null}
  from standardwebhooks import Webhook

  wh = Webhook(os.environ["MINTFAX_WEBHOOK_SECRET"])

  # In your handler:
  payload = wh.verify(raw_body, {
      "webhook-id": request.headers["webhook-id"],
      "webhook-timestamp": request.headers["webhook-timestamp"],
      "webhook-signature": request.headers["webhook-signature"],
  })
  ```

  ```php PHP theme={null}
  use StandardWebhooks\Webhook;

  $wh = new Webhook($_ENV['MINTFAX_WEBHOOK_SECRET']);

  // In your handler:
  $payload = $wh->verify($rawBody, [
      'webhook-id' => $_SERVER['HTTP_WEBHOOK_ID'],
      'webhook-timestamp' => $_SERVER['HTTP_WEBHOOK_TIMESTAMP'],
      'webhook-signature' => $_SERVER['HTTP_WEBHOOK_SIGNATURE'],
  ]);
  ```

  ```go Go theme={null}
  import standardwebhooks "github.com/standard-webhooks/standard-webhooks/libraries/go"

  wh, _ := standardwebhooks.NewWebhook(os.Getenv("MINTFAX_WEBHOOK_SECRET"))

  // In your handler:
  payload, err := wh.Verify(rawBody, http.Header{
      "Webhook-Id":        []string{r.Header.Get("webhook-id")},
      "Webhook-Timestamp": []string{r.Header.Get("webhook-timestamp")},
      "Webhook-Signature": []string{r.Header.Get("webhook-signature")},
  })
  ```

  ```ruby Ruby theme={null}
  require "standardwebhooks"

  wh = StandardWebhooks::Webhook.new(ENV["MINTFAX_WEBHOOK_SECRET"])

  # In your handler:
  payload = wh.verify(raw_body, {
    "webhook-id" => request.headers["webhook-id"],
    "webhook-timestamp" => request.headers["webhook-timestamp"],
    "webhook-signature" => request.headers["webhook-signature"],
  })
  ```
</CodeGroup>

Libraries are also available for Java, Rust, C#, and Elixir at the [Standard Webhooks site](https://www.standardwebhooks.com/).

## Why use the reference libraries

* They constant-time compare signatures. A hand-rolled `==` check leaks information through timing and is a known attack vector.
* They enforce the 5-minute timestamp tolerance for you. Requests outside the window are rejected. This is the spec's defense against replay attacks.
* They accept multiple secrets at once, which is what makes [secret rotation](#rotating-your-signing-secret) painless.
* A library upgrade picks up future signature versions (`v2`, ...) without code changes.

## What to verify on the body

The signature is computed over the **exact UTF-8 bytes** of the response body. If your framework parses JSON before your handler runs, capture the raw body first.

<CodeGroup>
  ```javascript Express theme={null}
  import express from "express";
  const app = express();

  // Capture the raw body before json parsing.
  app.use(express.json({
    verify: (req, _res, buf) => { req.rawBody = buf; },
  }));
  ```

  ```python Flask theme={null}
  @app.route("/webhooks/mintfax", methods=["POST"])
  def webhook():
      raw_body = request.get_data()  # bytes, before any parsing
      # verify(raw_body, headers) ...
  ```

  ```php Laravel theme={null}
  // In a Controller action:
  $rawBody = $request->getContent();  // string, raw
  // verify($rawBody, $headers) ...
  ```
</CodeGroup>

Verifying a re-serialized JSON body will fail. Whitespace, key order, and escape choices all change the byte stream.

## Rotating your signing secret

Each endpoint has exactly one active signing secret server-side, but the Standard Webhooks libraries verify against multiple secrets at once. This is what makes rotation a no-downtime operation.

1. **Prepare your verifier to accept both secrets.** Add the new secret value alongside the old one in your environment. Deploy.
2. **Trigger the rotation.** Use the "regenerate secret" action in the dashboard or `POST /webhooks/{webhook}/rotate-secret`. The new value is shown once and replaces the old secret atomically. From this moment, every delivery is signed with the new secret.
3. **Remove the old secret.** Once you are confident no in-flight retry will arrive with the old signature, remove it from your environment.

In-flight retries re-sign each attempt with the current active secret, so a fax mid-retry at rotation time will land with the new signature on its next attempt regardless of which secret signed the original.

If a secret leaks, regenerate it immediately. If your verifier was already configured for both secrets, no deliveries are missed. If not, deliveries fail verification on your side until you update the verifier; retries will re-deliver successfully after that.

## What about timestamp tolerance

mintfax does not enforce a tolerance window on its end. The `webhook-timestamp` is re-stamped on every delivery attempt, so the receiver-side tolerance applies to each individual delivery, not to the event's age.

The reference libraries enforce a 5-minute window by default. A retry that arrives 20 minutes after the original event was generated will carry a fresh `webhook-timestamp` and will pass the window check.

## Deduplication

If you care about replay resistance beyond the timestamp window, deduplicate on `webhook-id`. The same `id` value is delivered on every retry attempt of the same event, so an idempotency cache keyed on `webhook-id` is the cheapest way to make handlers exactly-once.

## Next

* [Delivery and retries](/webhooks/delivery-and-retries) - what happens when your endpoint returns non-2xx or times out
* [Inspect delivery attempts](/webhooks/inspect-attempts) - find a specific delivery and see exactly what was sent
