Coding agent documentation index Fetch the complete documentation index at: https://docs.worklittle.com/docs-agent-manifest.json Use this file to discover all available pages before exploring further.

Webhooks reliability

Signature verification, at-least-once delivery, idempotency keys, and the reconciliation sweep that keeps an ATS mirror correct when a delivery is missed.

Assume every event can arrive twice and any event can fail to arrive. Both assumptions are cheap to design for and expensive to retrofit.

How delivery works

You register an endpoint URL. When an event fires, Worklittle POSTs JSON and records the attempt in the delivery log. Failed deliveries retry with backoff.

{
  "id": "wh_…",
  "type": "candidate.application_submitted",
  "created_at": 1748948400,
  "data": { }
}

created_at is Unix seconds, not an ISO string. Manage endpoints via /webhooks REST routes or the MCP tools list_webhooks, create_webhook, update_webhook, rotate_webhook_secret, test_webhook, and list_webhook_deliveries. Subscribe to "*" for everything, or pick specific types. Full list: Webhooks.

Verify before you trust

Three headers accompany every delivery.

| Header | Purpose |
| --- | --- |
| `Worklittle-Webhook-Id` | Unique delivery id, use it to dedupe |
| `Worklittle-Webhook-Timestamp` | Unix timestamp, use it to reject replays |
| `Worklittle-Webhook-Signature` | `v1=<hmac>` computed over `timestamp.body` |
import crypto from "node:crypto";

function verify(sigHeader, timestamp, rawBody, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const received = sigHeader.replace(/^v1=/, "");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}

Read the raw body before any JSON parsing, because re-serialized JSON will not match the signature. Reject deliveries whose timestamp is more than a few minutes old. Rotate the secret with rotate_webhook_secret and accept both the old and new secret during the rollover window.

Idempotency

Delivery is at-least-once, so your handler must be safe to run twice on the same event.

Dedupe on the delivery id. Store Worklittle-Webhook-Id in a table with a unique constraint and a retention window covering the retry horizon. If the insert conflicts, acknowledge with 200 and stop.

Make writes idempotent anyway. Deduplication is a fast path, not a guarantee, especially across concurrent workers. Prefer upserts keyed on a stable business id over blind inserts, and make state transitions declarative. "Set stage to onsite" is idempotent; "advance the candidate one stage" is not.

Carry a key downstream. When a webhook triggers a call into another system, derive that request's idempotency key from the delivery id so a duplicate webhook cannot become a duplicate record in your HRIS.

Order is not guaranteed. Two stage changes seconds apart may arrive out of order. Use the event created_at to discard an update older than the state you already hold.

Acknowledge fast, work async

Return 2xx as soon as the payload is verified and durably queued. Do the real work in a background worker.

A slow handler causes a cascade you do not want: the delivery times out, Worklittle retries, your handler starts the same expensive work again, and the retry backlog grows while your workers are already saturated. Outbound delivery is not subject to your inbound API key rate limit, but a pile of retries against a struggling endpoint helps nobody.

Target well under a second for the acknowledgement path. Verification, dedupe insert, enqueue, return.

Reconciliation

Webhooks are for latency. Reconciliation is for correctness. Run both.

Every hour, list records changed since your last watermark and compare against your mirror. Use list_webhook_deliveries to inspect recent attempts and statuses when something looks off. Alert on your endpoint's failure rate, not only on your own exception count. Use test_webhook after any change to your handler, your TLS setup, or your firewall rules. * Keep the reconciliation window narrow and paginated so it stays cheap enough to run often.

For a careers site the same logic applies to cache: revalidate on job.published, job.updated, and job.closed, and keep a slow background refresh as a safety net.

Pitfalls

| Pitfall | Consequence |
| --- | --- |
| Parsing JSON before verifying | Signature never matches |
| No dedupe on the delivery id | Duplicate candidates, duplicate notes |
| Heavy work before the 2xx | Timeouts, retry storms |
| Assuming ordered delivery | An older update overwrites a newer one |
| Treating `created_at` as ISO | Date parsing failures |
| Webhooks with no reconciliation | Silent drift you find weeks later |
| Rotating a secret without a dual-accept window | Rejected deliveries during rollover |

Related docs

Webhooks overview, Webhook integration, ATS automation, Embedded job board.