> ## 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.

# user_received_badge

> Fired when a user receives a badge — your chance to sync external permissions.

This event fires **every time a badge is assigned to a user**, whether by purchase, manual admin assignment, or via API.

<Note>
  The internal event name (used when subscribing webhooks in the Console) is `user_received_badge`.
</Note>

## When it fires

* Admin assigns a badge manually in the dashboard
* External purchase via Hotmart/Kiwify webhook triggers a badge assignment
* User completes a course and earns an automatic badge
* You call the API yourself to assign one

## When it does NOT fire

* Assignment fails (e.g. badge doesn't exist on the tenant)
* User has been soft-deleted

## Payload

The payload is serialized in **PascalCase** and delivered in the body of the `POST` with `Content-Type: application/json`:

```json theme={null}
{
  "CustomerId": "01HQ0ABCDEF1234567890XYZ",
  "BadgeId": "01HQ4ABCDEF1234567890XYZ",
  "BadgeName": "Premium",
  "User": {
    "Id": "01HQ7Z3X4Y5Z6A7B8C9D0E1F2G",
    "Email": "mary@example.com",
    "FirstName": "Mary",
    "LastName": "Smith",
    "DisplayName": "Mary Smith",
    "Username": "mary.smith",
    "PhoneNumber": "+15551234567",
    "CreatedAt": "2026-04-12T14:32:01Z",
    "BadgeId": "01HQ4ABCDEF1234567890XYZ",
    "Badges": [
      "01HQ4ABCDEF1234567890XYZ",
      "01HQ4ZXYZ987654321FEDCBA"
    ]
  },
  "ReceivedAt": "2026-05-08T14:32:01Z"
}
```

### Payload fields

| Field              | Type         | Description                                                                                                          |
| ------------------ | ------------ | -------------------------------------------------------------------------------------------------------------------- |
| `CustomerId`       | GUID         | ID of the tenant that emitted the event. Use it to route when your endpoint receives webhooks from multiple tenants. |
| `BadgeId`          | GUID         | ID of the badge that was assigned.                                                                                   |
| `BadgeName`        | string       | Configured name of the badge (e.g. `Premium`).                                                                       |
| `User.Id`          | GUID         | ID of the user who received the badge.                                                                               |
| `User.Email`       | string       | User email.                                                                                                          |
| `User.FirstName`   | string       | First name.                                                                                                          |
| `User.LastName`    | string       | Last name.                                                                                                           |
| `User.DisplayName` | string       | Display name.                                                                                                        |
| `User.Username`    | string       | Username (no spaces).                                                                                                |
| `User.PhoneNumber` | string       | Phone, when provided.                                                                                                |
| `User.CreatedAt`   | ISO 8601     | When the user was created in the tenant.                                                                             |
| `User.BadgeId`     | GUID \| null | User's primary badge (compatibility — may equal the top-level `BadgeId`).                                            |
| `User.Badges`      | GUID\[]      | Full list of badges assigned to the user at the time of the event.                                                   |
| `ReceivedAt`       | ISO 8601     | When the assignment happened in the tenant.                                                                          |

## Request headers

| Header                   | Description                                                                                                            |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `X-Cativa-Signature`     | HMAC-SHA256 signature of the delivery, in the format `t=<unixTs>,v1=<hex>`. **Verify it before processing the event.** |
| `X-Cativa-Execution-Id`  | Unique ID of this event. Stable across retries — use it as your idempotency key.                                       |
| `X-Cativa-Automation-Id` | ID of the listener configured in the Console (same value across all deliveries from the same subscription).            |

The full explanation of how to verify `X-Cativa-Signature` (with examples in Node, Python, Go and C#) lives in [Subscribing and verifying webhooks](/en/webhooks/subscribing-and-verifying#verifying-the-hmac-signature).

## Sample receiver (Express)

This example verifies the HMAC signature, drops requests outside the 5-minute anti-replay window, and only processes authenticated events:

```js theme={null}
import express from 'express';
import crypto from 'crypto';

const app = express();
const SECRET = process.env.CATIVA_WEBHOOK_SECRET; // whsec_...

app.post(
  '/webhooks/cativa',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const sigHeader = req.header('X-Cativa-Signature') ?? '';
    const parts = Object.fromEntries(
      sigHeader.split(',').map(p => p.split('='))
    );
    const ts = Number(parts.t);
    const sig = parts.v1;

    if (!ts || !sig) return res.status(400).send('bad signature header');
    if (Math.abs(Date.now() / 1000 - ts) > 300) {
      return res.status(400).send('stale timestamp');
    }

    const expected = crypto
      .createHmac('sha256', SECRET)
      .update(`${ts}.${req.body.toString('utf8')}`)
      .digest('hex');

    const ok = crypto.timingSafeEqual(
      Buffer.from(expected, 'hex'),
      Buffer.from(sig, 'hex')
    );
    if (!ok) return res.status(401).send('invalid signature');

    const event = JSON.parse(req.body.toString('utf8'));

    if (event.BadgeName === 'Premium') {
      // Sync with your external system
      await grantAccessInExternalSystem(event.User.Id, event.User.Email);
    }

    // Respond 200 quickly — process the rest in the background
    res.status(200).send('ok');
  }
);
```

## Idempotency

Use the `X-Cativa-Execution-Id` header received with the request to detect duplicates (the same `executionId` is sent across all retries of a given event):

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

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

## Retries

If your endpoint fails, Cativa retries on the curve `30s → 5min → 30min → 2h → 6h → 24h` (6 retries, 7 deliveries total, \~33h of coverage). Status `400/401/403/404/410` are treated as permanent failures — no retry. Full table at [Subscribing and verifying webhooks](/en/webhooks/subscribing-and-verifying#retries-and-permanent-failures).

## Related events

<CardGroup cols={2}>
  <Card title="user_joined_group" icon="users" href="/en/webhooks/events/user-joined-group">
    Fired when the user joins a group (potentially via badge).
  </Card>

  <Card title="Subscribing to webhooks" icon="webhook" href="/en/webhooks/subscribing-and-verifying">
    How to register listeners, verify HMAC and handle retries.
  </Card>
</CardGroup>
