# Footprint Webhooks

Footprint (identity verification and KYC) delivers webhooks through Svix.
Every delivery is an HTTP `POST` with a JSON body and three signature headers:
`svix-id`, `svix-timestamp`, and `svix-signature`. The event name is the `event_kind`
field in the body. Footprint's own pages point to Svix's guides for receiving and verifying;
its event catalog is hosted on svix.com.

## When to Use This Skill

* How do I receive Footprint webhooks?
* How do I verify Footprint webhook signatures (`svix-signature`)?
* How do I handle `footprint.onboarding.completed` when a user's status was `pending`?
* How do I react to `footprint.watchlist_check.completed` (continuous AML monitoring)?
* Why is my Footprint webhook signature verification failing, or rejecting Hookdeck retries?

## Verification (core)

Signed content is `{svix-id}.{svix-timestamp}.{raw body}`. Svix signs it with HMAC-SHA256
using the base64-decoded secret (the part after `whsec_`), base64-encodes the result, and
sends one or more space-delimited `v1,<signature>` entries. Footprint recommends Svix's
verification guide and the official `svix` library, which compares signatures in constant
time.

```javascript
import { Webhook } from 'svix'; // svix 2.x is ESM-only

const wh = new Webhook(process.env.FOOTPRINT_WEBHOOK_SECRET); // "whsec_..."
// rawBody: the raw Buffer/string, never re-serialized JSON
wh.verify(rawBody, {
  'svix-id': req.headers['svix-id'],
  'svix-timestamp': req.headers['svix-timestamp'],
  'svix-signature': req.headers['svix-signature'],
}); // throws on a bad signature or a timestamp outside the window
const event = JSON.parse(rawBody.toString('utf8')); // parse only after verify passes

```

```python
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["FOOTPRINT_WEBHOOK_SECRET"])
wh.verify(raw_body, {"svix-id": msg_id, "svix-timestamp": ts, "svix-signature": sig})

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/footprint-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/footprint-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/footprint-webhooks/examples/fastapi/).

**Secret format is unconfirmed on Footprint's side.** `whsec_` is Svix's secret format.
Footprint's webhook pages never show the secret, so treat the `whsec_` prefix as an
assumption from Svix and check the value you get from your Footprint dashboard.

### Hookdeck retries and replays hit the 5-minute window

The `svix` and `standardwebhooks` libraries always enforce a fixed 5-minute timestamp window,
and it can't be configured. Hookdeck retries and replays resend the **original**
`svix-timestamp`, so the library rejects any retry or replay that happens more than 5 minutes
after the original delivery. If you verify behind Hookdeck (or replay old events), use the
manual verifier in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/footprint-webhooks/references/verification.md), where the window
can be dropped or widened. Deduplicate on `svix-id` instead of relying on the timestamp.

## Common Event Types

The event name is `event_kind` in the JSON body.

| Event | Triggered When |
| --- | --- |
| `footprint.onboarding.completed` | A user's onboarding reaches a terminal status (`pass`, `fail`, `none`). Footprint recommends subscribing to this one |
| `footprint.onboarding.incomplete` | An onboarding started via `POST /onboardings` still has unmet requirements |
| `footprint.onboarding.status_changed` | A user's status changed after onboarding. **Deprecated** in favor of `onboarding.completed` and `user.manual_review` |
| `footprint.watchlist_check.completed` | A watchlist check ran for a user (needs continuous monitoring on the playbook) |
| `footprint.user.manual_review` | A dashboard user changed the user's status during manual review |
| `footprint.user.info_requested` | A dashboard user asked the user for more information during review |
| `footprint.user.bank_link_updated` | A user's bank link was updated |
| `footprint.vault.updated` | A user or business vault was updated |
| `footprint.entity.entity_status_set` | A custom status was set, updated, or cleared on a user or business |

See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/footprint-webhooks/references/overview.md) for payload fields.

## Environment Variables

```bash
# Signing secret of the webhook endpoint (Svix format, starts with whsec_)
FOOTPRINT_WEBHOOK_SECRET=whsec_xxxxx

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 footprint --path /webhooks/footprint

```

Register the tunnel URL as an endpoint in the Footprint dashboard, then complete a test
onboarding in sandbox mode to trigger events.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/footprint-webhooks/references/overview.md) - Footprint webhook concepts, events, payloads, IP allowlist
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/footprint-webhooks/references/setup.md) - Register an endpoint and get the signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/footprint-webhooks/references/verification.md) - Svix verification details, manual verifier, timestamp window