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

# Webhooks

> Subscribe to data changes and have them delivered to your services as signed HTTP POSTs. Retry, filter, and verify with HMAC signatures.

Webhooks deliver real-time HTTP notifications to your own services whenever data changes in a project. When a record is inserted, updated, or deleted on any [Data Model](/features/backend/data-model/overview) table, Archie sends a signed `POST` to a URL you control, with the event payload in the body.

This page covers webhooks **Archie sends from your project to external systems**. For incoming webhook handling on the integration side (Stripe events, GitHub events, etc.), see [Integrations → Webhooks](/features/backend/integrations/webhooks).

## How it works

1. **Register** a webhook subscription: which table, which events, which URL.
2. **Data changes** — every matching mutation (REST or GraphQL) publishes an event.
3. **Delivery** — the webhook service `POST`s to your URL with the payload.
4. **Verify** — your endpoint validates the HMAC signature, then processes the event.
5. **Retry** — non-2xx responses retry with exponential backoff up to 5 attempts.

## Webhook management API

All management endpoints live under `/api/rest/_webhooks` and require a management API token.

| Method   | Endpoint                   | Purpose                              |
| -------- | -------------------------- | ------------------------------------ |
| `POST`   | `/api/rest/_webhooks`      | Create a subscription                |
| `GET`    | `/api/rest/_webhooks`      | List subscriptions                   |
| `GET`    | `/api/rest/_webhooks/{id}` | Get subscription + recent deliveries |
| `PATCH`  | `/api/rest/_webhooks/{id}` | Update a subscription                |
| `DELETE` | `/api/rest/_webhooks/{id}` | Delete a subscription                |

### Required headers

| Header          | Required | Description                                                |
| --------------- | -------- | ---------------------------------------------------------- |
| `Authorization` | Yes      | `Bearer <management-token>`                                |
| `X-Project-ID`  | Yes      | Project identifier                                         |
| `environment`   | Yes      | Target environment — must match the body for create/update |

The `environment` header is **required** on every webhook management request, including `GET` and `DELETE`.

## Create a webhook

```bash theme={null}
curl -X POST "https://your-gateway.example.com/gw/api/rest/_webhooks" \
  -H "Authorization: Bearer your-management-token" \
  -H "X-Project-ID: your-project-id" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "your-project-id",
    "environment": "master",
    "table": "orders",
    "events": ["INSERT", "UPDATE", "DELETE"],
    "url": "https://your-app.example.com/webhooks/orders",
    "secret": "whsec_your_signing_secret",
    "active": true
  }'
```

| Field         | Type    | Required | Description                                     |
| ------------- | ------- | -------- | ----------------------------------------------- |
| `projectId`   | String  | Yes      | Project identifier.                             |
| `environment` | String  | Yes      | Target environment.                             |
| `table`       | String  | Yes      | Table name to watch.                            |
| `events`      | Array   | Yes      | Any of `INSERT`, `UPDATE`, `DELETE`.            |
| `url`         | String  | Yes      | HTTPS endpoint to receive events.               |
| `secret`      | String  | Yes      | HMAC signing secret — keep this safe.           |
| `active`      | Boolean | No       | Default `true`. Set `false` to pause delivery.  |
| `filter`      | Object  | No       | Only deliver for records matching this filter.  |
| `select`      | Array   | No       | Limit which fields are included in the payload. |

### Filtering events

Only deliver when the record matches a condition:

```json theme={null}
{
  "table": "orders",
  "events": ["UPDATE"],
  "url": "https://your-app.example.com/webhooks/completed-orders",
  "secret": "whsec_secret",
  "filter": { "status": "completed" }
}
```

### Selecting fields

Limit the payload to specific fields — useful for keeping payloads small or avoiding sensitive data in webhook bodies:

```json theme={null}
{
  "table": "orders",
  "events": ["INSERT"],
  "url": "https://your-app.example.com/webhooks/new-orders",
  "secret": "whsec_secret",
  "select": ["id", "total", "status", "customerId"]
}
```

## Event payload

When a matching change happens, Archie delivers an HTTP `POST` to your URL with this body:

```json theme={null}
{
  "id": "evt_abc123def456",
  "timestamp": "2025-12-15T15:00:00Z",
  "project_id": "your-project-id",
  "environment": "master",
  "table": "orders",
  "event": "INSERT",
  "record": {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "total": 99.50,
    "status": "pending",
    "customerId": "cust-001"
  },
  "data": {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "total": 99.50,
    "status": "pending",
    "customerId": "cust-001"
  },
  "changed_fields": ["total", "status"]
}
```

| Field            | Description                                                          |
| ---------------- | -------------------------------------------------------------------- |
| `id`             | Unique event identifier — use this to deduplicate.                   |
| `timestamp`      | ISO 8601 timestamp of the event.                                     |
| `project_id`     | Project that generated the event.                                    |
| `environment`    | Environment in which the change happened.                            |
| `table`          | Table that was modified.                                             |
| `event`          | `INSERT`, `UPDATE`, or `DELETE`.                                     |
| `record`         | Full record (or selected fields) after the mutation.                 |
| `data`           | Same as `record`, kept for compatibility.                            |
| `changed_fields` | Fields that changed (for `UPDATE`; empty for `INSERT` and `DELETE`). |

For `DELETE`, `record` reflects the row's state immediately before deletion.

## Delivery headers

Each delivery includes:

| Header                | Value                                      |
| --------------------- | ------------------------------------------ |
| `Content-Type`        | `application/json`                         |
| `X-Archie-Event`      | `INSERT` / `UPDATE` / `DELETE`             |
| `X-Archie-Delivery`   | The event id (for tracking and dedup).     |
| `X-Archie-Signature`  | `sha256=<hex>` HMAC signature with prefix. |
| `X-Webhook-Signature` | Raw hex digest, no prefix.                 |

## Verifying signatures

Always verify the HMAC signature before processing the payload — never trust an unsigned webhook body.

### Node.js

```javascript theme={null}
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');

  const sig = signature.replace('sha256=', '');
  return crypto.timingSafeEqual(
    Buffer.from(sig, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

app.post('/webhooks/orders', (req, res) => {
  const signature = req.headers['x-archie-signature'];
  const isValid = verifyWebhookSignature(
    JSON.stringify(req.body),
    signature,
    process.env.WEBHOOK_SECRET
  );

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  const { event, table, record } = req.body;
  // process the event...
  res.status(200).send('OK');
});
```

### Python

```python theme={null}
import hmac
import hashlib

def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()
    sig = signature.replace('sha256=', '')
    return hmac.compare_digest(sig, expected)
```

Always use a constant-time comparison (`timingSafeEqual`, `hmac.compare_digest`) to avoid leaking the signature one byte at a time.

## Retries and circuit breakers

If your endpoint returns a non-2xx status or doesn't respond, the webhook service retries with exponential backoff:

| Attempt | Delay        |
| ------- | ------------ |
| 1       | Immediate    |
| 2       | \~1 second   |
| 3       | \~5 seconds  |
| 4       | \~30 seconds |
| 5       | \~5 minutes  |

After the maximum attempts, the delivery moves to a **dead-letter** state and shows up in `GET /_webhooks/{id}` for inspection.

A **circuit breaker** protects your endpoint from sustained failures: after 5 consecutive failures, deliveries pause for a 2-minute cooldown, then retry. This avoids hammering an endpoint that's clearly down.

## Inspecting deliveries

`GET /api/rest/_webhooks/{id}` returns the subscription and recent delivery history:

```json theme={null}
{
  "webhook": {
    "id": "wh_a1b2c3d4-e5f6-7890",
    "table": "orders",
    "events": ["INSERT", "UPDATE", "DELETE"],
    "url": "https://your-app.example.com/webhooks/orders",
    "active": true,
    "createdAt": "2025-12-15T14:30:00Z"
  },
  "deliveries": [
    {
      "id": "del_x1y2z3",
      "eventId": "evt_abc123",
      "status": "delivered",
      "httpStatus": 200,
      "attempts": 1,
      "createdAt": "2025-12-15T15:00:00Z"
    }
  ]
}
```

Use this endpoint as part of your monitoring — alert if a webhook accumulates failed deliveries.

## Best practices

* **Always verify signatures.** Never trust an unsigned payload.
* **Respond fast.** Return `200` within 5 seconds. Process the event asynchronously if needed.
* **Deduplicate by event id.** The same event may be redelivered if your endpoint returns a non-2xx — process the work once.
* **Use HTTPS.** Webhook URLs must be HTTPS.
* **Monitor for dead-letter deliveries.** Check `/_webhooks/{id}` regularly or wire it to alerting.

## FAQ

<AccordionGroup>
  <Accordion title="Webhooks vs subscriptions — which should I use?">
    Use webhooks for **server-to-server** delivery — your backend reacts to data changes from another machine. Use [GraphQL subscriptions](/features/backend/graphql-api-explorer/subscriptions) for **client UIs** — a browser stays connected over a WebSocket and receives events while it's open. Webhooks retry; subscriptions don't.
  </Accordion>

  <Accordion title="Why might the same event be delivered twice?">
    Network failures or 5xx responses trigger a retry. The retry uses the same event id (`X-Archie-Delivery`), so you can deduplicate by tracking processed event ids on your side. At-least-once delivery is the trade-off for guaranteed delivery on transient failures.
  </Accordion>

  <Accordion title="Can I rotate the signing secret?">
    Yes — `PATCH` the subscription with a new `secret`. To rotate without downtime, configure your endpoint to accept either the old or new secret during the rotation window, then drop the old one once the new is in place.
  </Accordion>

  <Accordion title="What happens if my webhook endpoint is down for hours?">
    Deliveries retry up to 5 times over a few minutes, then move to dead-letter. They aren't auto-redelivered after that — you'd need to read the dead-letter list and replay them yourself. For at-least-once-with-long-windows guarantees, plan endpoint reliability accordingly.
  </Accordion>

  <Accordion title="Can I have multiple webhooks for the same table?">
    Yes. Multiple subscriptions to the same table-and-event combination each receive a copy of the event. Useful for sending the same event to different downstream systems.
  </Accordion>
</AccordionGroup>
