# Webhooks

> Receive sales, emergencies and AI pauses live — events, HMAC signature, deliveries.

URL : https://docs.fanconvert.ai/en/api/rest/webhooks

Receive events without polling the API: `POST /public/v1/webhooks` with a public `https://` address (port 443 or 8443) and the events you want. The response gives the signing secret `whsec_…` **only once**.

Permissions: `webhooks:write` (and `webhooks:read` to list and read the log), plus the read permission of each event.

| Event | When | Read permission required |
|---|---|---|
| `sale.created` | a sale (MYM, Telegram, OnlyFans, Fanvue) | `sales:read` (+ `telegram:read` for Telegram) |
| `emergency.created` | a new emergency | `notifications:read` |
| `ai.paused` | the AI paused on a fan, or turned off for a creator | `chatting:read` |
| `ping` | `POST /public/v1/webhooks/{id}/ping`, to test | — |

An event **only carries identifiers and amounts**, never a fan's name or a message: read the details with your key.

```json
{
  "id": "evt_9b1c…",
  "type": "sale.created",
  "apiVersion": "v1-beta",
  "createdAt": "2026-11-07T12:30:00Z",
  "data": {"platform": "mym", "creatorId": "emma-demo", "saleId": "s_…",
           "conversationId": "123", "amountMinor": 1500, "currency": "EUR", "at": "…"}
}
```

## Verify the signature [#verify-the-signature]

Header `FanConvert-Signature: t=<seconds>,v1=<hex>`: compute HMAC-SHA256 of `"<t>.<raw body>"` with your secret, compare in constant time, reject a `t` older than 5 minutes.

```python
import hashlib, hmac, time

def verify(secret: str, header: str, body: bytes) -> bool:
    fields = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(fields["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{fields['t']}.".encode() + body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, fields["v1"])
```

## Delivery [#delivery]

- Answer `2xx` within 10 s. Otherwise, retries after 1 min, 5 min, 30 min, 2 h and 6 h (six attempts), then `failed`.
- Redirects are never followed.
- The `FanConvert-Event-Id` header lets you ignore a duplicate.
- The log: `GET /public/v1/webhooks/{id}/deliveries`.
- A webhook lives with its key: key revoked or expired, it is disabled. `DELETE /public/v1/webhooks/{id}` disables it (the log stays).
- Events are detected about every 30 s; on creation, nothing from the past is replayed.

Reference: [Webhooks](/en/api/reference/webhooks).

