- Verification events, for verifications your app starts with the mobile SDK: one
platform.finishedper platform, and oneverification.finishedat the end. - Session deliveries, for iframe sessions you create with the Partner API.
POST with a JSON body, signed with your webhook’s secret in the Zktls-Signature header. Every active webhook on your team receives every delivery, test and live.
Add a webhook
1
Open Webhooks
Log into the Partner Portal and click Webhooks in the sidebar.
2
Create a webhook
Click Create Webhook and enter your endpoint URL (
https://your-domain.com/webhook). It must be HTTPS.3
Get its signing secret
Each webhook has its own signing secret:
whsec_ followed by 64 hex characters. Until the Partner Portal shows it, we send it to you: with your app keys when we register your app. For a webhook you add after that, ask us for its secret. Keep it on your server, like a password.4
Tune delivery (optional)
Set the response timeout and retry attempts, then toggle the webhook Active.
Check the signature
Every delivery carries a header like this:tis the Unix time, in seconds, when this delivery attempt was signed.v1is the HMAC-SHA256 oft, a., and the raw request body, keyed with your webhook’s signing secret (the whole string,whsec_included), in lowercase hex.
- Read the raw body bytes, before any JSON parsing. A body that was parsed and serialized again won’t match.
- Compute the HMAC-SHA256 of
"<t>.<body>"with your secret, and compare it withv1in constant time. - Reject the delivery if
tis more than 300 seconds away from your clock. That stops a captured delivery being replayed later. - Only then parse the JSON.
Verification events
A verification your app starts withZktls.verify() sends:
- one
platform.finishedfor each platform the user started, when its result reaches Cr3dentials or the run fails; - one
verification.finishedwhen every platform has ended: done, failed, or not started.
platform.finished. verification.finished lists it as not_started.
Each event is delivered on its own, so verification.finished can arrive before a platform.finished of the same verification. Match events by verificationId and runId, not by the order they arrive in.
Both events carry your reference, unchanged, and "test": true when the verification was started with a test key.
The examples below are a test verification of zktls-demo. A live verification sends the same events with "test": false.
platform.finished
string
platform.finished.boolean
true for a verification started with a test key.string
The verification. Your app gets the same id in its
VerifyOutcome.string
The reference your app passed to
verify(), or your server put in the token.string
The platform id, such as
zktls-demo.string
This platform’s run within the verification. Use it to recognise a delivery you have already processed.
string
done or failed.boolean | null
true when the platform’s read was complete. false when a size cap ended it first, and result then holds what was collected. null when the run failed.object | null
The platform’s attested figures. Each platform has its own fields.
null when the run failed.string | null
The attester’s signed claim, in base64. Cr3dentials checked it before sending this webhook. Keep it if you want the attester’s own signed record of the result.
null when there is none.object | null
When the run failed,
{"code": "…", "message": "…"}. For example, no_result means the run was handed to an attester but its result never arrived. null otherwise.string
When the run ended, in ISO 8601.
zktls-demo, result is the demo bank’s latest transfer. The transfer’s own reference is the demo bank’s, not yours.
verification.finished
string
verification.finished.boolean
true for a verification started with a test key.string
The verification.
string
Your reference.
object[]
Every platform of the verification, in the order your app passed them, each with its
platform, runId and status: done, failed or not_started.string
When the verification finished, in ISO 8601: once every platform had ended and its
platform.finished was on its way. The same value as finishedAt in GET /partner/verifications/{id}.platform.finished. If your app was closed in the middle of the flow, the platforms it never reached end as not_started about 2 hours after the verification started, and verification.finished arrives then.
Test verifications
A verification started with a test key carries"test": true in both events. It goes to the same webhooks as live ones, so check test before you act on a result. Test verifications run only zktls-demo.
Matching results with your users
referenceis your own value: use it to find the user or the order. Choose one your server already knows, so a result matches even if your app was killed in the middle of the flow.verificationIdgroups the events of one verification. A user who finishes later starts a new verification, with a new id and, if you pass it again, the same reference.- Deliveries can arrive more than once, and out of order. We queue
verification.finishedonly after every platform’splatform.finished, so it usually arrives last. But deliveries are sent in parallel and retried, so aplatform.finishedcan still arrive after it. Process eachplatform.finishedonce perrunId, and eachverification.finishedonce perverificationId.
GET /partner/verifications/{id}.
Session deliveries
A session you create withPOST /partner/browser-session sends one delivery when it ends COMPLETED or ERROR. It has no event field, which tells it apart from verification events, and it is signed the same way.
string
The session this delivery is for.
string | null
The session’s group, if it has one.
string
COMPLETED or ERROR.number
Platform the session verified against.
string | null
Your reference ID from session creation, if you provided one.
object | null
Verified account data, present when verification completed.
string | null
Opaque reference to the cryptographic attestation (SHA-256). The raw attestation is never sent.
string | null
ISO 8601 completion timestamp.
string | null
Why the session failed, when
status is ERROR.object | null
Non-sensitive session metadata. Internal fields are stripped before delivery.
Requirements
Your endpoint must:Check the
Zktls-Signature header before trusting a delivery.Respond with a
2xx status code.Respond within the configured timeout (default 30 seconds).
Handle duplicate deliveries idempotently: key off
runId for platform.finished, verificationId for verification.finished, and sessionId for session deliveries.Retries
A delivery that doesn’t get a2xx in time is tried again with exponential backoff (after 5 s, then 15 s, 35 s and so on), up to the webhook’s retry attempts in all (default 3: the first delivery and two retries). Each retry sends the same body, signed again with a new t, so check it like any other delivery. After the last attempt the delivery isn’t sent again: read the verification with GET /partner/verifications/{id}. Review delivery history and failures under Webhooks → Logs in the portal.
Troubleshooting
Not receiving notifications
Not receiving notifications
Confirm the webhook is Active, the URL is reachable over HTTPS with a valid certificate, and your server returns a
2xx. Check the delivery logs in the portal.Deliveries failing
Deliveries failing
Common causes: non-2xx responses, slow responses past the timeout, or TLS certificate problems. Inspect the logged status code and error for each attempt.
The signature doesn't match
The signature doesn't match
Sign the raw body bytes exactly as received, not JSON your framework parsed and serialized again. Use the whole secret,
whsec_ included, and the secret of this webhook: each webhook has its own. If only the time check fails, set your server’s clock by NTP.