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}/messagesDo 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.
| Response | What it means | What the client should do |
|---|---|---|
| Successful response | A result is available | Save the returned message ID and mark the outbox item complete |
503, SERVICE_UNAVAILABLE, not_sent | The operation was not sent to OnlyFans | Retry the same operation with the same key, after backoff |
503, SERVICE_UNAVAILABLE, unknown | The message may already exist | Stop automatic sending and reconcile the result |
409, IDEMPOTENCY_CONFLICT | The key is reserved and no completed result can be replayed yet | Wait and retry the same key within a bounded retry budget; reconcile persistent conflicts |
503, IDEMPOTENCY_UNAVAILABLE | This attempt could not establish idempotency protection and was not executed | Retry the same key and payload after Retry-After; this says nothing about an earlier attempt |
422, IDEMPOTENCY_KEY_MISMATCH | This key belongs to a different request | Stop and fix the outbox/key mapping; do not bypass the error by automatically generating a new key |
No response, malformed response, or another ambiguous 5xx | There is no reliable delivery outcome | Keep 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
- Persist the key and immutable payload before the first attempt.
- Save successful message IDs and stop sending that outbox item.
- Retry only an appropriate retryable result, with the same key. Respect
Retry-After, use jitter, and enforce a finite attempt/time budget. - 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.