Messaging & mass DM
Select recipients, reuse Feed media, and handle uncertain outcomes without duplicate sends.
Choose the endpoint
| Intent | Endpoint | Behavior |
|---|---|---|
| One conversation | POST /accounts/{of_user_id}/chats/{fan_id}/messages | One DM; a positive price makes it PPV |
| Native OnlyFans mass message | POST /api2/v2/messages/queue | Creates one native queue item |
| CRM fan-out | POST /accounts/{of_user_id}/messages/mass | Resolves 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
- Upload each file with
POST /accounts/{of_user_id}/media. - Put each complete returned
mediaobject in the Feed'smediaFilesarray. - After Feed success, persist the post ID and permanent
media[].idvalues. If the response lacks ready media, readGET /api2/v2/posts/{post_id}; do not publish again. - 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. - 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_unknownand stop automatic retries. - Inspect
GET /api2/v2/messages/queueand available OnlyFans message history. The queue body is an array underdata; do not assumedata.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 field | Effect |
|---|---|
type | active (default), expired, or all cached subscribers |
fan_ids | Explicit IDs override other filters; first 5,000 inputs only |
min_spent | Minimum cached total spend |
since / until | Inclusive 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.