Gareth Wilson Gareth Wilson

Guide to Footprint Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationThe Footprint dashboard, Webhooks page
Delivery platformSvix
Event identityevent_kind in the body
Signature headerssvix-id, svix-timestamp, svix-signature
SchemeHMAC-SHA256, base64, in v1, entries
Signed content{svix-id}.{svix-timestamp}.{raw body}
SecretOne per endpoint. Svix format is whsec_<base64>
SuccessAny 2xx
Live or sandboxis_live in every body
Retries, timeoutNot stated by Footprint
Source IPs44.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:

EventFires when
footprint.onboarding.completedA user's onboarding onto a playbook reaches a terminal status
footprint.onboarding.incompleteAn onboarding started with POST /onboardings still has unmet requirements
footprint.onboarding.status_changedA user's status changes after onboarding, manual review or internal updates. Deprecated
footprint.user.manual_reviewA dashboard user changes the user's status during manual review
footprint.user.info_requestedA dashboard user asks the user for more information during review
footprint.watchlist_check.completedA watchlist check has run for the user
footprint.user.bank_link_updatedA user's bank link is updated
footprint.vault.updatedA user or business vault is updated
footprint.entity.entity_status_setA 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:

{
  "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.

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

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.

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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.