# Exa Webhooks

## When to Use This Skill

* How do I receive Exa Websets webhooks?
* How do I verify the `Exa-Signature` header?
* How do I handle `webset.item.created`, `webset.item.enriched`, or `webset.idle` events?
* Why is my Exa webhook signature verification failing?
* Where do I find my Exa webhook secret?

## Essential Code (USE THIS)

Exa signs every delivery with HMAC-SHA256. The `Exa-Signature` header looks like `t=<unix seconds>,v1=<hex>` and may carry **several `v1` values**; the request is valid if any one matches. The signed content is `"<t>.<raw body>"`, and the key is the webhook `secret` as a UTF-8 string (returned **only** in the response that creates the webhook, so store it then).

### Verification (Node.js)

```javascript
const crypto = require('crypto');

function safeEqual(a, b) {
  const x = Buffer.from(a), y = Buffer.from(b);
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifyExa(rawBody, header, secret) {
  const pairs = header.split(',').map((kv) => kv.trim().split(/=(.*)/s).slice(0, 2));
  const t = pairs.find(([k]) => k === 't')?.[1];
  const sigs = pairs.filter(([k]) => k === 'v1').map(([, v]) => v);
  if (!t || sigs.length === 0) return false;
  // Optional replay window, off by default: retries and replays keep the original timestamp
  const expected = crypto.createHmac('sha256', secret)
    .update(`${t}.`).update(rawBody).digest('hex');
  return sigs.some((s) => safeEqual(s, expected));
}

```

### Verification (Python)

```python
import hashlib, hmac

def verify_exa(raw_body: bytes, header: str, secret: str) -> bool:
    t, sigs = None, []
    for kv in header.split(","):
        k, _, v = kv.strip().partition("=")
        if k == "t":
            t = v
        elif k == "v1":
            sigs.append(v)
    if not t or not sigs:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(s, expected) for s in sigs)

```

Exa documents no SDK helper for this, so verify manually. Always use the **raw body**; re-serialised JSON will not match.

The timestamp check is **opt-in**: Hookdeck retries and replays keep the original timestamp, so a freshness window would reject legitimate redeliveries. The examples skip it unless `EXA_TIMESTAMP_TOLERANCE_SECONDS` is set to a positive number of seconds (default `0` = off).

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

## Common Event Types

The event arrives as JSON: `{ "id", "object": "event", "type", "data", "createdAt" }`, where `data` is the full resource.

| Event | Description |
| --- | --- |
| `webset.created` | A Webset was created |
| `webset.idle` | A Webset has no running operations |
| `webset.search.completed` | A search finished finding all items |
| `webset.item.created` | An item was added to the Webset |
| `webset.item.enriched` | An item's enrichment completed |

Others: `webset.deleted`, `webset.paused`, `webset.search.created`, `webset.search.updated`, `webset.search.canceled`, `import.created`, `import.completed`, `webset.export.created`, `webset.export.completed`, `monitor.created`, `monitor.updated`, `monitor.deleted`, `monitor.run.created`, `monitor.run.completed`. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/exa-webhooks/references/overview.md).

> **For the full event reference**, see [Exa event types](https://exa.ai/docs/websets/api/events/types).

## Important Headers

| Header | Description |
| --- | --- |
| `Exa-Signature` | `t=<unix seconds>,v1=<hex>[,v1=<hex>...]` |

## Environment Variables

```bash
EXA_WEBHOOK_SECRET=your_webhook_secret   # "secret" from the create-webhook response
EXA_TIMESTAMP_TOLERANCE_SECONDS=0        # optional; a positive value opts in to the replay window

```

## Local Development

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/exa-webhooks/references/overview.md) - Exa webhook concepts and events
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/exa-webhooks/references/setup.md) - Creating a webhook through the API
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/exa-webhooks/references/verification.md) - Signature verification details