OnlyFans API

Idempotency and safe retries

Use stable keys, interpret message-send outcomes, and avoid duplicate messages when requests fail.

Use an Idempotency-Key for each logical message you send. Keep that key when retrying the same operation so a retained result can be returned without sending the message or charging for the send again.

This guide covers Send Message:

POST /api/{account}/chats/{chat_id}/messages

Do not assume every mutating endpoint supports this header. Incoming webhook deduplication uses a different header, X-OFAPI-Idempotency-Key.

A timeout does not prove a message was not sent

Only an explicit request_outcome: "not_sent" confirms that the operation did not reach OnlyFans. An unknown outcome means OnlyFans may have completed it. An HTTP status alone, a missing webhook, or an expired key does not establish non-delivery.

Create the key before the first attempt

Generate a UUID and save it with your outbox item, account, chat, and complete request payload before making the request. Reuse these values across retries, worker restarts, and recovery from a client timeout.

curl --request POST \
  'https://app.onlyfansapi.com/api/acct_example/chats/123/messages' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: 70fb85b2-e610-40d0-8a70-0736e12af875' \
  --data '{"text":"Hello!"}'
  • Use one key per logical message, not one key per HTTP attempt.
  • A separately intended message gets a new key, even if its text is identical. Do not derive the key solely from message text.
  • Keep the account, chat, query parameters, message text, media, price, and other payload values unchanged on retries. Changing JSON object key order is fine; changing array order or values is not.
  • Keys are scoped by team, endpoint, and account. Within that scope, reusing a key for another chat or payload is a mismatch, not a second send.
  • Use 1–255 printable ASCII characters. A UUID is a suitable choice. Requests without a key do not have this protection.

Decide from the response

The examples below show relevant fields and headers; responses may include additional metadata. Match the status, error, and request_outcome fields rather than parsing human-readable messages.

ResponseWhat it meansWhat the client should do
Successful responseA result is availableSave the returned message ID and mark the outbox item complete
503, SERVICE_UNAVAILABLE, not_sentThe operation was not sent to OnlyFansRetry the same operation with the same key, after backoff
503, SERVICE_UNAVAILABLE, unknownThe message may already existStop automatic sending and reconcile the result
409, IDEMPOTENCY_CONFLICTThe key is reserved and no completed result can be replayed yetWait and retry the same key within a bounded retry budget; reconcile persistent conflicts
503, IDEMPOTENCY_UNAVAILABLEThis attempt could not establish idempotency protection and was not executedRetry the same key and payload after Retry-After; this says nothing about an earlier attempt
422, IDEMPOTENCY_KEY_MISMATCHThis key belongs to a different requestStop and fix the outbox/key mapping; do not bypass the error by automatically generating a new key
No response, malformed response, or another ambiguous 5xxThere is no reliable delivery outcomeKeep the existing key and outbox state; reconcile before resubmitting an uncertain send

Successful send and replay

HTTP/1.1 200 OK
Content-Type: application/json

{"data":{"id":1234567890,"text":"Hello!"}}

A repeat request with a retained result returns that result. The status need not be a success: a retained unknown result also replays.

HTTP/1.1 200 OK
Content-Type: application/json
Idempotent-Replayed: true
X-OFAPI-Credits-Used: 0
X-OFAPI-Idempotency-Original-Trace-Id: 8e0456a4-3534-4242-9720-bbdc186cf425

{"data":{"id":1234567890,"text":"Hello!"}}

Idempotent-Replayed: true means the endpoint did not execute again. Check the response body before marking the message complete. The original-trace header is included when that trace ID was retained.

Confirmed not sent: retry the same key

HTTP/1.1 503 Service Unavailable
Content-Type: application/json

{"error":"SERVICE_UNAVAILABLE","request_outcome":"not_sent","message":"The proxy connection to OnlyFans is temporarily unavailable. Please retry."}

A cooldown response can also include Retry-After. Respect it; otherwise use bounded exponential backoff with jitter. Keep the original key and payload. Do not run parallel retries for the same outbox item.

Unknown outcome: reconcile instead of sending again

HTTP/1.1 503 Service Unavailable
Content-Type: application/json

{"error":"SERVICE_UNAVAILABLE","request_outcome":"unknown","message":"The request outcome is unknown: OnlyFans may have completed the operation, but its response was not received. Verify the result before resubmitting to avoid duplicates."}

Keep the outbox item unresolved. Check List Chat Messages and any matching webhook/message IDs. A single empty result or a missing webhook is not proof of failure: events and reads can arrive later. If you cannot confidently determine the outcome, ask support to investigate rather than sending again.

Repeating the key can return the same saved warning. That is duplicate protection working. Do not rotate the key to bypass it.

Reserved key: wait, then reconcile if it persists

HTTP/1.1 409 Conflict
Content-Type: application/json
Retry-After: 1

{"error":"IDEMPOTENCY_CONFLICT","message":"A request with this Idempotency-Key is already in progress."}

Another attempt may still be running. A reservation can also remain after a worker crash or after a successful send whose response could not be stored. Polling the same key must be bounded; a persistent 409 is not evidence that the message failed. Never interpret expiration of this reservation as permission to resend an unresolved message.

Idempotency unavailable: this attempt was not executed

HTTP/1.1 503 Service Unavailable
Content-Type: application/json
Retry-After: 1

{"error":"IDEMPOTENCY_UNAVAILABLE","message":"Idempotency protection is temporarily unavailable.","description":"This attempt was not executed. Retry with the same Idempotency-Key and request body."}

Retry with the same key and payload after the indicated delay, within your retry budget. If a previous attempt had an unknown outcome, keep it unresolved: this response only describes the current attempt.

Key mismatch: fix the client state

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{"error":"IDEMPOTENCY_KEY_MISMATCH","message":"This Idempotency-Key was already used with a different request."}

Check whether two outbox items share a key, whether the chat/account changed, or whether a retry rebuilt the payload differently. A corrected or separately intended operation can need its own key, but reconcile the original operation before creating another send.

Bound retries and retain unresolved outbox items

  1. Persist the key and immutable payload before the first attempt.
  2. Save successful message IDs and stop sending that outbox item.
  3. Retry only an appropriate retryable result, with the same key. Respect Retry-After, use jitter, and enforce a finite attempt/time budget.
  4. On unknown, missing outcome information, persistent conflict, or exhausted retry budget, keep the item for reconciliation. Do not silently create a fresh send.

Current defaults retain in-flight reservations for 10 minutes and completed responses, including recognized unknown outcomes, for 24 hours. These are retention limits, not a guarantee that a send will become safe to retry afterward. Cache loss, eviction, crashes, expiry, or a new/missing key can remove protection. Idempotency does not provide an unconditional exactly-once guarantee across those failures.

For investigation, retain UTC timestamps, the account and chat IDs, your outbox ID and idempotency key, response status/body, x-ofapi-trace-id, and X-OFAPI-Idempotency-Original-Trace-Id when present. Never include API tokens in logs or support messages.

On this page