# Guide to Footprint Webhooks: Features and Best Practices

Footprint webhooks tell your backend when an identity decision changes: a user finishes onboarding, a reviewer overrides a status, a watchlist check comes back, or a user's vault is updated. If you verify customers or businesses with Footprint, the identity verification and data vault platform, webhooks are how those decisions reach your account opening, compliance and risk systems without polling.

This guide covers how Footprint delivers and signs webhooks, the events it sends and what they contain, the gaps in its documentation, and the practices that keep a Footprint receiver reliable.

## What are Footprint webhooks?

A Footprint webhook is a `POST` to an endpoint you register in the Footprint dashboard. Footprint sends each event over HTTPS as a JSON payload, and your endpoint returns a `2xx` to confirm it processed the event.

Footprint delivers its webhooks through Svix. Its documentation hands the receiving side to Svix's guides, including how to verify signatures, and its event catalogue is a page on svix.com. In practice, that means every request carries Svix's three headers:

* `svix-id`, the message ID, which stays the same if the message is sent again
* `svix-timestamp`, the send time in Unix seconds
* `svix-signature`, one or more space-separated `v1,<signature>` entries

Each signature is a base64 HMAC-SHA256 of `{svix-id}.{svix-timestamp}.{raw body}`, keyed with the bytes of your endpoint's signing secret. Svix secrets look like `whsec_` followed by base64, and the key is the base64 part decoded. Footprint's own pages never show the secret, so check the value in your dashboard before assuming that format.

## Footprint webhook features

| Feature | Details |
| --- | --- |
| Configuration | The Footprint dashboard, **Webhooks** page |
| Delivery platform | Svix |
| Event identity | `event_kind` in the body |
| Signature headers | `svix-id`, `svix-timestamp`, `svix-signature` |
| Scheme | HMAC-SHA256, base64, in `v1,` entries |
| Signed content | `{svix-id}.{svix-timestamp}.{raw body}` |
| Secret | One per endpoint. Svix format is `whsec_<base64>` |
| Success | Any `2xx` |
| Live or sandbox | `is_live` in every body |
| Retries, timeout | Not stated by Footprint |
| Source IPs | `44.228.126.217`, `50.112.21.217`, `52.24.126.164`, `54.148.139.208`, `2600:1f24:64:8000::/56` |

Footprint says every webhook request originates from one of those addresses, so you can allow them through a firewall.

## Common events

Footprint's catalogue lists nine event types:

| Event | Fires when |
| --- | --- |
| `footprint.onboarding.completed` | A user's onboarding onto a playbook reaches a terminal status |
| `footprint.onboarding.incomplete` | An onboarding started with `POST /onboardings` still has unmet requirements |
| `footprint.onboarding.status_changed` | A user's status changes after onboarding, manual review or internal updates. Deprecated |
| `footprint.user.manual_review` | A dashboard user changes the user's status during manual review |
| `footprint.user.info_requested` | A dashboard user asks the user for more information during review |
| `footprint.watchlist_check.completed` | A watchlist check has run for the user |
| `footprint.user.bank_link_updated` | A user's bank link is updated |
| `footprint.vault.updated` | A user or business vault is updated |
| `footprint.entity.entity_status_set` | A custom status is set, updated or cleared on a user or business |

For integrations built on Footprint's frontend SDKs, Footprint recommends three of these. `footprint.onboarding.completed` carries the final decision when the SDK returned `pending` because a verification vendor took longer. `footprint.user.manual_review` tells you a reviewer changed a status, after which Footprint recommends calling `GET /users/{fp_id}` for the new one. `footprint.watchlist_check.completed` reports the AML watchlist checks that run when a playbook has continuous monitoring enabled.

`footprint.onboarding.status_changed` is deprecated in favour of `onboarding.completed` and `user.manual_review`. Don't build new logic on it.

Bodies are flat. Every one carries `event_kind`, `fp_id`, `is_live` and `timestamp`, and each type adds its own fields. Here is the catalogue's `footprint.onboarding.completed` example:

```json
{
  "event_kind": "footprint.onboarding.completed",
  "external_id": null,
  "fp_id": "fp_id_xyz",
  "is_live": false,
  "onboarding_external_id": null,
  "onboarding_id": "ob_SRFT2a1mN7DAWJ0VPXkiqK",
  "playbook_key": "pb_test_QoEYTOve49Q2IAmaKVYnPs",
  "playbook_version": 3,
  "requires_manual_review": false,
  "status": "pass",
  "timestamp": "2024-06-14T02:40:00Z"
}

```

`fp_id` is Footprint's ID for the user, and `external_id` is yours if you set one. `is_live` is `false` for sandbox users. `status` is the decision, such as `pass`, `fail` or `none`. The manual review and info requested events carry only the user's IDs, which is why Footprint tells you to fetch the user afterwards. The body has no event ID of its own, so `svix-id` is the key for deduplication.

## Setting up Footprint webhooks

1. In the Footprint dashboard, open **Webhooks** and add an endpoint with your HTTPS URL.
2. Choose the event types to send. For an SDK integration, start with the three Footprint recommends.
3. Copy the endpoint's signing secret and store it server-side, for example as `FOOTPRINT_WEBHOOK_SECRET`.
4. Complete a sandbox onboarding to trigger events, and check that `is_live` is `false`.

If you use Hookdeck, create a Footprint source, set the endpoint's signing secret as its **Webhook Signing Secret**, and use the source URL as the endpoint URL in Footprint. Hookdeck reads the `svix-` headers, and falls back to the `webhook-` names of the Standard Webhooks spec if those are what arrive.

Hookdeck doesn't verify Footprint signatures at the edge. It accepts the request and answers Footprint with `200`, then checks the signature and records a mismatch as failed verification instead of delivering it to your service. It applies no timestamp window, so a retry or replay of an old event still verifies.

## Securing Footprint webhooks

Build the signed content from the `svix-id` header, a dot, the `svix-timestamp` header, a dot and the raw body. Compute an HMAC-SHA256 with the decoded secret, base64-encode it, and accept the request if it matches any `v1,` entry in `svix-signature`. During secret rotation there can be more than one entry.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyFootprintSignature(rawBody, headers, secret) {
  const svix_id = headers['svix-id'];
  const svix_timestamp = headers['svix-timestamp'];
  const svix_signature = headers['svix-signature'];
  if (!svix_id || !svix_timestamp || !svix_signature || !secret) return false;

  // whsec_<base64>: the key is the decoded bytes after the prefix.
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = crypto
    .createHmac('sha256', key)
    .update(`${svix_id}.${svix_timestamp}.`)
    .update(rawBody)
    .digest('base64');
  const computed = Buffer.from(expected, 'utf8');

  return svix_signature.split(' ').some((entry) => {
    const [version, signature] = entry.split(',');
    if (version !== 'v1' || !signature) return false;
    const received = Buffer.from(signature, 'utf8');
    return received.length === computed.length && crypto.timingSafeEqual(received, computed);
  });
}

const app = express();

app.post('/webhooks/footprint', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.FOOTPRINT_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyFootprintSignature(req.body, req.headers, secret)) {
    return res.sendStatus(401);
  }

  // Optional: reject old svix-timestamp values here. Off by default, because
  // retries and replays resend the original timestamp.
  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on svix-id, route on event.event_kind, check event.is_live.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import base64
import hashlib
import hmac

def verify_footprint_signature(raw_body: bytes, svix_id, svix_timestamp, svix_signature, secret: str) -> bool:
    if not svix_id or not svix_timestamp or not svix_signature or not secret:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed_content = f"{svix_id}.{svix_timestamp}.".encode("utf-8") + raw_body
    expected = base64.b64encode(hmac.new(key, signed_content, hashlib.sha256).digest())
    for entry in svix_signature.split(" "):
        version, _, signature = entry.partition(",")
        if version == "v1" and hmac.compare_digest(signature.encode("utf-8"), expected):
            return True
    return False

# In FastAPI: raw_body = await request.body()
# verify_footprint_signature(raw_body, request.headers.get("svix-id"),
#     request.headers.get("svix-timestamp"), request.headers.get("svix-signature"),
#     os.environ["FOOTPRINT_WEBHOOK_SECRET"])

```

You can also use Svix's official `svix` library, which Svix's guide recommends: `new Webhook(secret).verify(rawBody, headers)` in Node, or `Webhook(secret).verify(raw_body, headers)` in Python. Be aware that the library always rejects a `svix-timestamp` more than 5 minutes from the current time, and the window can't be turned off. If you verify behind Hookdeck, its retries and replays carry the original timestamp, so a replay of an old event through Hookdeck fails the library's check even though the signature is valid. The manual verifier above has no such window.

> **Make Footprint webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks Footprint's Svix signatures, answers each delivery straight away, and durably queues every onboarding and review event before your service sees it.

## Footprint webhook limitations and pain points

### Verification lives in someone else's documentation

**The Problem:** Teams hunt through Footprint's docs for the signing scheme and the secret format and don't find them.

**Why It Happens:** Footprint delegates both receiving and verifying to Svix's guides. The scheme, the header names and the `whsec_` secret format are Svix's, and Footprint's own pages show none of them.

**Workarounds:**

* Read Svix's verification guide alongside Footprint's webhook page.
* Check that the secret in your dashboard starts with `whsec_` before you decode it. If it doesn't, confirm the format with Footprint.

**How Hookdeck Can Help:** Hookdeck's Footprint source implements the Svix scheme, including the `whsec_` decoding and multiple `v1` entries, so your services don't need to.

### Library timestamp windows break replays

**The Problem:** Verification passes for live deliveries but fails when you retry or replay an event later.

**Why It Happens:** The `svix` library rejects any `svix-timestamp` more than 5 minutes old. A retry or replay from Hookdeck resends the original headers, so after 5 minutes the timestamp is "too old" even though the signature is still valid.

**Workarounds:**

* Verify manually, as above, and drop the timestamp check or widen it.
* Rely on deduplicating `svix-id` for replay protection.

**How Hookdeck Can Help:** Hookdeck verifies the signature once, without a timestamp window, and lets you replay any stored event to your service.

### No published retry policy

**The Problem:** You don't know how long Footprint keeps trying after your endpoint fails.

**Why It Happens:** Footprint documents that a `2xx` confirms an event but states no timeout, retry schedule or disabling policy. Svix has its own defaults, but Footprint doesn't say they apply.

**Workarounds:**

* Respond quickly and process asynchronously.
* After an outage, re-read the users who were onboarding at the time through the Footprint API.

**How Hookdeck Can Help:** Hookdeck accepts each event as soon as it arrives and stores it, then retries delivery to your service on a schedule you control.

### Thin review events

**The Problem:** A `footprint.user.manual_review` event tells you something changed but not what.

**Why It Happens:** The manual review and info requested events carry the user's IDs and a timestamp, not the new status.

**Workarounds:**

* Call `GET /users/{fp_id}` when they arrive, as Footprint recommends.
* Queue the lookup rather than making it before you respond.

**How Hookdeck Can Help:** Hookdeck can route these events to a dedicated worker that performs the lookup, separately from events that already carry their decision.

## Best practices

### Verify against the raw body

The HMAC covers the exact bytes Footprint sent. In Express, use `express.raw({ type: '*/*' })` and parse after verification. In Next.js, read `await request.text()`. In FastAPI, read `await request.body()`.

### Deduplicate on `svix-id`

The body has no event ID, and `svix-id` stays the same when a message is resent. Store it before processing. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Separate live and sandbox

Check `is_live` on every event. If sandbox and live endpoints ever share code, make sure a sandbox `pass` can't open a real account.

### Move off `status_changed`

If you still subscribe to `footprint.onboarding.status_changed`, replace it with `footprint.onboarding.completed` and `footprint.user.manual_review`, which Footprint recommends instead.

### Acknowledge fast and process asynchronously

Verify, store the event, return `200`, and do follow-up work such as user lookups afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Footprint delivers webhooks through Svix, so every request carries `svix-id`, `svix-timestamp` and `svix-signature`, and verification is an HMAC-SHA256 of the ID, timestamp and raw body with your decoded endpoint secret. Verify against the raw bytes, think twice before using a library with a fixed 5-minute window, deduplicate on `svix-id`, route on `event_kind`, check `is_live`, and fetch the user after manual review events.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks Footprint's signatures, and holds each event until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Footprint webhooks reliably in minutes.