ofapisv1
solution

Updated

Build an OnlyFans Scheduling Tool

If you are building an OnlyFans scheduling tool — a calendar your creators and chatters drag cards around in — the hard part is not the UI. It is two decisions the API makes for you: where a committed item is stored, and how cheaply you can paint a month. Both are settled below, with the credit arithmetic that decides whether a busy roster is profitable to serve.

The committed item lives on OnlyFans. Once POST /accounts/{id}/scheduled/posts returns an id, that item fires whether your worker is running or not. The queue is durable storage you do not operate, so treat it as the backing store for committed work and keep your own database for drafts rather than as a mirror. The second decision is forced by price: creating a scheduled post costs 3 credits, not 1, and there is no edit route. Endpoint shapes are on the scheduling API; this page is the product on top. Whether a calendar is even the right first build is settled by which jobs are worth automating first, which ranks it against welcomes, win-backs and drops.

One source of truth per state

Item state Lives in Why
Draft, dragged around the UI Your database Edits are free; nothing has been bought
Approved, outside the horizon Your database Still free to move, still not on the platform
Committed, inside the horizon The OnlyFans queue Fires without you; your row holds the remote id
Fired or cancelled Your database The queue forgets it; your history must not

The commit horizon is the lever pricing hands you. A create is 3 credits and a move is a delete plus a create, so every edit before commitment is free and every edit after costs 4. Push items on a rolling window — 48 hours ahead suits a team that revises daily — and most churn disappears. A tool that commits when a creator drops a card pays for every later change of mind.

The month grid is one counters call

GET /accounts/{id}/scheduled/posts/counters returns a map keyed by calendar date:

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

That is the shape a month grid needs, and it costs one credit for the whole month however many items it holds. Render badges from it and fetch nothing else until a user opens a day:

async function monthBadges(accountId, from, to, tz) {
  const qs = new URLSearchParams({ publishDate: from, publishDateEnd: to, timeZone: tz });
  const res = await fetch(
    `${BASE}/accounts/${accountId}/scheduled/posts/counters?${qs}`, { headers });
  const { data } = await res.json();
  if (data.syncInProcess) markGridProvisional();     // upstream still reconciling
  return new Map(Object.entries(data.list).map(
    ([day, counts]) => [day, (counts.post ?? 0) + (counts.message ?? 0)]));
}

Two details. syncInProcess means the queue is still reconciling upstream, so treat the grid as provisional rather than caching it. And the same timeZone must go to the counters call and to every day read, or a 23:00 item sits in one bucket on the badge and another in the list.

Opening a day is a windowed read, not a page

GET /accounts/{id}/scheduled/posts takes publishDate and publishDateEnd as required YYYY-MM-DD bounds plus a limit clamped to 1–100. There is no offset and no cursor. You cannot page a window, only narrow it. A calendar that loads a month in one list call silently truncates at a hundred items for a busy creator; shard by week or day and merge client-side.

Items come back as {id, type, publishDateTime, createdDateTime, entity}, where type is post or message — the read spans both queues despite the path, so your day view shows two lanes or filters on type.

Scheduled messages carry one more asymmetry: there is no GET for a scheduled message queue item. Its counters — total, pending, sentCount, viewedCount, isReady, hasError — appear in the create response and again in the delete response, nowhere else. If your calendar shows delivery progress for a queued mass DM, persist them at create time and label them as of that moment; they will not refresh. Cohort targeting via userLists comes from the fan segments API, the sending side from the mass message tool build.

What the post queue does not promise

The scheduled-post body is mediaFiles, isScheduled, isPublishedWithPeriod and scheduledAt — there is no caption field, so a queued post carries media and no text, and anything caption-shaped has to be a scheduled message instead. scheduledAt is validated on the way in and not echoed back: present, ISO-8601 and in the future or the call is a 400, and the success body is an id. Display the publishDateTime a window read returns rather than the value your form submitted, and keep minute-exact wording out of what you promise a creator. Scheduled messages are the tighter half — POST /accounts/{id}/scheduled/messages carries its instant through and echoes it back as date.

Four conflicts the tool has to handle

Drift. Someone schedules a post in the OnlyFans app and your calendar does not know. Reconcile by remote id on a timer: read the window, mark rows whose remote_id no longer appears as externally deleted, and surface unknown ids as foreign items. A calendar that omits work the creator can see in the app loses trust in a week.

The stale slot. A card queued for 18:00 is still in the commit worker at 18:05. The create fails scheduledAt must be a future date/time with a plain 400 — and a 400 is charged and not refunded, 3 credits gone. Reject any commit less than a minute out before spending the call, and route anything already past into a "missed" state.

Double-booking. Nothing in the API stops two items landing on the same minute. Slot uniqueness is your constraint, in your own schema.

Concurrent duplicates. Two workers firing the same commit with the same Idempotency-Key produce a 409 IDEMPOTENCY_CONFLICT — refunded, unlike the stale-slot 400. Derive the key from the slot rather than a random uuid, so a retried commit cannot double-book: slot-{slotId}-{isoInstant}. A replay returns the original response with Idempotency-Replayed: true, uncharged. See API errors.

What churn costs in bulk

Every move is a delete (1 credit) plus a create (3), so 4 credits per rescheduled item — and the asymmetry means the create dominates. Work it through for a five-creator roster posting three times a day, the weekly calendar push described as one of the four agency jobs:

Operation Items Credits
Commit one week's calendar 105 315
Re-plan that week (delete + recreate) 105 420
Cancel and re-plan half of it 53 212

A full re-plan costs more than the original commit. Two things follow. Shrink the committed set with a rolling horizon, so a re-plan only touches what was already pushed. And make bulk operations a queued background job, never a synchronous button: 105 moves is 210 requests, and a job that ignores X-RateLimit-Remaining stalls mid-run and leaves the week half-rebuilt. Pace from that header — see rate limits — and check what credits cost before quoting a roster a flat monthly fee.

One more trap: a recurring "18:00 local, every Friday" rule is not a fixed UTC offset. Store the local time plus the creator's zone and resolve each occurrence separately; adding seven days to the previous instant drifts an hour across a DST boundary, and scheduledAt is parsed strictly, so the drifted value is exactly what passes validation.

The worker loop

  1. Model slots with account_id, local time, the resolved UTC instant, payload, remote_id, status, and a uniqueness constraint on the slot.
  2. Upload assets to the vault first with the media upload API — mediaFiles takes ids that must already exist.
  3. A commit worker sweeps approved slots inside the horizon, posts them with a slot-derived Idempotency-Key, and writes back the returned id. A typed worker in Go, with a deadline on every call and an error you can branch on, is one good shape for it.
  4. Cancel is a delete plus a status change; a move is delete then create, in that order, so a failed create never leaves a duplicate live.
  5. A reconcile job re-reads each committed week and repairs drift.

Scope every call by accountId and one worker runs a whole roster under one token — the pattern on agency software. A walkthrough is in the scheduling guide.

FAQ

Should my database mirror the OnlyFans queue?

No — mirror only the identifiers. The platform queue fires independently of your infrastructure, so duplicating its contents gives you two states that can disagree and nothing to reconcile them. Store the slot, the remote id and the status, and treat a window read as the authority on what is queued.

When should the worker push an item to the platform?

On a rolling horizon rather than at approval time. Edits before commitment cost nothing and edits after cost 4 credits each, so holding items a day or two in your own store converts most churn from billable to free with no risk of a missed slot.

Why did a failed schedule still cost credits?

Because credits are debited before the handler runs and only some failures are given back. A 424 session error and any 5xx are refunded; a 400 from a past or malformed scheduledAt is not, so the 3-credit create is lost. Validate the instant client-side first.

How do I show delivery progress for a scheduled mass DM?

Capture it at create time and mark it as a snapshot. The queue item's total, pending, sentCount and hasError fields come back from the create and delete calls only — there is no read endpoint for a scheduled message — so a live progress bar is not something this API can back.

Will a post publish at the exact minute I send?

Do not design as if it will. The create call validates scheduledAt strictly — missing, malformed or past values come back as a charged 400 — but it returns an id and nothing more, so the authority on when an item sits is the publishDateTime in a window read. Build day-level badges backed by that read, and keep anything where the minute is the point on the scheduled-message side, which carries its instant end to end.

Can a drag-to-reschedule interaction be instant?

In your UI, yes; against the platform, treat it as asynchronous. Move the card optimistically, enqueue the delete-then-create as a background job, and reconcile the remote id when it returns. Get a free token and build the commit worker before the calendar UI.

Related guides

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