# Reducto Webhooks

Reducto (document parsing / extraction API) delivers async job notifications
through Svix. When a job submitted with
`async.webhook.mode = "svix"` finishes, Reducto sends an `async.update` webhook
whose body reports the job `status` (`Completed` or `Failed`) and `job_id`.

## When to Use This Skill

* How do I receive Reducto webhooks for async parse/extract jobs?
* How do I verify Reducto webhook signatures (`svix-signature`)?
* How do I handle a Reducto job that `Completed` or `Failed`?
* Why is my Reducto webhook signature verification failing?
* How do I pass `metadata` through a Reducto async job and read it in the webhook?

## Verification (core)

Reducto webhooks use the Svix scheme. Signed content is
`${svix-id}.${svix-timestamp}.${raw_body}`, HMAC-SHA256 keyed with the
base64-decoded bytes after `whsec_`, base64-encoded, sent as space-delimited
`v1,<sig>` entries. The official `svix` SDK (Node and Python) handles all of it,
including the 5-minute timestamp tolerance and constant-time comparison.

```javascript
import { Webhook } from 'svix';  // svix 2.x is ESM-only

const wh = new Webhook(process.env.REDUCTO_WEBHOOK_SECRET); // "whsec_..." from Svix dashboard
wh.verify(rawBody, {                                        // raw Buffer/string, NOT parsed JSON
  'svix-id': req.headers['svix-id'],
  'svix-timestamp': req.headers['svix-timestamp'],
  'svix-signature': req.headers['svix-signature'],
}); // throws WebhookVerificationError on bad signature / stale timestamp

// svix 2.x verify() returns undefined — parse the raw body after it passes
const { status, job_id, metadata } = JSON.parse(rawBody.toString('utf8'));

```

Python uses the same API: `Webhook(secret).verify(body_bytes, headers)` from
`svix.webhooks`, raising `WebhookVerificationError`.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/reducto-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/reducto-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/reducto-webhooks/examples/fastapi/).

## Webhook Event and Payload

Reducto sends a single Svix event type, `async.update`, for every async job.
The body has no `type` field — branch on `status`:

| `status` | Triggered When | Common Use Cases |
| --- | --- | --- |
| `Completed` | Async job finished successfully | Fetch results with `client.job.get(job_id)`, store output |
| `Failed` | Async job failed | Log, alert, retry the job |

```json
{
  "status": "Completed",
  "job_id": "a1b9090e-c9ae-420b-9726-f658afbbe338",
  "metadata": { "user_id": "123", "document_type": "invoice" }
}

```

The webhook only carries status — retrieve the actual results via the Reducto
API using `job_id`. `metadata` echoes whatever you passed in `async.metadata`.

## Environment Variables

```bash
# Svix signing secret for your endpoint (starts with whsec_)
REDUCTO_WEBHOOK_SECRET=whsec_xxxxx

```

Get it in [Reducto Studio](https://studio.reducto.ai) → **Webhooks** (opens the
Svix dashboard) → your endpoint → **Signing Secret**.

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 reducto --path /webhooks/reducto

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/reducto-webhooks/references/overview.md) - How Reducto async webhooks work, payload, statuses
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/reducto-webhooks/references/setup.md) - Svix dashboard setup, signing secret, submitting jobs with webhooks
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/reducto-webhooks/references/verification.md) - Signature verification details and gotchas