> ## Documentation Index
> Fetch the complete documentation index at: https://docs.context.dev/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Setting up Context.dev for a user? Follow https://docs.context.dev/agent-quickstart.md. MCP clients sign in with OAuth and need no API key; application code reads CONTEXT_DEV_API_KEY.

# Webhooks

> Verify, retry, and replay batch and monitor events.

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.

## Configure delivery

```json theme={null}
{"webhook":{"url":"https://app.example.com/webhooks/context","retry":{}}}
```

Omitting `retry` means best-effort delivery. Store the batch’s `webhook_secret` from the submit response; it is returned only there. A monitor’s `webhook.secret` is visible with Manage permission.

Slack and GovSlack incoming-webhook URLs receive formatted messages instead of the JSON event envelope.

## Verify signatures

`X-Context-Signature` contains `t=<unix>,v1=<hex>`. Compute HMAC-SHA256 over the timestamp, a period, and the unmodified raw request body, using the webhook secret. Compare in constant time and reject stale timestamps. Each attempt gets a new signature.

```typescript theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhook(rawBody: Buffer, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(",").map(part => part.trim().split("=")));
  if (!/^\d+$/.test(parts.t ?? "") || !/^[a-f0-9]{64}$/i.test(parts.v1 ?? "")) return false;
  const timestamp = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.`).update(rawBody).digest();
  const actual = Buffer.from(parts.v1, "hex");
  return actual.length === expected.length && timingSafeEqual(actual, expected);
}
```

Verify before parsing JSON. Use `X-Context-Id` as the stable event deduplication key, including across retries and replays. `X-Context-Event`, `X-Context-Delivery-Id`, and `X-Context-Attempt` identify the event and attempt.

## 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.

## Delivery history and replay

[Search deliveries](/api-reference/webhooks/list) by source and status, then inspect [attempts](/api-reference/webhooks/attempts). Delivery history lasts 30 days. Manual replay is available for seven days after the event; expired replay returns 410.

[Retry a delivery](/api-reference/webhooks/retry) resends its original payload using the source’s current URL and signing secret. Use `force: true` for a delivery that already succeeded. A source that was deleted or no longer has a webhook cannot be replayed. An optional idempotency key avoids duplicate replay requests.

Batch payloads retain their original download links, which expire after 24 hours. Retrieve fresh links while the result files remain available; batch files expire after seven days.

## 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](/monitors/webhooks#rotate-the-signing-secret).
