The Only API docs

Messaging & mass DM

DMs, PPVs, mass sends with a dry-run preview, and the write-actions gate.

Read a conversation

# List conversations
curl "$BASE/api/crm/$CRM/accounts/$OFUID/chats" -H "X-API-Key: $KEY"

# Message history with one fan
curl "$BASE/api/crm/$CRM/accounts/$OFUID/chats/$FANID/messages" -H "X-API-Key: $KEY"

Both of these paginate in surprising ways

/chats largely ignores limit and returns 10–14 conversations regardless, and /messages uses an id= cursor where a short page does not mean the end. A walker that gets this wrong loses most of a message history. Read Pagination before writing one.

Send a DM or PPV

curl -X POST "$BASE/api/crm/$CRM/accounts/$OFUID/chats/$FANID/messages" \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "text": "Hey! New drop is up 💕",
        "price": 0,
        "mediaFiles": []
      }'
  • price > 0 makes the message a PPV.
  • mediaFiles takes vault media IDs, locked behind the price.
  • Works for OnlyFans and Fansly.

Rate limited to 100 requests/minute.

Write actions are off by default

OnlyFans sends are gated per account by allow_of_write_actions. Without it you get a 403 with code: "WRITES_DISABLED" (match on the code, not the message). This applies to Fansly sends too:

curl -X PATCH "$BASE/api/crm/$CRM/accounts/$OFUID/polling" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"allow_of_write_actions": true}'

Mass DM

OnlyFans only — Fansly returns 501 with {"code": "PLATFORM_NOT_SUPPORTED"}.

curl -X POST "$BASE/api/crm/$CRM/accounts/$OFUID/messages/mass" \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "text": "New set just dropped 🔥",
        "price": 12,
        "mediaFiles": ["1234567"],
        "previews": ["1234567"],
        "audience": {
          "min_spent": 20,
          "since": "2026-01-01T00:00:00Z"
        },
        "dry_run": true
      }'

A message needs text, mediaFiles, or both — an empty send is rejected with 400 message requires text or mediaFiles.

dry_run defaults to true

This is the important detail. dry_run is true unless you explicitly set it to false. In dry-run mode the API resolves the audience, counts the recipients and returns a sample — without sending anything.

{
  "success": true,
  "dry_run": true,
  "recipients": 418,
  "sent": 0,
  "sample": [ { "fan_of_user_id": "987", "username": "somefan" } ],
  "note": "Preview only — no messages were sent."
}

recipients is the resolved count; sample is the first 10 only.

Always dry-run first

This is the "who will this actually reach?" preview, and it costs nothing. Check the count and the sample, then re-send the identical body with "dry_run": false.

A mass DM cannot be recalled once sent.

Audience filters

FieldEffect
typeall | active | expireddefaults to active
fan_idsExplicit id list. Overrides every other filter. Truncated at 5,000
min_spentOnly fans who have spent at least this much
since / untilInclusive bounds on the fan's subscribed_at

`type` defaults to `active`, and tags are not an audience filter

Omitting type reaches only current subscribers, not everyone.

There is no tag field — the audience is resolved from the subscriber cache, which carries no tags, so a tag key is silently ignored. To target a segment, resolve it yourself with GET /fans, then pass the ids as audience.fan_ids.

PPV performance

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

Unlock rates and revenue per PPV message, so you can tell which sends actually earned.

Vault media

Media IDs for mediaFiles and previews come from the vault, which lives on the passthrough surface — see the Content tag in the OnlyFans passthrough reference. Upload and vault management are not exposed on the CRM layer.

previews should be a subset of mediaFiles: those items stay unlocked as a teaser while the rest sit behind the price.

Pacing

Sends are OnlyFans writes and hit the platform directly. See Rate limits.

A mass send is a serial loop inside your request

The real send is not dispatched to a background worker and is not paced. It loops through recipients synchronously inside the HTTP request — up to 5,000 sends on one connection. Keep audiences small enough that the request completes, and pace by splitting the audience with since/until rather than relying on server-side throttling.

On this page