The Only API docs

Messaging & mass DM

Select recipients, reuse Feed media, and handle uncertain outcomes without duplicate sends.

Choose the endpoint

IntentEndpointBehavior
One conversationPOST /accounts/{of_user_id}/chats/{fan_id}/messagesOne DM; a positive price makes it PPV
Native OnlyFans mass messagePOST /api2/v2/messages/queueCreates one native queue item
CRM fan-outPOST /accounts/{of_user_id}/messages/massResolves cached subscribers and sends individual DMs synchronously

For a Feed followed by one native mass message, use /api2/v2/messages/queue. Both mass-send flows are OnlyFans-only and have different targeting contracts. Neither provides a documented exactly-once send guarantee.

A queue POST is a real send

There is no supported dry_run on /api2/v2/messages/queue. An unsupported field may be ignored while the message is sent. dry_run belongs only to the CRM /accounts/{of_user_id}/messages/mass helper.

Native mass message: the Fans audience

Passthrough calls require X-API-Key and user-id. The latter identifies the sending account, which must own the media. Its saved proxy is used unless you supply an X-Proxy override.

Use userLists: ["fans"] to select the native Fans list. Discover lists through GET /api2/v2/lists?view=queue&offset=0&limit=30&skip_users=all. Do not assume the Fans list includes expired subscribers, accounts you follow, or every historical chat contact. Subscriber statistics are not delivery counts.

Correction: use userLists for the Fans audience

The inspected OnlyFans composer uses queueBuyers for buyers of previous queued messages. Do not rely on queueBuyers: [] to mean all subscribers. Earlier examples describing it that way were incorrect. Select the Fans list explicitly with userLists: ["fans"].

Preview recipients without sending

This is the GET preview used by the native composer for the Fans list:

curl --get "$BASE/api/crm/$CRM/api2/v2/chats/users" \
  -H "X-API-Key: $KEY" -H "user-id: $OFUID" \
  --data-urlencode 'limit=1' --data-urlencode 'offset=0' \
  --data-urlencode 'total=1' --data-urlencode 'skip_users=all' \
  --data-urlencode 'filter[list_id]=fans' \
  --data-urlencode 'field=recent' --data-urlencode 'sort=desc'

Check success, then read data.total. limit=1 requests one sample; it does not limit the total or the later mass message to one recipient. The preview does not reserve recipients, validate attachments or guarantee delivery. If you change filters, preview the same filters; this example covers the unexcluded Fans list. Counts and eligibility can change before sending.

POST /api2/v2/messages/queue/size is used for an existing queue/buyer selection, not as the general list-audience preview. It also passes through the account write gate because it is a POST. Do not enable writes merely to preview.

Free message with two existing photos

This POST sends a real message when writes are enabled. Replace the example media IDs with the sending account's permanent photo IDs and supply the intended model's direct OnlyFans URL.

curl -X POST "$BASE/api/crm/$CRM/api2/v2/messages/queue" \
  -H "X-API-Key: $KEY" -H "user-id: $OFUID" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "See more here: https://onlyfans.com/MODEL_USERNAME",
    "price": 0,
    "lockedText": false,
    "mediaFiles": [123456789, 987654321],
    "previews": [],
    "isForward": false,
    "userLists": ["fans"]
  }'

Both photos are free at price: 0. Application labels such as PUBLIC and SECRET do not create different access levels. Store the returned queue identifier and response. Queue acceptance is not proof of delivery to every recipient.

Publish a Feed, then reuse its media

  1. Upload each file with POST /accounts/{of_user_id}/media.
  2. Put each complete returned media object in the Feed's mediaFiles array.
  3. After Feed success, persist the post ID and permanent media[].id values. If the response lacks ready media, read GET /api2/v2/posts/{post_id}; do not publish again.
  4. Check the expected attachments are present and ready. Existing media can be inspected with GET /api2/v2/vault/media/{media_id} or the vault listing.
  5. Put the permanent integer photo IDs in the mass message's mediaFiles.

No second upload is needed for those existing vault photos. Do not substitute source URLs, post IDs, entire upload responses or trimmed {processId} objects. Reusing a temporary upload object after the Feed consumes it is not verified and is unnecessary for this flow. See Posting.

Keep Feed success independent

Use a durable message job with a unique constraint on (crm_id, of_user_id, feed_post_id). Persist Feed success before making the job eligible and atomically allow only one worker to claim it. If a crash occurs before the job is created, reconcile the saved Feed and create the missing job; do not republish the Feed.

Record the attempt before sending. Store the payload fingerprint, attempt time, returned identifier and response. A fingerprint is an audit aid, not an idempotency key. A job left in sending after a crash must become outcome_unknown, not automatically return to pending.

Timeout, transport error or 5xx after submission

An error after submission does not prove that nothing was created. Disable blind POST retries in your HTTP client and job runner.

  • Mark ambiguous attempts outcome_unknown and stop automatic retries.
  • Inspect GET /api2/v2/messages/queue and available OnlyFans message history. The queue body is an array under data; do not assume data.list.
  • If the existing item is conclusively identified, save its ID and reconcile the job. Matching text alone is insufficient.
  • Absence from the pending queue does not prove failure: the message may already have been processed. Hold unresolved attempts for review.
  • Never roll back or repeat the successful Feed because the message failed.

There is no documented idempotency key or definitive lookup by client request ID for these sends. A unique local job does not make the external POST exactly-once. Avoiding duplicates can require leaving an uncertain send on hold. See Write outcomes & verification.

CRM mass DM: cached synchronous fan-out

POST /accounts/{of_user_id}/messages/mass accepts audience, not the native userLists targeting contract. It does not create a native queue item.

{
  "text": "New update",
  "price": 0,
  "mediaFiles": [],
  "audience": { "type": "active" },
  "dry_run": true
}

dry_run defaults to the JSON boolean true. It returns recipients, a sample of up to ten, and sent: 0, without sending. It does not validate media or reserve recipients. Normal API accounting and rate limits apply. Use the JSON boolean false only for an intentional real send; do not send strings such as "false".

Audience fieldEffect
typeactive (default), expired, or all cached subscribers
fan_idsExplicit IDs override other filters; first 5,000 inputs only
min_spentMinimum cached total spend
since / untilInclusive subscribed_at bounds

The current database helper limits cache resolution to 500 rows, and the route does not paginate. Spend filtering occurs after that limit. Explicit IDs are capped at 5,000 and should be deduplicated by the caller. Neither mode guarantees complete audience coverage. A cache refresh does not remove the cap.

Real sends loop synchronously within the request, with no durable mass-job ID. Inspect sent, failed and every results entry: success: true does not mean all recipients succeeded. After a timeout, repeating the whole audience can send duplicate DMs.

Write permission and platform support

Check GET /accounts/{of_user_id}/polling. An operator can intentionally enable allow_of_write_actions through PATCH on that route. Disabled sends return 403 with code: "WRITES_DISABLED". Do not automatically toggle it in preview or retry routines.

Single-DM CRM routes support OnlyFans and Fansly, but the current Fansly route does not forward mediaFiles. Upload and both mass-send flows here are OnlyFans-only; the CRM mass route rejects Fansly with 501.

Conversations and PPV

curl "$BASE/api/crm/$CRM/accounts/$OFUID/chats" -H "X-API-Key: $KEY"
curl "$BASE/api/crm/$CRM/accounts/$OFUID/chats/$FANID/messages" -H "X-API-Key: $KEY"
curl "$BASE/api/crm/$CRM/accounts/$OFUID/ppv-stats" -H "X-API-Key: $KEY"

See Pagination and Rate limits. For PPV, a positive price adds a paywall and previews identifies the unlocked teaser subset. Existing photo IDs are integers; fresh uploads use the complete media object returned by the upload route.

Verification scope

On 27 October 2026, authenticated reads confirmed the Fans list, the GET recipient preview and matching photo IDs in published Feed posts and the vault. Inspected OnlyFans client build 202610231500-5ba1710def confirms userLists targeting and integer serialization for existing photos.

No real mass message was submitted in that verification. These observations establish the read contract, media availability and client payload construction; they do not establish live send acceptance or delivery. Check permissions, session health, media readiness and targeting for the actual account at run time.

On this page