Webhooks
Rather than polling, register an HTTPS endpoint and we will POST to it when something happens. Available on Professional and above, under Integrations → Webhooks.
Events
| Event | Fires when |
|---|---|
test.invitation.created | An invitation is sent |
test.invitation.opened | A candidate opens their link |
test.invitation.expired | An invitation passes its expiry |
test.attempt.started | A candidate begins an assessment |
test.attempt.submitted | A candidate submits |
test.attempt.evaluated | Scoring finished, including any manual marking |
test.attempt.flagged_for_review | An attempt was flagged for a human to look at |
interview.scheduled | An interview is created |
interview.started | An interview room opens |
interview.completed | An interview ends |
interview.status_changed | An interview's status changes |
subscription.tier_changed | Your own plan changes |
Subscribe an endpoint to only the events you handle. This list is part of the API contract: new events may be added, but existing ones are not renamed or removed without a deprecation period.
If you want a candidate's final score, test.attempt.evaluated is the one to use — submitted fires before marking is complete.
Verifying the signature
Every request carries a Tu-Signature header:
Tu-Signature: t=1755590400,v1=5257a869e7...
v1 is an HMAC-SHA256, keyed with your endpoint's signing secret, over the string <timestamp>.<raw request body>. To verify:
- Parse
tandv1out of the header. - Reject the request if
tis more than 5 minutes from your clock, in either direction. - Compute
HMAC-SHA256(secret, "<t>.<body>")over the raw body — the exact bytes received. - Compare in constant time against
v1.
GOTCHA: sign the raw bytes, not a re-serialized object. Parsing the JSON and re-encoding it changes whitespace and key order, and the signature will never match. Capture the raw body before your framework parses it.
Reject anything that fails. An unverified webhook endpoint is an unauthenticated write API into your systems, and its URL is not a secret.
Rotating the signing secret
Rotation issues a second secret while the old one stays valid for a grace period, and both are accepted during it. Deploy the new secret, confirm deliveries still verify, then finish the rotation.
Retries
A delivery is retried on any network error, timeout, or 5xx response, on this schedule:
1 minute → 5 minutes → 30 minutes → 2 hours → 6 hours → 24 hours
That is six attempts across more than three days, so a receiver that is down overnight recovers without anyone intervening. After the last one the delivery is dead-lettered and not retried again.
A 4xx other than 429 is treated as permanent — you are rejecting the request and retrying will not help.
After 20 consecutive failures the endpoint is disabled automatically and must be re-enabled by hand once the receiver is fixed. This stops a decommissioned URL absorbing deliveries indefinitely.
Building a receiver that behaves
- Respond fast, then work. Acknowledge with a
2xxas soon as you have verified and stored the event; do the processing afterwards. A slow receiver times out and gets retried, which turns one event into several. - Deduplicate on
event_id. Delivery is at-least-once. A retry after a timeout can arrive for an event you already handled successfully. - Do not assume order. Retries mean a later event can arrive before an earlier one.
Your endpoint must be publicly reachable
We block requests to private and loopback address ranges, so localhost and internal IPs cannot be registered — that restriction protects both of us from server-side request forgery.
For local development, use a tunnelling service to expose your machine on a public HTTPS URL.