Skip to main content
Instead of polling, you can have Sahl post an event to your server when a call finishes. Webhooks are optional. Every KYC event is sent after the call’s database commit, in the background, so your API response never waits for your endpoint and never fails because of it.

Register an endpoint

In the console open Settings, then Webhooks, then Add Webhook.
  1. Enter the Endpoint URL. It must be https and resolve to a public address. Private, loopback and cloud metadata addresses are refused, both when you save and again before every send. Redirects are not followed.
  2. Choose the events (below).
  3. Save. If you did not supply your own secret (16 characters or more), Sahl generates one, shown once, in the form whsec_ followed by 48 characters. Copy it.
  4. Click Test on the endpoint. Sahl posts a test.ping event to your URL, signed like a real delivery, and shows the status your server answered. The test payload is not shaped like a KYC event (see Test ping).
Endpoint management needs the tenant admin or API manager role. Webhook endpoints are managed in the console, not through the Partner API.

Events

Only calls that carry a reference emit events, because an event names a case. A call without a reference stores nothing and emits nothing. Events are sent for both environments. The payload says which.

Payloads

Every payload has these four keys. Payloads carry ids, your reference, the environment and a verdict summary. They never carry a field value, a name, a date of birth or any document content: deliveries are stored on Sahl’s side and sent to a URL you typed in.

kyc.documents_read

failed_checks holds the ids of failed checks with severity critical or warning, without duplicates.

kyc.case_verified and kyc.case_assessed

kyc.eid_completed

complete is false when the request ended archived without the client finishing. failed_checks holds the failed critical eID check ids.

Test ping

The Test button sends a different shape, signed the same way:
Its header X-Sahl-Event is test.ping. It has no event or event_id key, so route on the header, not on a key. It carries no X-Sahl-Delivery header, waits 10 seconds for your answer, and counts any status below 400 as a success (real deliveries need a 2xx and wait 30 seconds).

Request headers

Verify the signature

  1. Read the raw body bytes before you parse the JSON. Re-serialised JSON does not match.
  2. Compute HMAC-SHA256(secret, timestamp + "." + body) and compare with X-Sahl-Signature-V2 in constant time.
  3. Reject a timestamp more than a few minutes from your clock (the code below uses 5 minutes, which is your choice, not a Sahl rule). Each retry is signed again, so its timestamp is fresh.
A receiver in Express. express.raw keeps the bytes.
Both functions above were run against the code that signs real deliveries: a valid delivery verifies, a changed body fails, and a stale timestamp fails.

Delivery and retries

The first attempt is made right after the call. Later attempts are made by a retry job that runs every few minutes, so a retry can come slightly after its scheduled time. An endpoint that you switch off keeps its due deliveries and resumes them when you switch it back on. Make your handler idempotent. De-duplicate on X-Sahl-Delivery (one id per delivery) or on event_id (one per event). Answer fast with a 2xx and do the work after. The console shows every delivery of an endpoint with its HTTP status and attempt count, and keeps up to 2,000 characters of your response body.

Troubleshooting