Skip to main content
Batches send a final-status event. Monitors send change.detected by default and can also send run.completed. Use a public HTTP(S) receiver and enable retries explicitly.

Events

Every payload has event, id, created_at, and data. event mirrors the X-Context-Event header and id mirrors X-Context-Id. Status and results describes the batch in data.batch; Monitor events and alerts describes data.change and data.run.

Configure delivery

Omitting retry means best-effort delivery. Use HTTPS for your receiver in production. Slack and GovSlack incoming-webhook URLs receive formatted messages instead of the JSON event envelope.

Store the signing secret

Context.dev generates a signing secret when you configure a webhook. Save this secret on your server. Each incoming request carries a signature in X-Context-Signature, calculated using that secret and the request body. You verify each request’s signature; you do not save it as a credential for future requests. For one source, inject the secret into your receiver as a server-side environment variable such as CONTEXT_WEBHOOK_SECRET. For multiple batches or monitors, keep a separate secret for each source in a secret manager or encrypted database field. Store a reference to it with the source ID and your application’s receiver configuration. Sources that share a receiver URL still need their own secrets. Keep the exact returned string, including any whsec_ prefix. Use that full string as the HMAC key; do not Base64-decode it or hash it before storage. Your receiver needs the original value to verify requests. This secret is separate from CONTEXT_DEV_API_KEY: the API key authorizes API calls, and the webhook secret verifies incoming events. Restrict secret access to the services that configure and receive webhooks. Keep it out of browser code, source control, URLs, and logs, including logs of API responses that contain it.

Select the right secret

For multiple sources, give each receiver configuration an application-generated ID, for example /webhooks/context/watch_123. Use that route ID to look up the expected source and its secret before verifying the request. The route ID is a lookup key, not authentication. After verification, check that the payload belongs to the expected batch or monitor before processing it. Do not trust an unverified body or header to select a customer or authorize work. Save the returned secret promptly: a delivery can arrive before your setup code finishes storing it. If a batch secret was not saved, later retrieval responses cannot recover it; use batch status and results to reconcile the work. For a monitor, retrieve it with Manage permission or rotate the secret, then replay any rejected deliveries.

Verify signatures

X-Context-Signature contains t=<unix>,v1=<hex>:
  • t is the delivery attempt’s Unix timestamp in seconds.
  • v1 is the hexadecimal HMAC-SHA256 digest of t.rawRequestBody, keyed with the signing secret.
For each request, read the original body bytes, prepend the timestamp string and a literal period, and compute the digest. Compare it with v1 using a constant-time comparison. Reject missing or malformed signatures and timestamps outside your tolerance. The helper below uses five minutes in either direction; keep your receiver’s clock synchronized. Each attempt is signed with its current timestamp, including retries and manual replays. Check the header’s t, not the payload’s created_at, which remains the original event time.
verify-webhook.ts
Verify before parsing JSON. Parsing and then calling JSON.stringify changes the bytes when whitespace, escaping, or key order differs, so a valid signature can fail. In Express, register the webhook route with express.raw before express.json. In a Fetch API handler, read await request.arrayBuffer() before calling any JSON parser. A Python verification example uses the same protocol.

Connect verification to your receiver

This Express example handles one source using CONTEXT_WEBHOOK_SECRET. Save the helper above as verify-webhook.ts. Supply persistEventOnce from your own storage module: it must atomically save the verified event in a durable inbox with a unique constraint on id, succeed for an already-saved ID, and throw if storage fails. A worker can then process pending inbox entries. The monitor digest recipe shows an inbox implementation in Python.
receiver.ts
Validate the event’s data against the payload reference for the event types you handle before performing business actions. For multiple sources, replace the single environment variable with the route-based lookup described above. The signature authenticates the timestamp and raw body, not the other headers independently. Use the verified body’s id as your deduplication key and check that it matches X-Context-Id, as above. The ID stays the same across retries and replays; the signature and attempt number can change. Keep X-Context-Delivery-Id and X-Context-Attempt for troubleshooting, not as authentication or deduplication keys.

Acknowledge and retry

Return any 2xx response within 15 seconds. Persist or enqueue verified events before acknowledging, then process them asynchronously. Redirects are not followed. retry: {} uses delays of 10 seconds, 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, and 16 hours, with up to 10% jitter. A custom delays_seconds list permits up to ten delays of 1–86,400 seconds, totaling at most 72 hours. An empty list disables automatic retries. Network errors, 408, 429, and 5xx responses are retryable. Other 4xx and 3xx responses are final. Retry-After is honored.

Confirm delivery

Signature verification confirms authenticity and body integrity. A delivered status confirms your receiver returned a 2xx response; it does not confirm that your background worker finished processing the event.
  1. Configure the receiver and store the source’s secret. For a monitor setup check, subscribe to run.completed so a completed run without changes also sends an event.
  2. Complete a batch or run the monitor. Record the verified event id in your inbox and the delivery ID in your operational logs. Avoid logging the secret or full signature header.
  3. List deliveries for that source and retrieve the delivery. Confirm status: "delivered", then inspect its attempts for the HTTP status returned by your receiver.
  4. Check your inbox and worker state separately to confirm processing. After correcting a receiver problem, replay the delivery. An already-saved event should return 2xx without repeating business actions.

Troubleshoot verification

Delivery history and replay

Each event creates one delivery, identified by a whd_ ID, and every attempt to send it is recorded under that delivery. Delivery history lasts 30 days. Retrieve a webhook delivery returns every field below; search results omit event_id, retry, and last_attempt.

Attempts

List webhook delivery attempts returns a delivery’s attempts, newest first, 25 per page and up to 100 with limit. Page with cursor set to next_cursor.

Search deliveries

List webhook deliveries is a POST whose body picks the source with type and returns deliveries from the last 30 days as summaries, with has_more and next_cursor for paging. The body also accepts tags, which label the search request itself in usage rather than filter deliveries.

Replay

Retry a webhook delivery resends the original payload using the source’s current URL and signing secret. It returns 202 with the delivery id and a Location header for its status, and the resend appears as a manual attempt. The optional body takes force: true to resend a delivery that already succeeded. An optional Idempotency-Key header, up to 200 characters, prevents duplicate retry requests. Manual replay is available for 7 days after the event (retry_expires_at); after that, retry returns 410. A 409 means the delivery can’t be retried or the idempotency key conflicts. A source that was deleted or no longer has a webhook cannot be replayed. Replayed batch payloads resend their original result links; see Download files. The delivery endpoints share a rate limit of 600 requests per minute per organization.

Permissions and rotation

Read operations need the source’s batches:read or monitors:read; retries need its write scope. A monitor URL change regenerates its secret. Secret rotation takes effect immediately; update the receiver after the call returns and replay rejected events when needed. See monitor events.