OnlyFans Automation API
An OnlyFans automation API is worth exactly what its endpoint list is worth, so the useful question is not whether a job can be automated but whether there is a route behind it today. The public API is four controllers under https://api.ofapis.com/api/public/v1, reached with one Bearer token and no request signing. Everything below is either in that surface or explicitly missing from it, because designing around a gap you discover in week three is expensive. For the same inventory read as a plan rather than a reference — what to automate first and what it costs to leave running — start at the automation guide.
What exists, and what does not
| Job | Endpoint | Status |
|---|---|---|
| Reply to a DM | POST /accounts/{id}/chats/{chatId}/messages |
Yes — messaging API |
| Read an inbox or a thread | GET /chats, GET /chats/{chatId}/messages |
Yes — chat API |
| Broadcast to a segment | POST /accounts/{id}/mailings → /preview → /trigger |
Yes — mass DM API |
| Queue a post | POST /accounts/{id}/scheduled/posts |
Yes — scheduledAt is validated as a future ISO-8601 instant; scheduling API |
| Put a caption on that post | — | The scheduled-post body carries mediaFiles and flags; there is no text field |
| Build a fan segment | POST /accounts/{id}/lists, then /{listId}/users |
Yes — fan segments API |
| Upload media | POST /accounts/{id}/media |
Multipart, 50 MB, jpeg/png/webp/mp4 — media upload |
| Register a webhook endpoint | — | Dashboard only; the token cannot do it |
| Read earnings or transactions | — | No endpoint |
| Campaign or traffic statistics | — | No endpoint |
| Rank fans by spend | — | Not in the data at all |
| Unsend or edit a sent DM | — | No endpoint |
Two of those deserve a sentence. Webhook endpoints are created through the dashboard, on a session-authenticated route — your API token will not provision one, so a self-serve multi-tenant product has to walk the customer through that step. And a subscriber record is {id, username, name, avatar, subscribedOn}; there is no spend field anywhere, so any notion of a high-value fan has to come from your own ledger, not from here. Two further lookups turn a bare fan id into a profile, and neither of them adds that number either. The analytics dashboard page covers what you can reconstruct.
Read, react, write
Reads are the cheap, safe tier: profile, subscribers, following, chats, messages, notifications, mentions, vault listings, lists, scheduled posts, mailings and runs. All are GET and all cost one credit. The paginated ones take limit (default 20, clamped 1–100) and offset, with three exceptions worth hard-coding: /lists and /lists/{listId}/users default to a limit of 10, message history pages backwards on the olderThanMessageId cursor, and /notifications accepts a limit but no offset at all.
The react tier is a single thing done in response to a single event: one DM, one list membership change. The write tier is everything that touches many fans or the future — mailings, scheduled posts and messages, media. The distinction matters because only the write tier can do damage on a retry, and only the write tier has a weight above one.
Weights are not flat
A credit is not a call. The default is 1, and the exceptions are deliberate:
POST /accounts/{id}/media— 5, for the signed upload and the finish step behind the media upload route.POST /accounts/{id}/scheduled/postsand/scheduled/messages— 3 each.POST /accounts/{id}/chats/{chatId}/messages— 2.POST /accounts/{id}/mailings/{id}/trigger— 0 at the call, thenrecipientCount × 1debited inside it.
One quirk to know: the weight table keys the account-scoped send path, so the token-scoped POST /chats/{chatId}/messages falls through to the default of 1. The same message costs differently depending on which of the two routes you call. Pick one form and stay on it, or your cost model will drift from your bill.
Three processes, one system
A working automation is usually three long-running things sharing a token.
The webhook consumer is an HTTP server. message.received is the only event type that exists, so this process is the entire reactive half of your product. Dedupe on the X-Ofapis-Delivery header — the payload carries no event id — and route multi-creator traffic on data.message.toUser, because there is no account id in the body either.
const seen = new Set(); // use Redis in production
app.post("/hooks/ofapis", express.raw({ type: "*/*" }), async (req, res) => {
const delivery = req.get("X-Ofapis-Delivery");
if (seen.has(delivery)) return res.sendStatus(200);
seen.add(delivery);
const { data } = JSON.parse(req.body.toString());
const accountId = accountIdForCreator(data.message.toUser.id);
res.sendStatus(200); // ack first, work after
await queue.add("reply", { accountId, chatId: data.chatId, text: data.message.text });
});
Ack before you work. Retries land at 1m, 5m, 30m, 2h and 6h across six attempts, and ten consecutive failures disable the endpoint outright — a slow handler is how an integration quietly switches itself off. The webhooks page has the signature scheme.
The scheduler is a cron process. It rebuilds segments overnight from /subscribers and /notifications, then queues content with POST /accounts/{id}/scheduled/posts. Note that the matching GET requires both publishDate and publishDateEnd — there is no "list everything queued" call, so your scheduler has to know which window to ask about. Details are on the scheduling API page, and a runnable version of exactly this kind of nightly job — paging, retries and an idempotent send included — is the Python quickstart.
The campaign runner is a queue worker. It creates a mailing, previews it, compares estimatedCost against a budget, triggers, and then polls the run to completion. Because dispatch throttles to one message every 500 ms, that last step can run for hours; it belongs in a worker with durable state, not in a request handler. The mass DM API page documents the run state machine.
All three share one per-minute rate limit per token — 60 on Free, 100 on Starter, 500 on Pro — so a nightly segment rebuild that pages a 20,000-fan list at 100 rows a call will starve your webhook replies unless you give it its own schedule. See rate limits.
Making the writes safe
Every POST accepts an Idempotency-Key. A replay returns the original response with Idempotency-Replayed: true and is not charged twice; a duplicate arriving while the first is still running gets a 409. That turns an at-least-once queue into something you can retry without counting.
Retries are not uniformly free, though. Credits are debited before the handler runs. 401, 402, 403 and 429 are refused before metering and cost nothing. 409 (idempotency conflict, or an account that is not active) and 424 (the account's OnlyFans session has expired) are debited and then refunded, as is any 5xx. But 400, 404, 413, 415 and 422 are charged and stay charged — and 422, not 424, is where an unlinked account or an invalid OnlyFans authorization lands. Dead-letter those rather than retrying a malformed body in a tight loop, and branch on the HTTP status: only the five gateway refusals carry a JSON error.code, everything else answers with a plain text message. See error codes.
Scope everything to a creator with accountId; the same code then runs across a roster on one token, which is the shape most agency automation ends up with.
FAQ
Which jobs still need the dashboard?
Linking an OnlyFans account and registering a webhook endpoint both happen in the dashboard, on session-authenticated routes that an API token cannot reach. Everything else in this page is token-driven. Plan the onboarding flow for your own product around those two manual steps.
Is there an endpoint for earnings or statistics?
No. There is no earnings, transactions, statistics or tracking-link resource in the public API, and no per-fan spend field on a subscriber record. Revenue reporting has to be assembled from what you can observe — run counters, notification events and your own instrumentation — or taken from OnlyFans statements directly.
Do account-scoped and token-scoped calls cost the same?
Not always. Sending a DM through /accounts/{id}/chats/{chatId}/messages is weighted at 2 credits, while the token-scoped /chats/{chatId}/messages falls back to the default weight of 1. Reads are 1 either way, so the difference only bites on sends.
What keeps a retrying worker from sending twice?
An Idempotency-Key on the POST. The replay is served from the stored response with Idempotency-Replayed: true and is not billed again, and a concurrent duplicate is rejected with 409 rather than executed. Without the header, a retry is a second send.
Can one integration drive a whole roster?
Yes — every resource has an account-scoped form, so one token and one codebase cover many creators. Remember that the rate limit is per token rather than per account, so a fifty-creator roster on Pro shares 500 requests a minute between all of them.
Pick the half you build first. The reactive side is a webhook consumer — start at webhooks. The planned side is a calendar and a commit worker — start at building a scheduling tool. Start free on 250 credits · full spec