POST to the workspace’s webhook URL, set in the console on the workspace’s page. The full request, field by field, is on Decision webhook.
When webhooks are sent
A webhook is sent each time a verification moves to one of these statuses:
One verification can therefore send several webhooks, for example
resubmission_requested and then approved. Handle every status, and ignore those you do not act on.
Payload
reasonis a decision reason when the status isdeclinedorresubmission_requested, andnullotherwise, with one exception: when a facial age estimation can’t confirm 18+ and asks the person for an ID, the webhook isresubmission_requestedwithreason: null.GET /v1/verifications/{id}reports that verification asdocuments_required.- When duplicate detection found the same face behind another verification, the payload also has
duplicate_detected: true,duplicate_countandduplicate_of(verification_id,external_id). - When someone on your team approved or declined the verification by hand in the console, it has
manual_moderation: the action, the reason they gave, and who did it. documentis on every webhook, whatever the status. It is the same objectGET /v1/verifications/{id}/documentreturns, withoutmediaandmeta, read from the attempt that decided the verification:type,issuing_countryandfields.issuing_subdivisionis the state or province that issued the document, as a bare code besideissuing_country(for exampleFLwithUS), ornull; today it is filled for US driving licences and ID cards, it is on every workspace, and it is kept after erasure. Identity (KYC) workspaces receive eleven fields (addressis the printed text as read, not parsed, and may contain line breaks); age verification workspaces receivefirst_name,last_name,date_of_birthanddocument_numberonly, and the other seven keys are absent.nationalityis published only when the document itself states it. The examples above show a KYC document ondeclined, an age workspace onapproved, and the empty object elsewhere.nullmeans the field was not read, is not printed on that document, or no document was read at all: a facial age estimation that passed without an ID, a test workspace (nothing is analysed), or a wallet check.documentitself is nevernull. Dates areYYYY-MM-DD, countries are ISO 3166-1 alpha-2, andtypeandgenderare open enums; see Reading results.- A resend, or a manual retry from the console or the MCP server, carries the document as it is now; an automatic retry sends the body as it was first sent. After the person’s data is erased, a resend carries only
typeandissuing_country, and every field isnull. - The webhook has no images. Fetch them with your key, as described in Reading results.
- An optional
fingerprint_signalsobject carries device and network signals. Treat it as informational; its fields may change.
Headers
Verifying the signature
Verify every webhook before trusting it: anyone canPOST to your URL.
- Read the raw request body. Do not parse and re-serialise it: the signature covers the exact bytes.
- Reject the request if
X-Timestampis more than 300 seconds away from your clock. This stops an old, captured request from being replayed. - Compute the hex HMAC-SHA256 of
X-Timestamp + "." + raw bodywith the workspace’s active secret key. - Compare it with
X-HMAC-Signaturein constant time.
Responding and retries
Answer with any2xx as soon as the signature checks out, and do the work in the background.
Redirects are followed, and the final response is the one that counts.
Every attempt, with your response code and body, is listed in the console under the workspace’s webhooks, where you can also send a delivery again. The MCP tools
list-webhook-deliveries, retry-webhook-delivery and resend-webhook-delivery do the same.
Duplicates and order
- A retried delivery keeps its
X-ProofAge-Webhook-Delivery-Id. Store the IDs you have processed and skip one you have seen. - A delivery sent again by hand is a new delivery with a new ID, so also make the processing itself idempotent: applying
approvedto a user who is already approved changes nothing. - Retries mean webhooks can arrive out of order. Compare
timestampwith the last one you applied for that verification, or fetch the current state withGET /v1/verifications/{id}before acting.
Receiving webhooks locally
Your development machine is not reachable from the internet, so ProofAge cannot deliver webhooks tolocalhost. Two ways to receive them while you build:
- A tunnel. Expose your local handler with a tunnel such as ngrok or Cloudflare Tunnel and set the tunnel’s HTTPS URL as the test workspace’s webhook URL.
- Replay. Leave the webhook URL pointing anywhere, then use the MCP tool
get-webhook-delivery-request: it returns the exact request ProofAge sends, signed with the active secret key, ready to replay againstlocalhostwith curl. Send the body byte for byte; a re-serialised body fails the signature check.
Before going live
- Verify the signature and the timestamp on every request, in production too.
- Serve the webhook URL over HTTPS.
- Handle all six statuses.
- Log the delivery ID, the status and your response, so you can match them with the console’s delivery log.