> ## Documentation Index
> Fetch the complete documentation index at: https://www.dropfans.io/developers/llms.txt
> Use this file to discover all available pages before exploring further.

# API v1 webhooks

> Subscribe a creator’s account to webhook topics with its OAuth token, verify the HMAC-SHA256 signature on every delivery, and test a topic on demand.

- Source: https://www.dropfans.io/developers/api-v1/webhooks
- Section: API v1 (OAuth apps)
- OpenAPI: https://www.dropfans.io/developers/openapi.json

Webhook subscriptions belong to one account and one app: subscribe with that account’s access token, and the topics you chose are POSTed to your URL whenever they happen on the account, for as long as your app holds a live grant with the topic’s scope.

## Subscribe

```bash
curl -X POST https://api.dropfans.io/v1/webhooks/subscriptions \
  -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
  -d '{"url": "https://app.example.com/hooks", "events": ["creator.payment.succeeded", "creator.follow.created"]}'
```

Answers `201 {"id": "…", "signingSecret": "whsec_…"}`. The signing secret is returned **once**: store it. Before the subscription is saved we send an unsigned verification request, `{"type": "webhook.verification", "timestamp": "…"}`, to your URL; any 2xx within 8 seconds passes (redirects are not followed). The URL must be public https.

The token needs `read:self` plus the scope of every topic. Up to 50 topics per subscription and 20 subscriptions per (account, app). `GET /v1/webhooks/subscriptions` lists them and `DELETE /v1/webhooks/subscriptions/{id}` removes one.

## Delivery

Each delivery is a JSON `POST` with the topic in the `X-Fanvue-Topic` header and the signature in the signature header, `X-Fanvue-Signature`. Envelope topics have this body:

```json
{
  "id": "3f1c2a9e-5b7d-4c8e-9f10-2a3b4c5d6e7f",
  "type": "creator.follow.created",
  "timestamp": "2026-11-01T12:00:00.000Z",
  "data": { "object": "follow", "…": "…" }
}
```

`data` uses snake_case keys; money is integer cents. `id` identifies the occurrence and never changes across retries, so use it to de-duplicate; `timestamp` is set on each attempt. Legacy topics send their own flat camelCase body with an `eventId` instead of the envelope.

Answer with any 2xx quickly and do the work afterwards. Other answers and timeouts are retried with backoff, up to 8 attempts in all. Deliveries stop when the subscription is deleted, the grant is revoked, or the topic’s scope or category is no longer allowed for your app.

## Verify the signature

The signature header has the form `t=<unix seconds>,v0=<hex>`, where `<hex>` is HMAC-SHA256 with your signing secret over the string `"<t>.<raw body>"`. Compute it over the raw request bytes (before any JSON parsing), compare in constant time, and reject timestamps more than about five minutes away from your clock.

```js tab="Node"
import crypto from 'node:crypto';

function verify(rawBody, header, secret) {
  const m = /^t=(\d+),v0=([0-9a-f]{64})$/.exec(header || '');
  if (!m) return false;
  const want = crypto.createHmac('sha256', secret).update(`${m[1]}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(m[1])) < 300;
  return fresh && crypto.timingSafeEqual(Buffer.from(want), Buffer.from(m[2]));
}
```

```python tab="Python"
import hashlib, hmac, re, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    m = re.fullmatch(r"t=(\d+),v0=([0-9a-f]{64})", header or "")
    if not m:
        return False
    want = hmac.new(secret.encode(), m[1].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return abs(time.time() - int(m[1])) < 300 and hmac.compare_digest(want, m[2])
```

## Test a topic

`POST /v1/webhooks/test` with `{"event": "<topic>"}` sends a sample of that topic (ids prefixed `TEST-`) to your subscriptions that include it: `202 {"id": "…", "event": "…"}`. It is `404` when no subscription of yours includes the topic, and `400` for legacy topics and for topics that cannot be tested yet (including chat topics while chat is not enabled for your app). 30 calls a minute.

## Topics

Topics delivered today:

| Topic | Scope | Format |
| --- | --- | --- |
| `payout.paid` | `read:creator` | envelope |
| `payout.failed` | `read:creator` | envelope |
| `follow.new` | `read:creator` | legacy flat body |
| `message.read` (chat) | `read:chat` | legacy flat body |
| `message.received` (chat) | `read:chat` | legacy flat body |
| `purchase.new` | `read:creator` | legacy flat body |
| `subscription.new` | `read:creator` | legacy flat body |
| `subscription.renewed` | `read:creator` | legacy flat body |
| `subscription.cancelled` | `read:creator` | legacy flat body |
| `subscription.expired` | `read:creator` | legacy flat body |
| `tip.new` | `read:creator` | legacy flat body |
| `creator.payment.succeeded` | `read:creator` | envelope |
| `creator.subscription.activated` | `read:creator` | envelope |
| `creator.subscription.renewed` | `read:creator` | envelope |
| `creator.subscription.deactivated` | `read:creator` | envelope |
| `creator.subscription.cancel_at_period_end_changed` | `read:creator` | envelope |
| `creator.post.liked` | `read:post` | envelope |
| `creator.post.commented` | `read:post` | envelope |
| `creator.message.deleted` (chat) | `read:chat` | envelope |
| `creator.refund.created` | `read:creator` | envelope |
| `creator.dispute.created` | `read:creator` | envelope |
| `creator.follow.created` | `read:creator` | envelope |
| `creator.message.received` (chat) | `read:chat` | envelope |
| `creator.message.sent` (chat) | `read:chat` | envelope |
| `creator.message.mass_sent` (chat) | `read:chat` | envelope |
| `creator.message.read` (chat) | `read:chat` | envelope |

The subscription endpoint also accepts the other published topic names, so an app can register its full list without errors, but nothing is delivered for them yet. Chat topics are delivered once chat is enabled for your app. Ignore topics you do not handle.

---

Previous: [API v1 resource list](https://www.dropfans.io/developers/api-v1/resources.md) · Next: [API reference overview](https://www.dropfans.io/developers/reference/overview.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt)
