ofapisv1
capability

Updated

OnlyFans Posts API

The first thing to know about the OnlyFans posts API is that there is no /posts resource. Feed posts are created, listed and deleted as rows in the scheduled queue, under /accounts/{accountId}/scheduled/posts. Every route is account-scoped, there is no unscoped variant, and a create costs 3 credits rather than the default 1. Code written against an imagined POST /posts fails on the first call; code written against the queue works and bills predictably.

What you reach for, and what exists

You expect What is actually there
POST /posts POST /accounts/{id}/scheduled/posts — 3 credits
GET /posts GET /accounts/{id}/scheduled/posts with a mandatory date window
GET /posts/{id} nothing; read the window that contains it
PATCH /posts/{id} nothing; delete and create a replacement
DELETE /posts/{id} DELETE /accounts/{id}/scheduled/posts/{postId} — 1 credit
"publish now" a near-future scheduledAt, or isScheduled: 0

The queue is not a staging area posts graduate out of; it is the publishing interface. A post going out in ten minutes and one going out in three weeks are the same object from the same call.

A feed post is media and a flag

The create body has four fields and no fifth:

Field Type Default Meaning
mediaFiles integer array [] vault media ids to attach
isScheduled integer 1 1 = dated, 0 = undated "publish later" queue
isPublishedWithPeriod integer 0 periodic publishing flag
scheduledAt string null ISO-8601 instant, required when isScheduled is 1

What is absent is as informative as what is present. There is no caption or text field, no price, no tags, no expiry, no audience targeting and no poll. A feed post in this API is a set of vault ids on a timestamp. Anything that needs words or money in front of a fan is a message, not a post — priced content goes through the PPV API, and broadcast copy through the mass DM API.

curl -X POST https://api.ofapis.com/api/public/v1/accounts/42/scheduled/posts \
  -H "Authorization: Bearer ofapis_sk_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: feed-42-2026-03-14T18Z" \
  -d '{"mediaFiles":[88123,88124],"isScheduled":1,"scheduledAt":"2026-03-14T18:00:00Z"}'
{ "data": { "id": 222333444 } }

That id is the postId you will need to delete it, and it is the only thing the create hands back — no echo of the body, no publish state. Record it against your own calendar row immediately — a database table, or the Google Sheet the content calendar already lives in — because there is no lookup-by-anything-else to recover it later.

mediaFiles are ids that already exist in the creator's vault. They come from an upload or, better, from ids you already hold in the vault media index. The field defaults to an empty array, so a body with no media is accepted by the schema and produces a post with nothing in it. Validate non-empty on your side.

scheduledAt is parsed strictly as a UTC instant and must be in the future, so 2026-03-14T18:00:00Z works and 2026-03-14 18:00 does not. The validation runs before anything is sent upstream, and the exact rejection messages are listed on the scheduling API page.

Reading the queue costs a credit even when it 400s

GET /scheduled/posts requires both publishDate and publishDateEnd as YYYY-MM-DD. There is no default window, so a bare call fails:

curl "https://api.ofapis.com/api/public/v1/accounts/42/scheduled/posts" \
  -H "Authorization: Bearer ofapis_sk_..."
#=> 400  Missing required parameter 'publishDate'

That response is billed. Credits are debited by the gateway filter before the request reaches parameter binding, and the refund rule covers only 409, 424 and 5xx — a 400 keeps your credit, as does a 404 from a wrong account id. Build the window into your client so the mistake is impossible. The optional parameters are timeZone (default UTC) and limit (default 20, clamped to 1–100).

One surprise in the response: each item carries a type of post or message, because the window spans the scheduled message queue as well. Filter client-side if your calendar keeps them in separate lanes. Items also arrive alongside a syncInProcess boolean — while it is true the queue is still being reconciled with OnlyFans, and a post you just created may legitimately not appear yet. That is sync lag, not a failed create. Re-creating it costs another 3 credits and leaves you with two posts.

Counters as a change detector

The sibling route GET /scheduled/posts/counters takes the same required window and returns a per-day map instead of the items:

{ "data": { "list": { "2025-07-01": { "message": 3, "post": 1 } }, "syncInProcess": false } }

One credit, the whole month, a few hundred bytes. That makes it the right thing to poll: hash the map, compare it to the last one, and only pull item lists for days whose counts moved.

let fingerprint = null;

async function changedDays(accountId, from, to) {
  const qs = new URLSearchParams({ publishDate: from, publishDateEnd: to });
  const res = await fetch(`${base}/accounts/${accountId}/scheduled/posts/counters?${qs}`, { headers });
  const { data } = await res.json();                     // 1 credit
  if (data.syncInProcess) return [];                     // upstream still settling, don't act

  const previous = fingerprint ?? {};
  fingerprint = data.list;
  return Object.keys({ ...previous, ...data.list }).filter(
    (day) => JSON.stringify(previous[day]) !== JSON.stringify(data.list[day]),
  );
}

A dashboard — or a cron-driven automation job — that polls counters and fetches day windows only on a delta spends single-digit credits an hour. The same dashboard pulling the full month list on every tick costs the same per call but returns kilobytes of unchanged items, and still cannot tell you what moved.

What churning the queue costs

Call Credits
Create a post 3
Delete a post 1
List a window 1
Counters for a window 1

Because there is no update route, moving a post is a delete plus a create: 4 credits. Rebuilding rather than patching is where budgets go. An agency roster of five creators at 20 posts a week, torn down and re-created every Monday, is 5 × 20 × 4 = 400 credits a week before a single read — about 1,700 a month against Starter's 30,000. Diffing the counters first and touching only the slots that actually changed typically cuts that by an order of magnitude. Plan allowances are on pricing.

Every create accepts Idempotency-Key. A replay returns the original {"data":{"id":...}} with Idempotency-Replayed: true and is not charged a second time, which makes a retry after a network timeout free instead of a 3-credit double-post. A duplicate sent while the first is still running gets 409 IDEMPOTENCY_CONFLICT, which costs nothing.

FAQ

Is there a top-level /posts endpoint?

No. The only feed-post routes are GET, POST and DELETE under /accounts/{accountId}/scheduled/posts, plus the /counters read. Anything you have seen describing a bare /posts resource on this API is wrong, and a request to it will not route.

Can a feed post carry a caption or a price?

No. The create body accepts mediaFiles, isScheduled, isPublishedWithPeriod and scheduledAt only — there is no text or price field anywhere in it. Content that needs copy or a paywall has to be sent as a message instead.

Why does a list call with no dates still cost a credit?

Because credits are debited in the gateway before Spring binds query parameters, so the missing-parameter 400 happens after the debit. Only 409, 424 and 5xx responses are refunded, which means the malformed call is simply spent. See API errors for the full charged-versus-refunded split.

How do I confirm a create actually landed in the queue?

Read the window that contains the post's scheduledAt and look for the returned id. If syncInProcess is true, wait and read again rather than re-creating — the queue is still reconciling upstream, and a second create gives you a duplicate post and another 3-credit charge.

How do I reschedule without paying twice?

You cannot avoid the 4 credits — delete then create is the only path, since no update route exists. What you can avoid is paying them twice: reuse a deterministic Idempotency-Key derived from the post id and the new timestamp so a retried move replays instead of double-booking the slot.

Start free · Scheduling guide · API reference

Related guides

All guides
Start free — 250 credits, no card
Generate a token and make your first call in minutes.
Get started