ofapisv1
guide

Updated

How to Automate OnlyFans DMs

This is the broadcast half of DM automation: one message, many fans, fired on your schedule rather than theirs. It is a different machine from a reply bot — the unit of work is a run that takes tens of minutes and bills per recipient, not a turn that takes a second and bills per call. If you want the reactive side, see building a bot. Here the three questions that matter are what it will cost before you commit, how long it will take, and how you know it finished.

Segment first, because the bill is per recipient

A mailing targets an OnlyFans list. Create one, then add fans by their numeric user id:

curl -X POST https://api.ofapis.com/api/public/v1/accounts/42/lists \
  -H "Authorization: Bearer ofapis_sk_..." -H "Content-Type: application/json" \
  -d '{"name":"vip-spenders"}'

curl -X POST https://api.ofapis.com/api/public/v1/accounts/42/lists/9/users \
  -H "Authorization: Bearer ofapis_sk_..." -H "Content-Type: application/json" \
  -d '{"userIds":[101,102,103]}'

Members are added in a batch to /lists/{listId}/users and removed one at a time with a DELETE on /lists/{listId}/users/{targetUserId}. Watch the read defaults: GET /lists and GET /lists/{listId}/users default to limit=10, not the 20 the rest of the API uses. Enumerating a 400-member list at the default is 40 calls and 40 credits; pass limit=100 and it is 4. The lists API has the rest of the surface.

Membership is explicit, not a live query, so something has to decide who belongs. That something is you: a subscriber record from the subscribers API is {id, username, name, avatar, subscribedOn} and carries no spend figure, so "VIPs" has to be computed from data you already hold — your own payment records, your own tally of PPV unlocks — and synced into the list on a schedule. Any guide that implies the API hands you a spend-ranked audience is describing a field that does not exist. The ids you push in come from your own subscriber sync, and running this across a roster means one list per creator, because lists are account-scoped.

Price the blast before you fire it

A mailing is a stored scenario: message, media, target list, PPV price — it is also the building block behind recurring PPV campaigns. Creating one costs a credit and sends nothing.

curl -X POST https://api.ofapis.com/api/public/v1/accounts/42/mailings \
  -H "Authorization: Bearer ofapis_sk_..." -H "Content-Type: application/json" \
  -d '{"name":"june-drop","text":"new set is up 👀","listId":"9","priceCents":1500,"lockedText":false}'

The returned id is a UUID. Then preview it — also a credit, and the only way to see the bill in advance:

{"data":{"mailingId":"9d4c1e77-3b2a-4f68-a5c9-7e10b2f8d341","listId":"9",
         "recipientCount":214,"perRecipientWeight":1,"estimatedCost":214}}

estimatedCost is recipientCount × perRecipientWeight, and the weight is 1 credit per recipient today. The one case to handle: if the mailing has no listId — meaning all subscribers — the preview returns recipientCount: -1 and estimatedCost: 0, because the audience is not counted without a full enumeration. That is not "free", it is "unknown", and a budget check that treats 0 as cheap will fire a blast it cannot pay for.

The trigger is where the money moves

POST /accounts/{id}/mailings/{mailingId}/trigger takes no body. Inside one call it resolves the audience again, debits recipientCount × perRecipientWeight atomically, writes a run record and returns immediately:

{"data":{"runId":"4b61...","recipientCount":214,"creditsDebited":214}}

The gateway weight on this route is 0; the whole charge is the per-recipient debit, which is why creditsDebited and not a flat number is the figure to log. Three behaviours to design around.

Recipients are re-resolved at trigger time, not read from the preview, so a membership change between the two calls makes them disagree — creditsDebited is authoritative, estimatedCost is a quote. When the mailing targets a list, resolution pages that list 50 at a time; when it targets all subscribers, it enumerates the account's chat conversations instead, so fans you have never exchanged a message with are not in that audience.

If you are short, the debit fails with 402 INSUFFICIENT_CREDITS and nothing is sent. If the resolved audience is empty you get 422, which is one of the charged error codes.

And the double-launch guard is narrower than it looks: 409 is returned only while a run for that mailing is still active. Re-triggering a mailing whose run has completed recounts the audience, debits again and sends the message a second time. The mailing is not an idempotent object — if a retry could reach the trigger twice, put an Idempotency-Key on the POST, which returns the original response with Idempotency-Replayed: true rather than starting a second run.

The run: 500 ms per message

Dispatch is handled by a worker that claims pending runs, sends one message at a time and sleeps 500 ms between them. That throttle is the number to plan around:

Recipients Wall clock
200 ~1.7 minutes
1,000 ~8.3 minutes
5,000 ~42 minutes
20,000 ~2.8 hours

A run moves PENDING → IN_PROGRESS → COMPLETED. Two other terminal-ish states matter. If the OnlyFans session expires mid-run the run goes PAUSED and the account is flagged, and it resumes from the next unsent recipient once the account is re-linked — a 5,000-recipient blast that dies at minute 20 does not restart from zero and does not re-bill. A run whose worker dies is recovered: the claim goes stale after ten minutes and a sweep returns it to PAUSED for pickup.

Recipients that fail individually — blocked, unsubscribed — are marked skipped and the run continues. When it completes, credits for skipped recipients are refunded, which means creditsDebited on a finished run is the pre-refund figure and your real spend is (recipientCount − skippedCount) × perRecipientWeight.

Track it to completion

import time, requests

BASE = "https://api.ofapis.com/api/public/v1/accounts/42/mailings"
H = {"Authorization": "Bearer ofapis_sk_..."}
MAILING = "9d4c1e77-3b2a-4f68-a5c9-7e10b2f8d341"
BUDGET = 3000

prev = requests.post(f"{BASE}/{MAILING}/preview", headers=H).json()["data"]
if prev["recipientCount"] < 0:
    raise SystemExit("audience not countable — set a listId before firing")
if prev["estimatedCost"] > BUDGET:
    raise SystemExit(f"{prev['estimatedCost']} credits exceeds budget {BUDGET}")

fire = {**H, "Idempotency-Key": f"june-drop-{MAILING}"}
run = requests.post(f"{BASE}/{MAILING}/trigger", headers=fire).json()["data"]
run_id = run["runId"]
print("debited", run["creditsDebited"], "for", run["recipientCount"], "recipients")

while True:
    r = requests.get(f"{BASE}/{MAILING}/runs/{run_id}", headers=H).json()["data"]
    print(r["status"], r["successCount"], "sent", r["skippedCount"], "skipped")
    if r["status"] in ("COMPLETED", "FAILED"):
        break
    if r["status"] == "PAUSED":
        alert("mailing paused — check the account link")
    time.sleep(60)

Poll on a minute, not a second: each GET .../runs/{runId} is a credit, and a 42-minute run polled every five seconds costs more than 500 credits to watch. GET .../runs lists every run for the mailing, newest first, which is also how you check whether a completed run already exists before you let a scheduler fire again.

Sending it later

The trigger fires now and accepts no scheduling parameter. For a future send there are two honest options: hold the trigger in your own scheduler, or use POST /accounts/{id}/scheduled/messages, which costs 3 credits, takes text, mediaFiles, a price in cents, a future ISO-8601 scheduledAt and targeting via userLists or userIds, and hands the delivery to OnlyFans' own queue. That route is covered alongside the feed queue in scheduling posts. For PPV pricing on either path see the PPV API, and the full request shapes are in the mass DM reference and the API docs. With broadcasts working, the next step in this series is the reactive side — a bot that answers each fan individually off an inbound event instead of a run.

FAQ

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

About 42 minutes. The dispatch worker sends one message at a time with a 500 ms gap, so wall clock is roughly recipients ÷ 2 seconds. Plan campaigns around that rather than expecting the trigger call to mean delivery.

What happens if I trigger the same mailing twice?

The 409 guard only covers a run that is still active. Once a run has completed, triggering the same mailing again recounts the audience, debits credits again and sends the message a second time. Use an Idempotency-Key on the trigger, or check the run list first.

Why does my preview say recipientCount is -1?

Because the mailing has no listId and therefore targets all subscribers, which is not counted without enumerating the whole audience. estimatedCost comes back 0 in that case — treat it as unknown, not free, and point the mailing at a list if you need a number before firing.

Do I get credits back for fans who never receive the message?

Yes. Recipients that fail — blocked, unsubscribed — are marked skipped, and when the run completes the credits for them are refunded. The creditsDebited field on the run record is the amount taken at trigger time, before that refund lands.

Can I target fans by how much they have spent?

Not from the API alone. A subscriber record contains id, username, name, avatar and subscribedOn and no spend field, so spend-based segments have to be computed from your own records and pushed into a list with POST /lists/{listId}/users.

Related guides

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