Webhooks & polling
Webhooks

Webhooks & polling

Signed webhooks tell your app about sales, refunds and ID-check results as they happen; polling endpoints cover the same ground as a fallback.

View as MarkdownUpdated Oct 23, 2026

Webhooks are per app. Set one https endpoint for your app with PUT /api/external/webhooks using a key your app owner holds (their own, or one their agency minted for its personas). Events about every creator who holds a key for your app (and every account you created) are then POSTed to it.

Events#

EventFires whendata
drop.paida buyer completes checkout on a dropan Order, including your per-fan externalRef
drop.refundeda paid drop order is refunded or charged backthe Order plus refundedAt and reason (refund or chargeback)
tip.paida fan's tip settlesa Tip, including your per-fan externalRef and the fan's message
tip.refundeda paid tip is refunded or charged backthe Tip plus refundedAt and reason (refund or chargeback)
account.kyc_updatedan account you created moves through the ID checkaccountId, username, kycStatus (in_review, approved, declined), sellerStatus
pingyou call POST /webhooks/testmessage

Envelope#

Every delivery is a JSON POST:

{
  "id": "evt_2b7c91d0e4a5f6b7c8d9e0f1",
  "type": "drop.paid",
  "created": "2026-10-17T16:20:01.000Z",
  "data": {
    "orderId": "clxor0001order",
    "productId": "clxdr0p000001prod",
    "accountId": "clx2f8a1b0001qw3k",
    "externalRef": "tg_48213377",
    "amountCents": 2750,
    "currency": "USD",
    "paidAt": "2026-10-17T16:20:00.000Z",
    "paidWith": "card",
    "status": "paid",
    "buyer": { "id": "clxbu0001buyer", "email": "[email protected]", "telegramUserId": "48213377", "telegramUsername": "fan_one" }
  }
}

Headers:

HeaderValue
DropFans-Event-Idthe event id (same as id) — dedupe on it
DropFans-Eventthe event type
DropFans-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>

Verify the signature#

The signature is an HMAC-SHA256, keyed with your whsec_… secret, over <t>.<raw request body>. Compute it on the raw bytes, before any JSON parsing. Compare in constant time, and reject timestamps more than 5 minutes old.

import crypto from 'node:crypto';

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Delivery and retries#

  • Answer any 2xx within 5 seconds to acknowledge. Do the work afterwards (queue it). Redirects are not followed.
  • Anything else is retried with backoff (about 1 min, 5 min, 15 min, 1 h, 3 h, then every 6 h), up to 8 attempts in total. After that the event is marked failed.
  • Events can arrive more than once and out of order. Dedupe on DropFans-Event-Id (or on orderId for sales).
  • GET /api/external/webhooks shows your last 20 deliveries with status codes and errors.

Polling (fallback)#

Webhooks can be missed if your endpoint is down for longer than the retry window. Keep a slow sweep as a safety net and budget it against your rate tier: 5,000 requests a day (personal) and 50,000 per key (app).

1. Sales — orders and check-status#

GET /api/external/orders with since = your last sweep returns every paid order with its externalRef. POST /drops/check-status (up to 200 product ids per call) does the same per product. With webhooks on, one sweep every 15–60 minutes is plenty.

2. Video processing — video-status#

While a video is processing after upload, poll POST /vault/video-status (up to 50 ids) every 10–15 seconds until isReady or isFailed.

3. Moderation — posts and vault#

Next: Broadcast one drop to many fans or Creator sign-up from your platform.

Questions? [email protected]