ofapisv1
guide

Updated

OnlyFans Webhook Events Explained

There is exactly one OnlyFans webhook event today — message.received, fired when a fan sends a DM to a linked creator. That is the whole catalogue, and it is still enough to retire the poll loop almost everyone builds first: ask for the conversation list on a timer, diff it against last time, act on the difference. That loop works on day one with twelve fans and falls apart around two hundred, and the failure is arithmetic rather than bad luck. This piece is about that arithmetic, the one guarantee a push model gives you instead, and the mistakes that make a first integration look like a platform problem when it is not.

The poll loop, costed honestly

Two separate budgets constrain a poller, and they bind at different times.

The first is credits. A listing call weighs one credit. Check the conversation list once a minute and you spend 1,440 credits a day, 43,200 in a month — half again the 30,000 a Starter plan includes, before a single reply has been sent. On the Free tier's 250 credits that loop runs dry in four hours and ten minutes. Tighten the interval to ten seconds, the responsiveness a chat product actually wants, and you are at 259,200 a month: more than twice the 120,000 on Pro, to learn mostly that nothing has happened. The credit model charges for frequency, not for value.

The second budget is the per-minute ceiling, and it is harsher because money cannot fix it. Free allows 60 requests a minute, Starter 100, Pro 500 — one token, one minute. A loop that walks conversations individually rather than reading the list is the version most people write second, once they want per-thread state. Two hundred active threads checked every minute is two hundred requests a minute: impossible on Free and Starter outright, and on Pro it permanently occupies 40% of your ceiling to do nothing but ask. The calls that get squeezed out are the ones that matter — your replies — and they come back as 429 RATE_LIMIT_EXCEEDED with a Retry-After you then have to honour. Rate limit headroom is a resource you spend on polls or on work, never both.

And the latency is bad even when the budget holds. A sixty-second interval means an average detection delay of thirty seconds and a worst case of sixty. You pay the full cost of the loop and still answer a fan slower than a human with a phone.

What the push model costs instead

An inbound delivery is not an API call you made, so it is metered as nothing. Zero credits, zero against the minute ceiling. The dispatcher sweeps for due events every five seconds, so a message typically reaches your handler within a few seconds of arriving.

Run one creator both ways: four hundred inbound DMs a day, every one answered. Polling at one minute costs 43,200 credits on the loop plus 24,000 on the replies at two credits each — which does not fit inside Starter at all. Webhooks cost 24,000, all of it replies, leaving 6,000 spare and a detection delay measured in seconds. The loop was more expensive than the product.

The asymmetry is structural. Polling scales with how often you ask; events scale with how often something happens. For a creator inbox those numbers diverge by orders of magnitude, which is why auto-reply systems built on a timer get rewritten onto events. The same split decides which OnlyFans automation jobs are worth running at all: the event-shaped ones are cheap, the poll-shaped ones are not.

What at-least-once actually obliges you to do

Here is the trade. The push model hands you a weaker guarantee than the loop did, and you have to absorb it in code.

Delivery is at-least-once. A retried event is the same event, not a new one, and your handler will see it twice whenever the first attempt looked like a failure from the sender's side — including the case where you did all the work correctly and the acknowledgement was lost coming back. There is no exactly-once mode to switch on; what every mature system actually offers is at-least-once plus a key you can deduplicate on.

That key is a header, not a field in the body — the envelope carries no identifier of its own, which surprises people who expect one. The webhooks reference names it, and the bot build keys its dedupe table on it. What matters here is the discipline it implies: write the claim before the side effect, not after.

wrong:  send the reply  ->  record that we handled it
right:  record that we handled it (atomically)  ->  send the reply

The wrong order is not theoretical. Crash between those two lines — a deploy, an OOM kill, a node eviction — and the retry finds no record and replies again. The right order can only drop a reply, which you can see and repair, instead of failing silently for a week.

Two details make this cheap. The claim must be atomic — an insert against a unique index, or SET NX in Redis, not a read-then-write. And the outbound call afterwards can carry an idempotency key of its own, so even a lost race ends in a replayed response rather than a second message to a fan. Keep the records a day; an event gives up after roughly eight and a half hours.

Four ways this goes wrong

Verifying the parsed body. Every framework helpfully turns a JSON request into an object before your code sees it. Re-serialise that object and you get the same data with different bytes — reordered keys, different spacing — so the signature no longer matches what was actually sent. The symptom is distinctive: everything fails verification, including the test delivery, and the code looks obviously correct. Capture raw bytes before the parser runs.

Treating a retry as a new event. Without a dedupe key, a single DM that happened to arrive during a thirty-second blip becomes two replies, or six if your endpoint was genuinely unwell. Fans notice. This is the failure that makes a bot look broken to the only people whose opinion matters.

No timeout budget. The tempting handler does everything inline: verify, look up the fan, call a model, send the reply, return 200. Under load that exceeds the sender's ten-second read timeout, so the attempt is written off and retried — while your first invocation is still running and will finish. Now two are in flight, both doing the work, and whatever caused the slowness has doubled. Acknowledge first, work in a queue consumer; the setup walkthrough has a receiver shaped that way, and a Pipedream workflow gets you the same shape — raw-body HMAC check, dedupe store, concurrency limit — without running a server at all.

Answering 4xx to things you do not understand. A non-2xx is a retry signal. Reject a payload because its type is unfamiliar and you have asked for that same payload back five more times over eight hours, and ten events treated that way in a row will get your endpoint switched off. Reserve rejection for a signature that fails. Anything else gets acknowledged and, if you cannot use it, dropped.

Where this leaves the poll loop

Not dead, just demoted. Webhooks tell you what changed from now on; they say nothing about the past, and an event that exhausted its retries is not recoverable. So keep a slow reconciliation pass — read conversations back through the chat API occasionally, compare against what you processed, heal the gaps. Hourly, that costs 720 credits a month and puts a floor under the whole system. Events for latency, a cheap sweep for completeness, never a tight loop asking whether anything has happened yet; the rest of the API best practices notes assume you have landed there.

FAQ

How many events can I subscribe to today?

One: a message-received event, fired when a fan sends a DM to a linked creator. The reference page lists it and nothing else, because nothing else exists yet, and any guide claiming tip or subscription events from this platform is describing a product that has not shipped. New subscriptions have to be discovered by polling the notifications API instead.

Is polling ever the right choice?

Yes, for reconciliation rather than reaction. A low-frequency sweep that re-reads conversations and repairs anything the event stream dropped is cheap and worth having. What you should not keep is a tight loop whose job is to discover new messages — that is the part events replace outright.

Why not just offer exactly-once delivery?

Because it is not implementable over a network where an acknowledgement can be lost after the work is done. The sender cannot distinguish "never arrived" from "arrived and the reply vanished", so it must retry, and the receiver must deduplicate. Every mature webhook system in the industry makes the same trade.

Do webhook deliveries count against my per-minute limit?

No. The minute ceiling applies to requests you make with your token; a delivery is a request ofapis makes to you. Only the outbound calls your handler triggers — a reply, a lookup — consume credits and headroom, which you can plan against the tiers on pricing.

What happens to events while my server is down?

Each one is retried on a widening schedule for roughly eight and a half hours before it is abandoned. A short outage therefore costs you nothing but latency. A long one loses events permanently, which is the concrete reason to keep the reconciliation sweep around.

Related guides

All guides
Start free — 250 credits, no card
Generate a token and make your first call in minutes.
Get started