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 stage — fetch, 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
| body | result |
|---|---|
mediaFiles: [{processId, host, thumbId, name, extra}] | attached |
media: [{…same object…}] | post created, 0 media |
mediaFiles: [{processId}] alone | post 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.