ofapisv1
info

Updated

OnlyFans API Error Codes

OnlyFans API errors from ofapis are decided in a fixed order, and the HTTP status tells you almost everything: whether a retry can ever work, and whether the call just cost you a credit. This page is the full taxonomy — every status, what triggers it, which of the two error body shapes it arrives in, and what it costs. Not every failure is free — see what an error costs below.

Two error shapes, not one

Success is always {"data": ...}. Failure is not always {"error": ...}, and assuming it is will crash your client on the first validation error it meets.

Refusals decided in the gateway — 402, 403, 429, the idempotency 409, and the fail-closed 401 — carry a JSON envelope with a machine-readable code:

{ "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Rate limit exceeded. Retry after 12s." } }

Everything raised inside the handler — 400, 404, the 409 for an inactive account, 413, 415, 422, 424 and the vault's 502 — comes back as a bare string body with the right HTTP status and no envelope and no code:

HTTP/1.1 424 Failed Dependency

The OnlyFans session for account 42 is expired.

A 500 is a third shape again: {"error":{"code":"INTERNAL","requestId":"..."}}, with no message. And a 401 raised by the token filter is written by the servlet container, so it has whatever body that produces.

Branch on the HTTP status first and treat the body as a hint rather than a contract. The identifiers in the table below name the condition — they are what the reference calls each case, not a string you can count on parsing out of every response. One status can carry more than one condition: a 409 is either an idempotency conflict or an inactive account, the two want opposite reactions, and only the first arrives with a code to read.

Every response also echoes X-Request-Id. Send your own value in that request header and it comes back unchanged; omit it and the gateway generates a UUID. That id is written into the server-side log line for your call, so quoting it turns "a request failed yesterday" into a single log lookup.

Where each error is decided

The gateway evaluates a public API request in a fixed order, and the first check that fails wins. That order explains most surprising precedence:

  1. Token — a missing Authorization header, a value that doesn't start ofapis_sk_, or a revoked token stops here with 401. Nothing downstream runs. See authentication.
  2. Idempotency — on a POST carrying Idempotency-Key, a completed record replays the stored response with Idempotency-Replayed: true, and an in-flight one returns 409 IDEMPOTENCY_CONFLICT.
  3. Rate limit — the per-token per-minute counter. Over the ceiling, 429 with Retry-After. Because this runs before the billing checks, you can be rate-limited while your credit balance is zero.
  4. Subscription — no subscription, or one that isn't ACTIVE, returns 403 NO_ACTIVE_SUBSCRIPTION.
  5. Credits — the endpoint's weight is debited here. Too small a balance returns 402, and nothing is spent.
  6. Handler — everything else. The request is already paid for by the time your payload is parsed, which is why some 4xx responses are billable.

Every code

Status Condition What triggers it Retry? Credits
400 BAD_REQUEST Blank list name, empty userIds, a scheduledAt in the past, an unparseable date, a missing required query param No — fix the payload Charged
401 UNAUTHENTICATED Header missing, token not prefixed ofapis_sk_, token unknown or revoked No Free
402 INSUFFICIENT_CREDITS Balance below the endpoint's weight After a top-up Free
403 NO_ACTIVE_SUBSCRIPTION No plan, or a subscription that is canceled or past due After subscribing Free
404 ACCOUNT_NOT_FOUND The accountId, mailing or run id isn't yours No Charged
409 IDEMPOTENCY_CONFLICT The same Idempotency-Key is already in flight on this token Yes, after a pause Free
409 ACCOUNT_NOT_ACTIVE The account is PENDING or DISCONNECTED After reconnecting Refunded
413 MEDIA_FILE_TOO_LARGE Upload above 52,428,800 bytes (50 MB) No — shrink the file Charged
415 MEDIA_TYPE_NOT_SUPPORTED Anything but image/jpeg, image/png, image/webp, video/mp4 No Charged
422 OF_AUTH_INVALID The stored OnlyFans session failed validation After re-linking Charged
422 ACCOUNT_NOT_LINKED No linked account behind this token After linking Charged
424 OF_SESSION_EXPIRED The account is in ERROR state, or OnlyFans rejected the stored session upstream After re-linking Refunded
429 RATE_LIMIT_EXCEEDED Over the per-minute ceiling for this token Yes, after Retry-After Free
5xx — Unhandled server error (500), or a vault upload that failed upstream (502) Yes Refunded

What an error costs

Credits are debited before the request reaches the handler, and whether you get them back depends on how far it got.

Free. 401, 402, 403 and 429 are refused at the gate, so nothing is ever debited. 409, 424 and any 5xx are debited and then refunded automatically — which is the important one for retry logic, because an expired session or a transient upstream failure is exactly what you retry, and doing so costs nothing.

Charged. 400, 404, 413, 415 and 422 are not refunded. The request was authenticated, inside your rate limit and paid for; it ran, and the input was what was wrong. So validate payloads locally against the request schemas in the OpenAPI reference rather than discovering the shape by probing the API, and never retry one of these unchanged — it will fail identically and bill again.

What it costs is the endpoint's weight, not a flat credit. A 413 on media upload burns the full five credits that endpoint is weighted at, because the size check happens inside the handler, after metering. Weights per route are listed on the cost page.

The handler to write

Four buckets cover every row in the table: retry as-is, pause and retry the same key, re-link the account, or stop and fix the call.

import time, requests

RETRYABLE = {429, 500, 502, 503, 504}

def call(method, url, attempts=5, **kw):
    for n in range(attempts):
        r = requests.request(method, url, headers=H, **kw)
        if r.ok:
            return r.json()["data"]

        # Only gateway refusals carry the JSON envelope; handler errors are
        # a bare string, so never assume r.json() will parse.
        try:
            err = r.json()["error"]
        except Exception:
            err = {"code": str(r.status_code), "message": r.text[:200]}

        if r.status_code in RETRYABLE:
            time.sleep(float(r.headers.get("Retry-After", 2 ** n)))
            continue
        if r.status_code == 409 and err["code"] == "IDEMPOTENCY_CONFLICT":
            time.sleep(1.0)
            continue
        if r.status_code == 424:
            raise NeedsRelink(err["message"], r.headers["X-Request-Id"])
        raise ApiError(r.status_code, err["code"], err["message"])

    raise ApiError(r.status_code, "RETRIES_EXHAUSTED", url)

NeedsRelink is not a retry. A 424 means the creator's session is dead server-side and no backoff revives it — raise a "reconnect this account" task and stop scheduling work against that account until it clears. On a roster that means quarantining one creator while the others keep running, which is the pattern agency operations is built around. Anything reaching ApiError is a caller bug: log the code, the X-Request-Id and the body, then fix it rather than loop.

The same four buckets, written idiomatically for other stacks: typed API errors and context deadlines in the Go quickstart, a Guzzle stack that honours Retry-After in the PHP guide, and the undici call with a hard timeout in the Node walkthrough.

Retrying safely

Any POST accepts an Idempotency-Key header, and the record is scoped to (token, key) with a 24-hour lifetime. Reuse the same key for a retry and you get the original response back verbatim with Idempotency-Replayed: true, charged once. This matters most for mass DMs and message sends, where a timeout leaves you genuinely unsure whether the first attempt landed — and most of all for a greeting fired at a new subscriber, where the duplicate is the first thing that fan ever hears from the creator.

The gateway discards the in-progress record whenever it refunds or rejects — on 409, 424, 5xx, a rate-limit block or a credit failure — so a key that failed is reusable immediately rather than poisoned for 24 hours. A key that succeeded is not: reusing it replays instead of re-sending.

When you get a 5xx

A 500 body carries {"error":{"code":"INTERNAL","requestId":"..."}} and no detail, by design — stack traces and SQL fragments never cross the wire, so the requestId is the only handle you get. A 502 is narrower: the vault accepted the request but the upstream upload failed, and one that repeats on the same file usually means the file, not the service. Both refund the weight. Pacing belongs on the rate limits page; per-endpoint shapes are in the API reference.

FAQ

Do failed API calls cost credits?

Most of them don't. A 401, 402, 403 or 429 never reaches metering, and a 409, 424 or 5xx is refunded automatically — so the failures you actually retry are free. A 400, 404, 413, 415 or 422 is charged, because the request ran and the payload was the problem.

What does a 424 error mean?

The token is valid but the stored OnlyFans session is dead (OF_SESSION_EXPIRED). The credit is refunded, and the fix is re-linking the account rather than retrying. Until that happens, every call against the account returns the same 424 without touching OnlyFans.

One caveat worth knowing: the OpenAPI document still annotates ACCOUNT_NOT_LINKED as 424, while the exception handler that actually runs returns 422. Trust the status you observe over the one in the spec, and treat that case as charged rather than refunded.

Why did a 400 still charge me a credit?

Because metering happens before the handler parses your body. The gateway debits the endpoint's weight, then runs the request; a validation failure inside the handler means the work was attempted and the weight stays spent. Only 409, 424 and 5xx trigger an automatic refund.

Which errors are safe to retry automatically?

429 after waiting out Retry-After, any 5xx, and a 409 IDEMPOTENCY_CONFLICT after a short pause. Everything else either needs a human (424, 403, 402) or a code change (400, 404, 413, 415, 422). Retrying those unchanged reproduces the error and bills again.

What is X-Request-Id for?

It correlates one call with its server-side log line. Set it yourself on the request and it is echoed back untouched; leave it off and the gateway mints a UUID and returns that. A 500 puts the same value in the response body, which is the only detail you get on an internal error.

Start free · Authentication · Rate limits · API reference

Related guides

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