> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fastcac.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verifying signatures

> Make sure a webhook came from FastCAC and wasn't changed.

Webhooks are signed with the [Standard Webhooks](https://www.standardwebhooks.com) scheme:
HMAC-SHA256 over the message id, timestamp and raw body, using your endpoint's secret. Use a
Standard Webhooks library rather than writing your own; each one checks the signature and rejects
timestamps more than five minutes old, which stops replays.

> [!WARNING]
>
> Verify against the **raw** request body, byte for byte. If your framework parses JSON first and
> you re-serialize it, the signature won't match. Parse the body only after verifying it.

Get the secret from the dashboard or `GET /v1/webhooks/endpoints/{id}/secret`. Pass it with its
`whsec_` prefix; the libraries accept it as is.

```javascript Node.js theme={null}
// npm install standardwebhooks
import express from "express";
import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.FASTCAC_WEBHOOK_SECRET);
const app = express();

app.post("/fastcac/webhooks", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = wh.verify(req.body.toString("utf8"), req.headers);
  } catch {
    return res.status(400).send("bad signature");
  }
  // event.id, event.type, event.data
  res.sendStatus(204);
});
```

```python Python theme={null}
# pip install standardwebhooks
import os
from flask import Flask, request
from standardwebhooks.webhooks import Webhook

wh = Webhook(os.environ["FASTCAC_WEBHOOK_SECRET"])
app = Flask(__name__)

@app.post("/fastcac/webhooks")
def fastcac_webhook():
    try:
        event = wh.verify(request.get_data(), dict(request.headers))
    except Exception:
        return "bad signature", 400
    # event["id"], event["type"], event["data"]
    return "", 204
```

```go Go theme={null}
// go get github.com/standard-webhooks/standard-webhooks/libraries/go
package main

import (
	"io"
	"net/http"
	"os"

	standardwebhooks "github.com/standard-webhooks/standard-webhooks/libraries/go"
)

func main() {
	wh, err := standardwebhooks.NewWebhook(os.Getenv("FASTCAC_WEBHOOK_SECRET"))
	if err != nil {
		panic(err)
	}
	http.HandleFunc("/fastcac/webhooks", func(w http.ResponseWriter, r *http.Request) {
		body, _ := io.ReadAll(r.Body)
		if err := wh.Verify(body, r.Header); err != nil {
			http.Error(w, "bad signature", http.StatusBadRequest)
			return
		}
		// json.Unmarshal(body, &event)
		w.WriteHeader(http.StatusNoContent)
	})
	http.ListenAndServe(":8080", nil)
}
```

```php PHP theme={null}
<?php
// composer require svix/svix  (Svix's library verifies Standard Webhooks signatures)
require 'vendor/autoload.php';

$payload = file_get_contents('php://input');
$headers = [
    'webhook-id' => $_SERVER['HTTP_WEBHOOK_ID'] ?? '',
    'webhook-timestamp' => $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '',
    'webhook-signature' => $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '',
];

try {
    $wh = new \Svix\Webhook(getenv('FASTCAC_WEBHOOK_SECRET'));
    $event = $wh->verify($payload, $headers);
} catch (\Exception $e) {
    http_response_code(400);
    exit('bad signature');
}
http_response_code(204);
```

## Rotating the secret

`POST /v1/webhooks/endpoints/{id}/secret/rotate` issues a new secret. For a short while FastCAC
signs with both the old and the new secret (the `webhook-signature` header carries both), so you
can deploy the new secret without dropping events.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.