Events
The event types, what triggers each, and their payload fields.
Polling is opt-in per account. Once you enable it —
PATCH /accounts/{of_user_id}/polling with {"enabled": true} — we poll that
account in the background, diff the results, and emit typed events. Those events are stored, broadcast over
SSE, delivered to your webhooks, and
evaluated by your automations — all from the same emission.
Event types
event_type | Emitted when | Key payload fields |
|---|---|---|
new_subscriber | A new subscription appears | fan, price, subscribed_at, expire_at |
renewed_subscriber | An existing subscription renews | fan, price, subscribed_at, expire_at |
expired_subscriber | The subscriber count drops (sampled every 10th poll) | previous_total, new_total, delta — negative |
new_tip | A tip row appears in the transactions ledger | fan, amount, net, currency, tx_type, status, description |
new_message | A new-message notification arrives | fan, text — the only type with a text field |
new_purchase | A paid-content row appears — tx_type is message, post, stream or chargeback | fan, amount, net, currency, tx_type, status, description |
balance_increased | Fansly only — the wallet balance rises. Never emitted for OnlyFans; the money signal there is new_tip/new_purchase | delta, available, previous_balance, new_balance, currency |
polling_paused | Polling auto-pauses after 5 consecutive failures | reason, failures |
payout_completed | Reserved — not currently emitted | — |
Background jobs additionally emit progress events on the SSE stream, which are not stored or delivered to webhooks:
| Event | Emitted by |
|---|---|
export.progress / export.complete | Data exports |
refresh.progress / refresh.complete | Cache refreshes and backfills, including the automatic backfill when you first enable polling |
import.progress / import.complete | Bulk account import — progress is coalesced to at most one per second |
Webhook subscriptions also accept * as a wildcard for every type. * is a
subscription filter only — it is never the event_type of a delivered event.
Envelope
{
"id": 12345,
"event_type": "new_tip",
"crm_id": "crm_0123456789abcdef",
"of_user_id": "1234567",
"occurred_at": "2026-04-18T12:34:56.789",
"payload": {
"fan": {
"id": "987",
"username": "somefan",
"display_name": "Some Fan",
"avatar": "https://…"
},
"amount": 10.0,
"net": 8.0,
"currency": "USD",
"tx_type": "tip",
"status": "done",
"description": "Tip from somefan",
"created_at": "2026-04-18T12:34:50+00:00"
}
}Reading past events
curl "$BASE/api/crm/$CRM/events?types=new_tip,new_subscriber&limit=100" \
-H "X-API-Key: $KEY"| Parameter | Purpose |
|---|---|
types | Comma-separated event types |
of_user_id | Restrict to one connected account |
since / until | ISO bounds on created_at (when we recorded it), not occurred_at. since is exclusive, until inclusive |
limit | Default 100, max 500 |
This route is panel-wide and takes no offset — narrow with since/until
rather than paging.
Behaviours worth knowing
new_subscriber and renewed_subscriber are derived exclusively from the
platform's subscriber feed, distinguished by whether the entry is a subscribe or a
renewal. Notifications are not used for this — they carry no reliable renewal
indicator, so using them would double-count renewals as new subscribers.
new_tip and new_purchase take amount and net directly from the transactions ledger — real numbers. The exception is new_message, whose amount
is best-effort-parsed out of a formatted display string ("$25.00") and is often
null. Reconcile money against cached transactions regardless.
This event is derived from a drop in the total subscriber count, sampled only on
every 10th poll — roughly every 20 minutes at the default interval, so several
expiries collapse into one event. delta is new_total - previous_total, so it is
negative (-3 means three expired), and it carries no fan identity.
To recover who, read GET /subscribers/cached?type=expired, which names them with
each row's expired_at.
Events are unique on (crm_id, of_user_id, event_type, source_event_id).
source_event_id is a key we derive per source — the transaction id, the fan id plus
subscribe timestamp, and so on — not always the platform's own id. A poll that
re-walks the same record does not emit twice. Your handler should still be idempotent — webhook delivery
retries on failure, so the same event can arrive more than once.
Choosing a delivery mechanism
Webhooks
Best for servers. Signed, retried, with a delivery log. Survives your process restarting.
Server-sent events
Best for dashboards and live UI. No retry — if you disconnect, you miss what happened while you were gone.
Automations
No infrastructure at all. Match a condition, fire a Discord/Slack/Telegram message, a DM, or a fan tag.
Polling /events
Simplest, and the right fallback for batch reconciliation.