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 againsvix-timestamp, the send time in Unix secondssvix-signature, one or more space-separatedv1,<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_ | A user's status changes after onboarding, manual review or internal updates. Deprecated |
footprint.user.manual_ | A dashboard user changes the user's status during manual review |
footprint.user.info_ | A dashboard user asks the user for more information during review |
footprint.watchlist_ | A watchlist check has run for the user |
footprint.user.bank_ | A user's bank link is updated |
footprint.vault.updated | A user or business vault is updated |
footprint.entity.entity_ | 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_ tells you a reviewer changed a status, after which Footprint recommends calling GET /users/{fp_id} for the new one. footprint.watchlist_ reports the AML watchlist checks that run when a playbook has continuous monitoring enabled.
footprint.onboarding.status_ 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:
{
"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
- In the Footprint dashboard, open Webhooks and add an endpoint with your HTTPS URL.
- Choose the event types to send. For an SDK integration, start with the three Footprint recommends.
- Copy the endpoint's signing secret and store it server-side, for example as
FOOTPRINT_WEBHOOK_SECRET. - Complete a sandbox onboarding to trigger events, and check that
is_liveisfalse.
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.
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:
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_ 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 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-idfor 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_ 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.
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_, replace it with footprint.onboarding.completed and footprint.user.manual_, 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.
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 sits in front of your receiver, checks Footprint's signatures, and holds each event until your service has processed it.
Get started with Hookdeck for free and handle Footprint webhooks reliably in minutes.