How Does the OnlyFans API Work?
OnlyFans publishes no API of its own — why that is, and what exists instead — so a working "OnlyFans API" is a layer somebody else operates between your code and the platform. That answer is only useful if you can see inside the layer. This page traces one request from the curl you type to the JSON you get back: each stage, what it can reject you with, and what it costs.
The short version: your Bearer token identifies you, a stored OnlyFans session identifies the creator, and a gateway between them decides whether the call is authenticated, in budget, inside your rate ceiling and not a duplicate — before it touches OnlyFans at all.
The request path, stage by stage
Everything under https://api.ofapis.com/api/public/v1 passes through the same ordered pipeline.
- Request id. The first filter reads
X-Request-Idor mints a UUID, attaches it to every log line for that request, and echoes it back. Send your own and you can correlate an incident across both sides. - Token authentication. The
Authorization: Bearer ofapis_sk_...header is SHA-256 hashed and looked up; the raw token is never stored or logged. A missing header, wrong prefix, unknown hash or revoked token stops here with401. - Idempotency admission. On a
POSTcarrying anIdempotency-Key, the gateway claims that key for your token. A completed key replays the stored response withIdempotency-Replayed: true; one still in flight gets409 IDEMPOTENCY_CONFLICT. - Rate limiting. A per-token counter over a 60-second window. Every response carries
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset(unix seconds: window start plus 60). Over the ceiling,429plusRetry-After. - Subscription check. No active plan,
403 NO_ACTIVE_SUBSCRIPTION. - Credit debit. The endpoint's weight is deducted before the handler runs. Too low a balance is
402 INSUFFICIENT_CREDITS. - Account and session resolution. The handler resolves which linked account this call is for, loads its session, and the proxy layer picks the egress.
- The upstream call, then the response envelope and the refund decision.
Stages 2 through 6 are refusals that cost nothing: no credit has been debited by the time they fire.
Resolving the account and its session
Path shape decides which creator you act as. A global route such as GET /me resolves to your first ACTIVE linked account; the scoped form GET /accounts/42/me names one, and 42 is the ofapis account id from your dashboard, not the OnlyFans user id.
Resolution is also a status gate, and it runs before the upstream call:
- Account id not yours or not found:
404 ACCOUNT_NOT_FOUND. - Linked but in an error state:
424 OF_SESSION_EXPIRED, which is refunded. - No account linked at all on a global route:
422 ACCOUNT_NOT_LINKED, which is not. - Session present but rejected upstream:
422 OF_AUTH_INVALID. All three mean re-link, not retry — see error handling.
The session itself is stored as AES-256-GCM ciphertext in a single column, decrypted in memory for the call and never returned by any endpoint. That is the whole reason the integration exists: you hold a token you can revoke, and nobody has to paste OnlyFans credentials into a config file. Authentication covers the two-credential split.
Egress: whose IP the call comes from
Resolution stamps the account id onto the session data, and the HTTP client is then rebuilt per account: an HTTP CONNECT proxy with basic Proxy-Authorization, read from a 60-second cache, so a proxy change takes up to a minute to land. An account with no proxy assigned uses the shared base client unchanged.
Tiering matters when you benchmark. Free draws from a shared pool, Starter gives your account a dedicated proxy, Pro gives a dedicated proxy per creator. Measuring egress stability on a Free key is not a like-for-like test.
The upstream call
This is the part that is genuinely hard to run yourself. OnlyFans' own client signs every request, and ofapis reproduces that signature: a SHA-1 digest over a newline-joined message of a rotating static parameter, the millisecond timestamp, the path with its query string and the OnlyFans user id, plus a checksum taken at rule-specified offsets into that digest. It goes out as sign alongside time, app-token, user-id, x-bc, x-hash, x-of-rev and a consistent user-agent / sec-ch-ua pair. The rules are refreshed from an external source rather than hard-coded, because they change.
None of it reaches your code. You send one header; signing, device identifiers and keep-alive sit on the other side of the boundary.
The envelope that comes back
Success is always {"data": ...}. Failure comes in two shapes: refusals decided in the gateway — 401, 402, 403, 429 and the idempotency 409 — carry {"error": {"code": "...", "message": "..."}}, while anything raised inside a handler is returned as a bare string body with the status and no code at all. So branch on the HTTP status first and read error.code only when it is there; every status, with which shape it arrives in, is catalogued separately. Collection reads add hasMore and nextOffset inside data; message history uses the olderThanMessageId cursor instead, and limit is clamped server-side to 1–100 whatever you send.
Metering is part of the path, not a report
Weight is resolved before the handler by templating every numeric path segment to {id} and looking up a METHOD:/path key:
POST /api/public/v1/accounts/42/media
→ key "POST:/api/public/v1/accounts/{id}/media" → 5 credits
GET /api/public/v1/accounts/42/subscribers
→ no key → default 1 credit
The consequence most people get wrong is refunds. Because the debit happens first, what comes back depends on how far the request travelled. A 5xx, a 424 and a 409 are debited and then refunded automatically, so retrying an expired session or a transient upstream failure is free. A 400, 404, 413, 415 or 422 is not refunded: it was authenticated, inside your limit and paid for, and the payload was the problem. Full matrix and per-endpoint weights: the cost model.
A worked request
Sending a DM with an idempotency key, which costs 2 credits:
curl -i -X POST \
https://api.ofapis.com/api/public/v1/accounts/42/chats/98765/messages \
-H "Authorization: Bearer ofapis_sk_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: welcome-98765-2026-02-11" \
-H "X-Request-Id: 9f2c1b7e-dm-retry-1" \
-d '{"text":"Thanks for subscribing!"}'
HTTP/1.1 200 OK
X-Request-Id: 9f2c1b7e-dm-retry-1
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 1770000060
Paste that into a shell and you have the whole model in one command; the terminal audit walkthrough builds a few more of them with jq if you want to poke at the API before writing any code. Fire the identical command again and the gateway replays the first response with Idempotency-Replayed: true and does not debit twice — which is what makes an at-least-once job queue safe to point at a send endpoint. Attaching media is two steps: POST /accounts/42/media returns a mediaId for 5 credits, and you pass it in mediaIds on the send, as media upload describes.
Pull versus push
Everything above is request-response. The one thing that arrives unprompted is the message.received webhook, signed with X-Ofapis-Signature as t=<unix>,v1=<hex HMAC-SHA256> over "<t>.<raw body>". Deduplicate on X-Ofapis-Delivery; route multi-creator traffic on data.message.toUser, because the payload carries no account id. Setup is in webhook setup, and the per-minute ceilings that govern whatever you still poll are in rate limits.
Which jobs are worth putting on top of this path — and which ones have no endpoint behind them at all — is sorted out in what OnlyFans automation can and cannot do, with the build-side menu in the automation API.
FAQ
What happens between my request and the OnlyFans response?
Eight stages: request id, token hash lookup, idempotency admission, rate limiting, subscription check, credit debit, account and session resolution, then the signed upstream call. The first six belong to the gateway; only after they pass does anything reach OnlyFans, and the reply is wrapped in a data or error envelope on the way back.
Where is the OnlyFans session stored and can I read it?
It is held server-side as AES-256-GCM ciphertext and decrypted only in memory for the duration of a call. No public endpoint returns it, your API token cannot exfiltrate it, and your code never handles an OnlyFans password or cookie.
Do I have to run a session refresh job?
No. The session is kept alive on the provider's side, with no refresh token or cron on yours. When it does eventually go bad you find out through a 424 on your next call, and the fix is re-linking the account in the dashboard.
Does retrying a send duplicate the message?
Not if you send an Idempotency-Key on the POST. A repeat of a completed key replays the original response with Idempotency-Replayed: true and is not charged again; a repeat while the first is still running returns 409, so retry after a moment rather than in a tight loop.
Which part of this would I have to build myself without a provider?
The request signing and its rotating rule set, a per-account egress you procure and police yourself, a session keep-alive that survives expiry mid-job, and your own metering and backoff. The signing scheme is the item with no stable public specification, which is what makes it the recurring maintenance cost. Every route above is described in the API documentation.