---
title: Webhooks
description: Get an event when a batch finishes, and verify it came from Anyrow.
url: https://anyrow.ai/docs/webhooks
canonical_url: https://anyrow.ai/docs/webhooks
last_updated: '2026-10-11'
---

# Webhooks

Instead of polling a batch, register a webhook: when an extraction finishes, Anyrow posts an
event to your HTTPS endpoint, signed with a secret only you and Anyrow know. Webhooks belong to
an organization and need a plan that includes them.

## Events

| Event | When |
| :-- | :-- |
| `batch.complete` | Every file in the batch was extracted |
| `batch.partial` | Some files were extracted, some failed |
| `batch.failed` | The batch produced no rows |
| `ping` | You asked for a test delivery |

The body is JSON:

```json
{
  "api_version": "2026-03-01",
  "batch_id": "batch_01h5a3g8k9m2n4p6q8r0t2v4x6",
  "project_id": "proj_01h5a3g8k9m2n4p6q8r0t2v4x6",
  "status": "complete",
  "total_rows": 12,
  "duration_ms": 8421
}
```

### 1. Register your endpoint

The response carries the webhook's signing `secret` (`whsec_...`). Store it; it isn't shown
again.

```bash title="cURL"
curl -X POST "https://api.anyrow.ai/v1/organizations/$ORG_ID/webhooks" \
  -H "Authorization: ApiKey $ANYROW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hooks/anyrow", "events": ["batch.complete", "batch.failed"] }'
```

```ts title="Node.js"
const { data: webhook } = await anyrow.webhook.create({
  params: { organization_id: orgId },
  json: { url: "https://example.com/hooks/anyrow", events: ["batch.complete", "batch.failed"] },
})
// webhook.secret: store it now
```

```python title="Python"
webhook = requests.post(
    f"https://api.anyrow.ai/v1/organizations/{org_id}/webhooks",
    headers={"Authorization": f"ApiKey {os.environ['ANYROW_API_KEY']}"},
    json={"url": "https://example.com/hooks/anyrow", "events": ["batch.complete", "batch.failed"]},
).json()
```

### 2. Verify each delivery

Deliveries follow the [Standard Webhooks](https://www.standardwebhooks.com) spec. Three headers
come with each one:

| Header | Holds |
| :-- | :-- |
| `webhook-id` | The delivery's id, the same on every retry: use it to skip duplicates |
| `webhook-timestamp` | When it was sent, in Unix seconds |
| `webhook-signature` | `v1,<base64>`: an HMAC-SHA256 of `{id}.{timestamp}.{body}` |

Compute the signature over the raw body with your secret (base64-decoded, without `whsec_`),
compare it in constant time, and reject deliveries older than a few minutes. Any Standard
Webhooks library does all of it:

```ts title="Node.js"
import { Webhook } from "standardwebhooks"

const webhook = new Webhook(process.env.ANYROW_WEBHOOK_SECRET!.replace(/^whsec_/, ""))

export async function POST(request: Request) {
  const body = await request.text()
  const event = webhook.verify(body, Object.fromEntries(request.headers)) // throws if forged
  // handle event.batch_id, event.status ...
  return new Response(null, { status: 204 })
}
```

```python title="Python"
from standardwebhooks.webhooks import Webhook

webhook = Webhook(os.environ["ANYROW_WEBHOOK_SECRET"].removeprefix("whsec_"))

def handle(body: bytes, headers: dict) -> None:
    event = webhook.verify(body, headers)  # raises if forged
    # handle event["batch_id"], event["status"] ...
```

### 3. Answer fast

Return a `2xx` within 30 seconds, and do slow work after answering.

## Retries

A delivery that times out or gets a `5xx`, `408` or `429` is retried up to 6 times, after 1
second, 5 seconds, 30 seconds, 5 minutes, 30 minutes and 2 hours. A `Retry-After` header on your
`429` or `503` is honored (up to a day). Any other `4xx` isn't retried. After 10 failed
deliveries in a row the webhook is disabled and you get a notification.

## Test and rotate

- [Send a test event](/docs/api/webhooks/test) to check your endpoint; it sends `ping`.
- [Rotate the secret](/docs/api/webhooks/rotate-secret) if it leaks. For 24 hours after a
  rotation each delivery is signed with both secrets (two values in `webhook-signature`), so you
  can deploy the new one without dropping events.
- [Deliveries](/docs/api/webhooks/deliveries) lists recent attempts, with the status and body
  your endpoint returned.
