Register an endpoint
In the console open Settings, then Webhooks, then Add Webhook.- Enter the Endpoint URL. It must be
httpsand 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. - Choose the events (below).
- 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. - Click Test on the endpoint. Sahl posts a
test.pingevent 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).
Events
Only calls that carry areference 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: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
- Read the raw body bytes before you parse the JSON. Re-serialised JSON does not match.
- Compute
HMAC-SHA256(secret, timestamp + "." + body)and compare withX-Sahl-Signature-V2in constant time. - 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.
express.raw keeps the bytes.
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.