ofapisv1
solution

Updated

OnlyFans Agency Automation

OnlyFans agency automation is not one program. It is four jobs on different clocks: a sync that runs all day, a campaign that runs on a schedule, a calendar that runs ahead of time, and a reconciliation that runs at night. This page is for whoever owns those jobs at an agency — the exact calls, what each run costs in credits, and how each one fails — assuming a roster already linked and addressed through /accounts/{accountId}/… on a single token. If you are still deciding how the agency should be organised around them, start with how an agency runs at scale.

Job 1 — the continuous inbox sync

Runs: always. Purpose: your panel must never be the last place a message appears.

The cheap version is event-driven. A message.received webhook fires on inbound DMs, costs no credits, and carries chatId plus the message body. Your handler writes it to your store and fans it out to a chatter queue; a worked walkthrough of that handler covers the reply path end to end. Because delivery is at-least-once with retries, the handler has to be idempotent — key on X-Ofapis-Delivery, or on the message id, and ignore repeats.

Polling is the backstop, not the primary. Run GET /accounts/{id}/chats?limit=100 every fifteen minutes per creator to catch anything a failed delivery dropped. For a five-creator roster that is 480 calls a day, about 14,400 credits a month — affordable. The same sweep at one-minute intervals is 216,000 a month, which is more than the Pro allowance on its own. Interval choice is a budget decision, not a taste one.

Fails when: your endpoint goes down. Deliveries retry on a fixed backoff — one minute, five minutes, thirty minutes, two hours, six hours — and are abandoned after six attempts. An endpoint that fails ten times in a row is disabled automatically, at which point only the polling backstop is keeping you current. Alert on the gap between the last delivery and now.

Job 2 — the segmented drop

Runs: two or three times a week per creator. Purpose: revenue.

Four calls in order, and the order is the whole point:

import os, time, requests

BASE = "https://api.ofapis.com/api/public/v1"
H = {"Authorization": f"Bearer {os.environ['OFAPIS_TOKEN']}"}

def drop(account_id, list_id, text, price_cents, budget):
    key = f"drop:{account_id}:{list_id}:{time.strftime('%Y-%m-%d')}"

    m = requests.post(f"{BASE}/accounts/{account_id}/mailings", headers={**H, "Idempotency-Key": key},
                      json={"name": f"drop {time.strftime('%d %b')}", "text": text,
                            "listId": str(list_id), "priceCents": price_cents,
                            "lockedText": price_cents > 0}).json()["data"]

    est = requests.post(f"{BASE}/accounts/{account_id}/mailings/{m['id']}/preview",
                        headers=H).json()["data"]
    if est["recipientCount"] < 0:
        raise RuntimeError("untargeted mailing — refusing to fire blind")
    if est["estimatedCost"] > budget:
        raise RuntimeError(f"{est['recipientCount']} recipients = {est['estimatedCost']} credits")

    run = requests.post(f"{BASE}/accounts/{account_id}/mailings/{m['id']}/trigger",
                        headers={**H, "Idempotency-Key": key + ":trigger"}).json()["data"]
    return run["runId"], run["creditsDebited"]

Creating the mailing is 1 credit and preview is another, and preview makes real calls upstream to page through the list and count it. The trigger call itself is weighted zero and then debits 1 credit per recipient atomically before it queues anything: a 3,000-fan list is 3,000 credits taken in one movement, or a 402 and nothing sent if the balance is short. The gate between preview and trigger is the only thing standing between a mistyped list id and a four-figure credit event. Build the audience itself with the lists API — add fans to a list, then target the list — rather than broadcasting to everyone.

Dispatch is asynchronous and paced: messages go out one at a time with a 500 ms gap, so a 1,000-recipient run takes roughly eight minutes and a 5,000-recipient run roughly forty. Plan drop times around that, and do not start a second run of the same mailing while one is live — a trigger against a run that is in progress or paused returns 409. That guard stops a collision, not a repeat: trigger a mailing whose run already finished and it recounts the audience, debits again and sends again, which is why the date is in the idempotency key above.

Fails when: you target nothing (listId: null enumerates recipients from existing chat threads and preview returns recipientCount: -1, so you are firing blind), or you run out of credits mid-plan (402 before anything queues, which is the good failure), or the creator's session expires mid-run. In that last case the run moves to PAUSED rather than dying, and resumes once the account is re-linked. More on the broadcast mechanics in the mass DM API.

Job 3 — the content calendar push

Runs: weekly, ahead of the week it covers. Purpose: the creator stops being a bottleneck on Tuesdays.

Read what is already queued before you add to it, or you will double-book a slot:

BASE=https://api.ofapis.com/api/public/v1
AUTH="Authorization: Bearer $OFAPIS_TOKEN"

curl -s -G "$BASE/accounts/$ACCOUNT_ID/scheduled/posts" -H "$AUTH" \
  --data-urlencode "publishDate=2026-03-02" \
  --data-urlencode "publishDateEnd=2026-03-08" \
  --data-urlencode "timeZone=America/New_York" \
  --data-urlencode "limit=100"

curl -s -X POST "$BASE/accounts/$ACCOUNT_ID/scheduled/posts" -H "$AUTH" \
  -H "Content-Type: application/json" -H "Idempotency-Key: cal-2026-03-04-1900" \
  -d '{"mediaFiles":[123456789],"isScheduled":1,"scheduledAt":"2026-03-04T19:00:00Z"}'

Creating a scheduled post or scheduled message costs 3 credits, not 1, so a week of 10 slots per creator is 30 credits before media. Dates on the read are YYYY-MM-DD with a TZ database name, which matters because a creator's "9pm" is a local-time promise. scheduledAt on the POST is an ISO-8601 UTC instant and must be in the future — a past timestamp is a 400, which is the single most common bug in calendar pushes written against a cron that drifted, and a 400 is charged rather than refunded, so a drifted job quietly burns 3 credits per rejected slot. Scheduled messages use the same pattern with userIds or userLists; the broader workflow is in the scheduling tool and the content side in the posts API.

Fails when: media is not in the vault yet. Upload through POST /accounts/{id}/media first and reference the returned ids; a scheduled post referencing nothing is a slot that silently publishes empty. Uploads are the most expensive single call at 5 credits, and the gate is 50 MB per file with image/jpeg, image/png, image/webp and video/mp4 accepted — anything else comes back 415, charged.

Job 4 — the nightly reconciliation

Runs: once, after the day's sends. Purpose: know what actually happened before the creator asks.

For every run you triggered today, GET /accounts/{id}/mailings/{mailingId}/runs/{runId} and record status, recipientCount, successCount, skippedCount and creditsDebited. The number worth watching is skippedCount: recipients who were blocked or unreachable. Credits for skipped recipients are refunded automatically when the run completes, so your reported spend should be creditsDebited minus the refund, and a sudden rise in skips on one creator usually means an audience list has gone stale rather than that anything broke.

Pair this with your own request log — status code, endpoint, X-Request-Id, accountId — and you can answer the two questions that decide contracts: what did this creator cost us this month, and which job is burning credits without producing revenue. There is no statistics or earnings endpoint to fall back on, so this nightly job is the only place per-creator cost will ever exist. The error taxonomy to classify against is in API errors, and the per-creator attribution pattern in multi-account management.

The daily shape, in one table

Job Clock Calls per creator per day Dominant cost
Inbox sync continuous ~96 polls + webhook deliveries polling interval
Segmented drop 2–3× per week 3 calls = 2 credits + 1 per recipient recipients
Calendar push weekly ~10 calls at 3 credits each, 5 per upload media uploads
Reconciliation nightly 1 per run negligible

Broadcasts dominate everything else by an order of magnitude. If your credit line is moving and your revenue is not, it is almost always job 2 — model it against your credit allowance before you add a fourth creator, and keep the route reference open while you wire the jobs. For the prior question of which jobs have an endpoint behind them at all, and which never will, see what OnlyFans automation covers.

FAQ

How long does a mass DM to 5,000 fans take to send?

About forty minutes. Dispatch is paced at roughly two messages per second — one send every 500 ms — and runs asynchronously after trigger returns, so you poll the run record for progress rather than waiting on the request. Schedule drops with that window in mind instead of aiming for an exact minute.

What happens if a creator's session expires during a campaign?

Calls for that account start returning 424, and an in-flight mailing run is paused rather than failed. Once the account is re-linked the run resumes from the recipients it had not reached, so fans who were already messaged are not messaged twice. A 424 refunds the credit for the call; the 422 you get once the account is unlinked outright, or its stored OnlyFans authorization is rejected, does not — so an account nobody re-linked is a slow leak rather than a free pause.

Can I trigger the same mailing twice by accident?

Not while one is running — a second trigger against an in-progress or paused run returns 409, and the trigger call is weighted zero, so the collision itself costs nothing. A finished run is a different matter: triggering the same mailing again recounts the audience, debits again and sends to the same fans again, with no guard at all. Send an Idempotency-Key on every POST — a repeat with the same key replays the original response with Idempotency-Replayed: true and is not charged twice — and make the key contain the day or the campaign, so a retry an hour later is still the same key.

How do I automate different creators on different schedules?

Keep the schedule in your own roster table and pass accountId into every job, rather than writing a job per creator. One worker reading a table of (account_id, job, cron) rows scales to any roster size; a copy-pasted script per creator does not survive the fifth one.

Should automation handle PPV pricing?

Set the price in a mailing or message programmatically, but have a human approve the number. Automating the send is reversible; automating the price means a bad band gets applied to thousands of fans before anybody notices, and refunds are not an API call. The field-by-field mechanics of a priced send are in PPV automation.

Related guides

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