The Only API docs

Posting & scheduling

Upload media, publish a post, or queue it for later — and the four ways this fails silently.

Posting media is two calls: upload the file, then create the post referencing what you got back.

# 1. upload
curl -X POST "$BASE/api/crm/$CRM/accounts/$OFUID/media" \
  -H "X-API-Key: $KEY" -F "[email protected]"
# -> {"success":true,"media":{"processId":"…","host":"…","thumbId":1,"name":"photo.jpg","extra":"…"}}

# 2. post it, passing that object through whole
curl -X POST "$BASE/api/crm/$CRM/api2/v2/posts" \
  -H "X-API-Key: $KEY" -H "user-id: $OFUID" -H "Content-Type: application/json" \
  -d '{"text":"new set 🔥","mediaFiles":[{"processId":"…","host":"…","thumbId":1,"name":"photo.jpg","extra":"…"}]}'

Working example

A runnable Python client covering upload, immediate posting, scheduling and every trap on this page: theonlyapi-posting-example.zip.

Uploading

Two ways in — raw bytes, or a URL we fetch for you.

curl -X POST "$BASE/api/crm/$CRM/accounts/$OFUID/media" \
  -H "X-API-Key: $KEY" \
  -F "[email protected]"

Any format OnlyFans accepts, including HEIC straight off an iPhone. Up to 512 MB.

OnlyFans has no single upload endpoint of its own — internally it's a signed S3 create, a direct PUT of the bytes (5 MiB parts above 5 MiB), a multipart finish, then a handoff to a converter host. POST /accounts/{of_user_id}/media runs all four server-side so you make one call.

On failure the response carries a stagefetch, create, put or convert — telling you which part broke.

Scheduling

Send isScheduled and scheduledDate:

curl -X POST "$BASE/api/crm/$CRM/api2/v2/posts" \
  -H "X-API-Key: $KEY" -H "user-id: $OFUID" -H "Content-Type: application/json" \
  -d '{
        "text": "going live tomorrow",
        "mediaFiles": [ { "processId": "…", "host": "…", "thumbId": 1, "name": "photo.jpg", "extra": "…" } ],
        "isScheduled": 1,
        "scheduledDate": "2026-08-20T12:00:00+00:00"
      }'

Confirm it actually queued — GET /api2/v2/schedules should list the post id:

curl "$BASE/api/crm/$CRM/api2/v2/schedules" -H "X-API-Key: $KEY" -H "user-id: $OFUID"

Delete a queued post the same way as a published one: DELETE /api2/v2/posts/{post_id}.

Four ways this fails silently

Each of these returns HTTP 200 while doing nothing you wanted.

1. postedAt does not schedule

postedAt is ignored on create. In either format (…Z or …+00:00) the post publishes immediately. If you have been scheduling with postedAt, those posts went out the moment you created them.

Use isScheduled + scheduledDate, and verify against /api2/v2/schedules.

2. Media goes in mediaFiles, not media

bodyresult
mediaFiles: [{processId, host, thumbId, name, extra}]attached
media: [{…same object…}]post created, 0 media
mediaFiles: [{processId}] alonepost created, 0 media

Pass the upload result through whole. Dropping thumbId or name loses the attachment with no error. Check media.length on the response.

3. A new upload is not in the vault yet

OnlyFans creates the vault row when a post consumes the media — roughly 10–15 seconds after posting, not at upload time. A fresh upload will not appear in GET /api2/v2/vault/media, nor in /vault/media/processing, nor under /vault/media/hash.

This is OnlyFans' own behaviour: it has no upload-to-vault operation, and its web client's vault page is browse/organise only. Don't poll the vault for a new upload's id — use the returned object. Integer vault IDs are only for re-using media that has already been posted.

4. Posts are rate limited to one per ~10s

Two posts inside ten seconds returns 400 "Please allow 10 seconds". Space them by 12s when publishing in a loop.

Write actions are off by default

Uploading and posting act as the creator, so both are gated per account. Enable once:

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}'

Without it you get 403 with code: "WRITES_DISABLED" — match on the code, not the message. Check current state with GET /accounts/{of_user_id}/polling.

send_attachments: false is not about uploading

The account capability send_attachments refers to DM attachments, which are not wired yet. Upload support is reported by media_upload.

Re-using media already in the vault

Media that has been posted before is referenced by plain integer id:

curl "$BASE/api/crm/$CRM/api2/v2/vault/media?field=recent&sort=desc&limit=24" \
  -H "X-API-Key: $KEY" -H "user-id: $OFUID"

Both forms can go in the same mediaFiles array — integers for existing vault media, whole objects for fresh uploads.

To match a local file against something already uploaded, use GET /api2/v2/vault/media/hash?h={md5}&size={bytes} (note h, not hash). The MD5 must be of the original bytes; a re-encoded or CDN-downloaded copy will not match.

Stories and messages

The same uploaded object attaches to stories (POST /api2/v2/stories) and DMs (POST /api2/v2/chats/{fan_id}/messages) through the same mediaFiles field. See Messaging & mass DM for the send flow and the PPV pricing rules.

Platform support

OnlyFans only. Fansly accounts return 501 from the upload endpoint — its upload pipeline is not wired yet. See Platforms.

On this page