OnlyFans + Zapier
No OnlyFans app exists in Zapier's directory, and ofapis has not published one. The authentication is small enough that a generic HTTP action covers it: one Authorization: Bearer <token> header, no OAuth screen, no signing step. Webhooks by Zapier sends that header, so every route is reachable from a Zap today. Zapier is also the most constrained tool here — it has no real loop — so this page says plainly where that bites.
1. Build the action step
- Add an action step and search for Webhooks by Zapier.
- Choose the Custom Request event. Not "POST" and not "GET": those presets hide or mangle the fields you need, and Custom Request is the only one that passes a raw body through untouched.
- Method:
GETto read,POSTto write. - URL: the whole endpoint, e.g.
https://api.ofapis.com/api/public/v1/accounts/42/subscribers?type=active&limit=100. - Data: for a POST, the JSON body as text. Leave it empty on a GET.
- Unflatten:
no, so nested JSON you send stays nested. - Headers: two rows —
Authorizationwith valueBearer ofapis_sk_…, andContent-Typewith valueapplication/json. - Test the step. The sample output is the live response, and Zapier remembers its field names for the steps below it.
Store the token as an account-level custom variable (Settings → Custom Variables) and reference it in the header instead of typing it into each step; rotation then touches one place. See authentication.
2. Reading the response in later steps
ofapis returns {"data": …} on success and {"error": {"code", "message"}} on failure. Zapier flattens that into underscore paths in the field picker, so the first fan of a subscriber page is:
data__list__0__id
data__list__0__username
data__list__0__name
data__hasMore
data__nextOffset
A subscriber record carries id, username, name, avatar and subscribedOn. There is no spend or lifetime-value field, so do not design a Zap around one — subscribers lists what comes back, and the API without scraping covers what is and is not exposed at all.
3. Zapier cannot loop, and that matters here
Reads take limit (default 20, clamped to 1–100) and offset (default 0). Walking those pages needs a loop that feeds its own next request — and a plain Zap has no such construct. Three honest options, in order of preference:
- Don't page. Ask for
limit=100once. For most creators one page is the whole working set, and the step costs one credit. - Put the loop in code. A Code by Zapier (JavaScript) step can
fetchseveral pages and return the merged array; the Node.js request examples drop in with only therequirelines removed. Code steps have a short runtime ceiling that varies by plan — check yours first, and if the code is outgrowing it, Pipedream gives the same JavaScript a full Node runtime, a key/value store andnode:crypto. - Looping by Zapier iterates a list you already hold. It will fan 100 fans out into 100 cycles, but it cannot discover page two, and every cycle consumes a task.
If walking a full roster is the job rather than an edge case, that is the honest moment to move tools: n8n's HTTP Request node pages until hasMore is false by itself, and Make handles it with a bounded Repeater.
One trick is worth knowing: a Code by Zapier step that returns an array of objects makes every following step run once per object. That is how you get a per-fan action out of a single API call, without the Looping add-on.
Message history is a different shape — /chats/{chatId}/messages pages on the cursor olderThanMessageId, so each page needs the previous page's oldest message id, again a Code step's job.
4. Errors, retries and what a 429 looks like
A non-2xx response puts the Zap into Errored and Zapier emails you; replay it from the task history. Whether that failure cost anything depends on the status. Credits are debited before the handler runs: 401, 402, 403 and 429 are refused before metering and cost nothing, a 409, a 424 or any 5xx is debited and then refunded, but a 400, 404, 413, 415 or 422 stays charged — and so does every replay of it.
The codes worth a Filter step are 401 UNAUTHENTICATED (bad or revoked token), 402 INSUFFICIENT_CREDITS, 403 NO_ACTIVE_SUBSCRIPTION, 409 IDEMPOTENCY_CONFLICT and 429 RATE_LIMIT_EXCEEDED. A malformed query parameter returns 400 with a plain string body rather than the error envelope, so a step that tries to read error.code from it finds nothing. See error codes.
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds), and a 429 adds Retry-After in seconds. Per token per minute the ceiling is 60 on Free, 100 on Starter, 500 on Pro. Zapier has no sleep action, so the way to stay under it is Delay by Zapier → Delay For between steps, or a slower trigger interval — more on that in rate limits.
5. Where the Idempotency-Key goes
Any POST accepts an Idempotency-Key header: add it as a third row in Headers on the Custom Request step. A repeat with the same key replays the original response, returns Idempotency-Replayed: true, and is not charged again. This matters more in Zapier than anywhere else, because replaying an errored task from the history is one click and easy to click twice. Key it off the fan or the event — welcome-{{fan_id}} — never off a timestamp.
Worked example: new subscriber → welcome DM
message.received is the only webhook event that exists, so there is no "new subscriber" push to catch. A new-subscriber Zap has to poll, and this is how to poll it without double-sending; what the greeting itself should do is a separate decision from the plumbing below.
Trigger — Schedule by Zapier → Every Hour.
Action — Webhooks by Zapier → Custom Request,
GET https://api.ofapis.com/api/public/v1/accounts/42/subscribers?type=active&limit=100, with the two headers from step 1 above.Action — Webhooks by Zapier → Custom Request,
GET …/accounts/42/chats?limit=100. Each entry indata.listis a conversation described by awithUserobject pluscanSendMessageandunreadMessagesCount. Use it to confirm a conversation is open before you write to it, instead of posting blind.Action — Code by Zapier (JavaScript). Read the high-water mark from Storage by Zapier, keep only fans you have not seen and can message, and return them as an array so the rest of the Zap runs once per new fan:
const seen = new Set(JSON.parse(inputData.seenIds || '[]')); const fans = JSON.parse(inputData.subscribers).data.list; const chats = JSON.parse(inputData.chats).data.list; const open = new Set(chats.filter(c => c.canSendMessage).map(c => String(c.withUser.id))); return fans .filter(f => !seen.has(String(f.id)) && open.has(String(f.id))) .map(f => ({ fanId: f.id, username: f.username }));Filter by Zapier: continue only if
fanIdexists, so an empty run stops here instead of firing a malformed request.Action — Webhooks by Zapier → Custom Request,
POST https://api.ofapis.com/api/public/v1/accounts/42/chats/{{fanId}}/messages, withIdempotency-Keyset towelcome-{{fanId}}and this body. The{chatId}segment of the messaging route is the fan's OnlyFans user id for a one-to-one thread — run one call by hand against /docs to confirm it before you leave the Zap on:{ "text": "Thanks for subscribing — here is what is coming this week.", "price": 0, "mediaFiles": [] }Action — Storage by Zapier → Set Value: write the new ids back so the next hour's run skips them.
One send is one credit. To reach a whole segment in one call instead of one fan at a time, point the same Custom Request step at the mailings resource described under mass DM — it bills recipientCount × perRecipientWeight, not one credit, which pricing spells out. For the reactive half of the job, point an ofapis webhook at a Catch Hook trigger URL per webhook setup and dedupe on the X-Ofapis-Delivery header, because delivery is at-least-once.
FAQ
Is there an OnlyFans trigger in Zapier?
No. The only push ofapis offers is the message.received webhook, which you catch with Webhooks by Zapier → Catch Hook. Anything else — new subscribers, new tips — has to be polled on a Schedule by Zapier trigger and deduped on your side.
How do I get more than one record out of a Custom Request step?
Return an array of objects from a Code by Zapier step. Every step below it then runs once per object — the only built-in fan-out that does not need the Looping add-on. A Custom Request step itself always produces exactly one output bundle.
Can a Zap page through all my subscribers?
Not natively. Ask for limit=100 and accept one page, or do the paging inside a Code by Zapier step that fetches each offset itself. Looping by Zapier iterates a list you already hold; it cannot discover the next page.
Where do I put the Idempotency-Key?
As a row in the Headers section of the Custom Request step, on every POST. Give it a value tied to the thing being done — a fan id, a campaign date — so a replayed task returns the stored response instead of sending again.
Does a failed Custom Request cost me a credit?
It costs a Zapier task but no ofapis credit. A 401 or 429 is refused before metering and a 5xx is refunded, so neither costs anything — including replays from the task history. A malformed request that reaches the API and returns 400 or 404 is charged, so fix the payload rather than re-running the Zap to see what happens.
Related: Make.com · n8n · all integrations