Skip to main content
Webhooks send results to your server as soon as they’re ready, so you don’t poll. Your team’s webhooks receive two kinds of delivery:
  • Verification events, for verifications your app starts with the mobile SDK: one platform.finished per platform, and one verification.finished at the end.
  • Session deliveries, for iframe sessions you create with the Partner API.
Each delivery is a 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.
You can also add a custom header key and value. Cr3dentials sends it with every delivery, which can help a request through your gateway. It doesn’t prove where a delivery came from: the signature does.
To get a real, signed delivery, run a test verification with your test key and zktls-demo, as in the iOS and Android quickstarts.

Check the signature

Every delivery carries a header like this:
  • t is the Unix time, in seconds, when this delivery attempt was signed.
  • v1 is the HMAC-SHA256 of t, a ., and the raw request body, keyed with your webhook’s signing secret (the whole string, whsec_ included), in lowercase hex.
Check every delivery before you trust it:
  1. Read the raw body bytes, before any JSON parsing. A body that was parsed and serialized again won’t match.
  2. Compute the HMAC-SHA256 of "<t>.<body>" with your secret, and compare it with v1 in constant time.
  3. Reject the delivery if t is more than 300 seconds away from your clock. That stops a captured delivery being replayed later.
  4. Only then parse the JSON.

Verification events

A verification your app starts with Zktls.verify() sends:
  • one platform.finished for each platform the user started, when its result reaches Cr3dentials or the run fails;
  • one verification.finished when every platform has ended: done, failed, or not started.
A platform the user never started sends no 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.
For 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}.
It usually arrives just after the last 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

  • reference is 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.
  • verificationId groups 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.finished only after every platform’s platform.finished, so it usually arrives last. But deliveries are sent in parallel and retried, so a platform.finished can still arrive after it. Process each platform.finished once per runId, and each verification.finished once per verificationId.
You can also read a verification at any time with GET /partner/verifications/{id}.

Session deliveries

A session you create with POST /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.
Acknowledge fast and do heavy work asynchronously, so you don’t hit the timeout.

Retries

A delivery that doesn’t get a 2xx 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

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.
Common causes: non-2xx responses, slow responses past the timeout, or TLS certificate problems. Inspect the logged status code and error for each attempt.
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.