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

# Webhooks

> How webhooks work in Cativa: event types, delivery guarantees, HMAC signing and retries.

Webhooks are how Cativa **notifies you** when something happens — instead of you polling the API every minute. This page is the high-level view. For the step-by-step on subscribing, signature verification and retries, see [Subscribing and verifying webhooks](/en/webhooks/subscribing-and-verifying).

## How it works

When something relevant happens in a tenant (user receives a badge, post is created, payment confirmed), Cativa builds the event payload and POSTs to **your public endpoint** with `Content-Type: application/json`.

```
Action in tenant
       │
       ▼
Cativa builds the payload
       │
       ▼
POST https://your-app.com/webhooks/cativa
```

Register the URL and the event types you want to listen to in the Cativa dashboard, under **Console > Webhooks**. Each listener gets its own `secret` (format `whsec_` + 64 hex), used to sign every delivery from that listener.

## Delivery guarantees

<CardGroup cols={2}>
  <Card title="At-least-once" icon="repeat">
    Each event is delivered **at least once**. Always use `X-Cativa-Execution-Id` for idempotency on your side, avoiding double-processing in case of duplicates.
  </Card>

  <Card title="Order NOT guaranteed" icon="shuffle">
    Events can arrive out of the order in which they happened. Don't write code that assumes `user_created` arrives before `user_received_badge` — they can swap.
  </Card>
</CardGroup>

### Why no ordering?

Cativa delivers events in parallel to reach your endpoint quickly. Enforcing order would cut throughput by an order of magnitude. Instead, each event payload contains **all the data you need** to process it in isolation.

## HMAC signature (X-Cativa-Signature)

Every delivery is signed with **HMAC-SHA256** using the listener's `secret`. You verify the signature before processing the event, ensuring the request really came from Cativa and the body was not tampered with in transit.

The signature ships in this header:

```
X-Cativa-Signature: t=1715177521,v1=8c1d4e2a3b5f4d8a9c6e7f0b1a2d3e4f8a9c6e7f0b1a2d3e4f8a9c6e7f0b1a2d
```

* `t` — Unix timestamp (seconds) of the moment of delivery.
* `v1` — HMAC-SHA256 (hex) over the string `"<t>.<rawBody>"`, using the listener's `secret` as the key.

Full verification examples (Node, Python, Go, C#) live in [Subscribing and verifying webhooks](/en/webhooks/subscribing-and-verifying#verifying-the-hmac-signature).

<Warning>
  Compute the HMAC over the **raw body** (the exact string received), not over re-serialized JSON. Re-serializing changes whitespace and key order, which invalidates the signature.
</Warning>

## Retries with backoff

If your endpoint doesn't reply with `2xx`, Cativa retries on this curve:

```
30s  →  5min  →  30min  →  2h  →  6h  →  24h
```

That's **6 retries** after the initial attempt — **7 deliveries** in total, covering roughly **33 hours**. Cativa honors the `Retry-After` header you return (capped at the next backoff window's max).

### When we retry vs permanent failure

| Status                          | Behavior                         |
| ------------------------------- | -------------------------------- |
| `2xx`                           | Success — no retry               |
| `408 Request Timeout`           | Retry                            |
| `429 Too Many Requests`         | Retry (honors `Retry-After`)     |
| `5xx` (500, 502, 503, 504, ...) | Retry                            |
| Network errors / TCP timeouts   | Retry                            |
| `400 Bad Request`               | **Permanent failure** — no retry |
| `401 Unauthorized`              | **Permanent failure** — no retry |
| `403 Forbidden`                 | **Permanent failure** — no retry |
| `404 Not Found`                 | **Permanent failure** — no retry |
| `410 Gone`                      | **Permanent failure** — no retry |

If every attempt fails, the delivery is recorded internally as failed. v1 does **not** yet ship a Console UI to inspect failed deliveries — contact Cativa support to investigate.

## Payload format

<Warning>
  Unlike many APIs, the payload **does not use a generic envelope** (`{id, type, data}`). Each event has its own shape in **PascalCase**, with `CustomerId` at the top level for multi-tenant routing.
</Warning>

Example (payload for `user_received_badge`):

```json theme={null}
{
  "CustomerId": "01HQ0...",
  "BadgeId": "01HQ4...",
  "BadgeName": "Premium",
  "User": {
    "Id": "01HQ7Z3X4Y5Z6A7B8C9D0E1F2G",
    "Email": "mary@example.com",
    "DisplayName": "Mary Smith"
  },
  "ReceivedAt": "2026-05-08T14:32:01Z"
}
```

Common envelope fields across events:

| Field                 | Description                                                                                                                                              |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CustomerId`          | ID of the tenant that emitted the event. **Use it to route when your endpoint receives webhooks from multiple tenants.**                                 |
| Event-specific fields | Each event adds its own fields (e.g. `BadgeId`, `BadgeName`, `User`, `ReceivedAt` for `user_received_badge`). See each event's page for the exact shape. |

## Event catalog

Cativa exposes events for the platform's main actions. Start with the canonical event:

<CardGroup cols={2}>
  <Card title="user_received_badge" icon="shield-check" href="/en/webhooks/events/user-received-badge">
    Fired when a badge is assigned to a user. Full reference page with payload and example receiver.
  </Card>

  <Card title="user_created" icon="user-plus" href="/en/webhooks/events/user-created">
    New user signed up.
  </Card>

  <Card title="user_joined_group" icon="users" href="/en/webhooks/events/user-joined-group">
    User joined a group (manual or via badge).
  </Card>

  <Card title="post_created" icon="message" href="/en/webhooks/events/post-created">
    New post published in a group.
  </Card>

  <Card title="paywall_payment_completed" icon="credit-card" href="/en/webhooks/events/paywall-payment-completed">
    Paywall payment completed successfully.
  </Card>
</CardGroup>

The full list of available events (including `comment_created`, `course_completed`, `lesson_completed` and `user_received_private_message`) lives in [Subscribing and verifying webhooks](/en/webhooks/subscribing-and-verifying#available-events).

## Anti-pattern: processing events synchronously in the endpoint

<Warning>
  **Do not** run slow operations (HTTP calls, heavy queries, report generation) **inside** the webhook handler. Return `2xx` as fast as possible and process in the background.

  The correct pattern: the endpoint **enqueues the event into your own queue and responds 200**. A worker on your side processes it later, calmly.

  ```js theme={null}
  app.post('/webhooks/cativa', express.json(), async (req, res) => {
    // Enqueue for later processing — don't process here
    await myQueue.enqueue(req.body);
    res.status(200).send('ok');
  });
  ```
</Warning>

## Idempotency on your side

Since delivery is at-least-once, you need to **detect duplicates**. Use the `X-Cativa-Execution-Id` header (always sent and stable across retries of the same event) as a deduplication key:

```js theme={null}
async function processEvent(executionId, payload) {
  const alreadyProcessed = await db.events.exists(executionId);
  if (alreadyProcessed) return;

  await db.transaction(async (tx) => {
    await applyBusinessLogic(tx, payload);
    await tx.events.insert({ id: executionId, processedAt: new Date() });
  });
}
```

Persisting the execution identifier in the same transaction as the business logic guarantees that either everything happened or nothing happened — no chance of double-processing.

## Next steps

<CardGroup cols={2}>
  <Card title="Subscribing and verifying webhooks" icon="webhook" href="/en/webhooks/subscribing-and-verifying">
    How to register a listener, verify HMAC, handle retries.
  </Card>

  <Card title="user_received_badge" icon="shield-check" href="/en/webhooks/events/user-received-badge">
    Full reference page for an event — concrete example of payload and receiver.
  </Card>
</CardGroup>
