Skip to main content
Do not poll for results. When a candidate finishes, Record sends you an event with their result record. Nothing is dropped: an event you do not confirm comes again.

Two ways to receive events

Use the webhook as the main path and the feed as the safety net. An event is confirmed once, by whichever way gets there first, and then stops coming from the other: a 2xx removes it from the feed, and a pull that confirms it cancels the webhook that was waiting. If you run both, let your pull wait about a minute before confirming new events, so the webhook gets its turn. Which one delivered it? The delivery log shows delivered_via: webhook or feed. Your URL is yours to choose. Register any HTTPS URL, such as https://ats.example.com/hooks/record or https://example.com/api/record-results. Record posts to exactly that URL.

How the pull loop works

Say 5 events are waiting. Your first pull returns all 5. If you never confirm them, the next pull returns the same 5 again. Once you pass their ids in ack, they are gone and you get only newer ones.
  1. Pull with ack: []. You get the events, each with an event_id.
  2. Save them. Skip an event_id you already saved.
  3. Pull again with those ids in ack. They are confirmed and the next events come back.
  4. Stop when a pull returns no events.
Confirm only what you really saved: that is what makes a lost response or a crash safe. Confirming is safe to repeat, and ids that are unknown or already confirmed are accepted.

When an event is sent

Once, when a candidate reaches completed, declined or failed and their result has stayed the same for about 45 seconds (the last questions are answered after the skill result exists, so Record waits instead of sending one event per answer). If a finished result later changes (a re-grade), you get a new event with a higher version for the same candidate.

What your server must do

  1. Verify the signature (below), then answer 2xx within 10 seconds. That is the acknowledgement. Process the event after answering.
  2. Keep the event_id of what you processed and skip repeats: delivery is at-least-once, so the same event can arrive twice.
  3. Keep the highest version per candidate; ignore an older one that arrives late.
  4. Any other answer, a timeout or a connection error means “not received”: Record retries.

Retries and nothing lost

Verify the signature

Each delivery has Record-Signature: t=<seconds>,v1=<hex>. Compute HMAC-SHA256 of <t>.<raw request body> with your API secret key (VFN_SK_…, the same one you sign your own calls with) and compare to v1. Reject if t is more than 5 minutes old. Nothing else to configure.
Your URL must be HTTPS on port 443 and reachable from the internet. The path is yours to choose. Redirects are not followed. You can have up to 5 endpoints.

Webhook endpoints

Create an endpoint

POST /webhooks/endpoints Register the HTTPS URL that receives result events. The path is up to you. Deliveries are signed with your API secret key. Scope: webhooks:manage · Idempotency-Key: optional
Save data.endpoint.endpoint_id for the next step. Can also return idempotency errors (IDEMPOTENCY_IN_PROGRESS, IDEMPOTENCY_KEY_REUSED) and the common errors. Full schema

List endpoints

GET /webhooks/endpoints Each endpoint with its status (active, paused, auto_paused), consecutive failures and last success. Scope: webhooks:manage
Can also return the common errors. Full schema

Get an endpoint

GET /webhooks/endpoints/{endpoint_id} One endpoint with its current status. Scope: webhooks:manage
Can also return the common errors. Full schema

Update an endpoint

PATCH /webhooks/endpoints/{endpoint_id} Send only what changes. status: paused holds deliveries (nothing is lost); status: active resumes them, oldest first. Use it to bring back an auto_paused endpoint. Scope: webhooks:manage · Idempotency-Key: optional
Can also return idempotency errors (IDEMPOTENCY_IN_PROGRESS, IDEMPOTENCY_KEY_REUSED) and the common errors. Full schema

Delete an endpoint

DELETE /webhooks/endpoints/{endpoint_id} Deliveries still waiting for it stop. The events stay in the feed for 30 days. Scope: webhooks:manage · Idempotency-Key: optional
Can also return idempotency errors (IDEMPOTENCY_IN_PROGRESS, IDEMPOTENCY_KEY_REUSED) and the common errors. Full schema

Send a test event

POST /webhooks/endpoints/{endpoint_id}/test Sends a signed webhook.test right now and shows what your server answered. Not stored, not retried. Scope: webhooks:manage
Can also return the common errors. Full schema

Backfill past results

POST /webhooks/endpoints/{endpoint_id}/backfill Queues the results since from (at most 30 days back) as normal deliveries. Safe to repeat. Scope: webhooks:manage · Idempotency-Key: optional
Can also return idempotency errors (IDEMPOTENCY_IN_PROGRESS, IDEMPOTENCY_KEY_REUSED) and the common errors. Full schema

List deliveries

GET /webhooks/endpoints/{endpoint_id}/deliveries Every send to this endpoint, newest first: attempts, last HTTP status or error, when the next retry is due. Scope: webhooks:manage
Can also return the common errors. Full schema

Retry a delivery

POST /webhooks/deliveries/{delivery_id}/retry For a dead one after you fixed your endpoint. Its attempts count from zero again. Scope: webhooks:manage · Idempotency-Key: optional
Can also return idempotency errors (IDEMPOTENCY_IN_PROGRESS, IDEMPOTENCY_KEY_REUSED) and the common errors. Full schema

Event feed

Pull and confirm events

POST /events/pull One call does both jobs. Send the ack ids of the events you already processed, and get the next events back. An event keeps coming back until you put its id in ack. Scope: events:read
Can also return the common errors. Full schema

Get an event

GET /events/{event_id} Fetch one event by its id. Scope: events:read
Can also return the common errors. Full schema