# Attio Webhooks

## When to Use This Skill

* How do I receive Attio webhooks?
* How do I verify the Attio-Signature header?
* How do I handle `record.created` / `record.updated` / `list-entry.updated` events?
* Why is my Attio webhook signature verification failing?
* How do I deduplicate Attio webhook retries?

## Verification (core)

Attio signs every webhook with the webhook's **secret** (shown in developer settings and returned once when the webhook is created via the API). The `Attio-Signature` header (duplicated as `X-Attio-Signature` for legacy middleware) is the **hex-encoded HMAC-SHA256 of the raw request body**. There is no timestamp or prefix in the signed content. Use the raw body and compare timing-safe.

Node:

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

function verifyAttioSignature(rawBody, signature, secret) {
  if (!signature || !secret || !/^[0-9a-f]{64}$/i.test(signature)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(signature.toLowerCase(), 'hex'), Buffer.from(expected, 'hex'));
}
// signature = req.headers['attio-signature'] ?? req.headers['x-attio-signature']

```

Python:

```python
import hmac, hashlib

def verify_attio_signature(raw_body: bytes, signature: str | None, secret: str | None) -> bool:
    if not signature or not secret:
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    # Compare bytes: compare_digest raises TypeError on non-ASCII str input
    return hmac.compare_digest(signature.lower().encode(), expected.encode())

```

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

## Payload Shape

Each delivery is a JSON envelope with a `webhook_id` and an `events` array. Today each delivery contains exactly one event, but Attio documents that batching may come later, so **always iterate `events`**:

```json
{
  "webhook_id": "23e42eaf-323a-41da-b5bb-fd67eebda553",
  "events": [
    {
      "event_type": "record.created",
      "id": { "workspace_id": "…", "object_id": "…", "record_id": "…" },
      "actor": { "type": "workspace-member", "id": "…" }
    }
  ]
}

```

Payloads carry IDs only. Fetch the full record, entry, note or task from the Attio REST API if you need its data.

## Common Event Types

| Event | Description |
| --- | --- |
| `record.created` | A record (person, company, deal, custom object) was created |
| `record.updated` | An attribute on a record changed (`id.attribute_id` says which) |
| `record.deleted` | A record was deleted |
| `record.merged` | Two records were merged (fires once per original) |
| `list-entry.created` | A record was added to a list |
| `list-entry.updated` | A list attribute on an entry changed |
| `list-entry.deleted` | A record was removed from a list |
| `note.created` | A note was created |
| `task.created` | A task was created |
| `comment.created` | A comment was created |

> **For the full event list**, see [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/attio-webhooks/references/overview.md) and [Attio's webhook reference](https://docs.attio.com/rest-api/webhook-reference/record-events/recordcreated).

## Important Headers

| Header | Description |
| --- | --- |
| `Attio-Signature` | Hex HMAC-SHA256 of the raw body, keyed with the webhook secret |
| `X-Attio-Signature` | Same value, for legacy middleware |
| `Idempotency-Key` | Unique per message, **stable across retries**; use it to deduplicate |

## Delivery Behavior

* Respond with any `2xx` within **5 seconds** or the delivery counts as failed.
* Failed deliveries are retried up to 10 times with exponential back-off over about 3 days, after which the webhook is marked `degraded`.
* Delivery is at-least-once; deduplicate on `Idempotency-Key`.
* Target URLs must be HTTPS.

## Environment Variables

```bash
ATTIO_WEBHOOK_SECRET=your_webhook_secret   # From developer settings or the create-webhook API response

```

## Local Development

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

```

Attio requires an HTTPS target URL, so use the Hookdeck URL the CLI prints when you create the webhook, then use **Send test event to target URL** in developer settings.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/attio-webhooks/references/overview.md) - Attio webhook concepts, full event list, payload fields
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/attio-webhooks/references/setup.md) - Create webhooks in developer settings or via API, get the secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/attio-webhooks/references/verification.md) - Signature verification details and gotchas