API v1 webhooks
API v1 (OAuth apps)

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.

View as MarkdownUpdated Nov 1, 2026

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#

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:

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

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]));
}

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:

TopicScopeFormat
payout.paidread:creatorenvelope
payout.failedread:creatorenvelope
follow.newread:creatorlegacy flat body
message.read (chat)read:chatlegacy flat body
message.received (chat)read:chatlegacy flat body
purchase.newread:creatorlegacy flat body
subscription.newread:creatorlegacy flat body
subscription.renewedread:creatorlegacy flat body
subscription.cancelledread:creatorlegacy flat body
subscription.expiredread:creatorlegacy flat body
tip.newread:creatorlegacy flat body
creator.payment.succeededread:creatorenvelope
creator.subscription.activatedread:creatorenvelope
creator.subscription.renewedread:creatorenvelope
creator.subscription.deactivatedread:creatorenvelope
creator.subscription.cancel_at_period_end_changedread:creatorenvelope
creator.post.likedread:postenvelope
creator.post.commentedread:postenvelope
creator.message.deleted (chat)read:chatenvelope
creator.refund.createdread:creatorenvelope
creator.dispute.createdread:creatorenvelope
creator.follow.createdread:creatorenvelope
creator.message.received (chat)read:chatenvelope
creator.message.sent (chat)read:chatenvelope
creator.message.mass_sent (chat)read:chatenvelope
creator.message.read (chat)read:chatenvelope

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.

Questions? support@dropfans.io