Skip to main content

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.

This event fires every time a badge is assigned to a user, whether by purchase, manual admin assignment, or via API.
The internal event name (used when subscribing webhooks in the Console) is user_received_badge.

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:
{
  "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

FieldTypeDescription
CustomerIdGUIDID of the tenant that emitted the event. Use it to route when your endpoint receives webhooks from multiple tenants.
BadgeIdGUIDID of the badge that was assigned.
BadgeNamestringConfigured name of the badge (e.g. Premium).
User.IdGUIDID of the user who received the badge.
User.EmailstringUser email.
User.FirstNamestringFirst name.
User.LastNamestringLast name.
User.DisplayNamestringDisplay name.
User.UsernamestringUsername (no spaces).
User.PhoneNumberstringPhone, when provided.
User.CreatedAtISO 8601When the user was created in the tenant.
User.BadgeIdGUID | nullUser’s primary badge (compatibility — may equal the top-level BadgeId).
User.BadgesGUID[]Full list of badges assigned to the user at the time of the event.
ReceivedAtISO 8601When the assignment happened in the tenant.

Request headers

HeaderDescription
X-Cativa-SignatureHMAC-SHA256 signature of the delivery, in the format t=<unixTs>,v1=<hex>. Verify it before processing the event.
X-Cativa-Execution-IdUnique ID of this event. Stable across retries — use it as your idempotency key.
X-Cativa-Automation-IdID 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.

Sample receiver (Express)

This example verifies the HMAC signature, drops requests outside the 5-minute anti-replay window, and only processes authenticated events:
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):
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.

user_joined_group

Fired when the user joins a group (potentially via badge).

Subscribing to webhooks

How to register listeners, verify HMAC and handle retries.