ofapisv1
integration

Updated

OnlyFans + n8n

n8n has no OnlyFans node, and ofapis does not publish a community one. Nothing is missing: the whole of ofapis authentication is a single Authorization: Bearer <token> request header, which the core HTTP Request node sends natively. There is no OAuth redirect to register, no request signing, no second secret. Self-hosted, that also means the token, the execution data and the fan records never leave your box — the usual reason agencies running a roster of creators pick n8n over a cloud-only builder.

1. Credential, first node, first 200

  1. Go to Credentials → New credential → Header Auth. Set Name to Authorization and Value to Bearer ofapis_sk_…. Save it as ofapis.
  2. Add an HTTP Request node to a blank workflow.
  3. Set Authentication to Generic Credential Type, Generic Auth Type to Header Auth, and select the ofapis credential.
  4. Method GET, URL https://api.ofapis.com/api/public/v1/me.
  5. Open Options → Response and enable Include Response Headers and Status. You need the headers for the rate-limit work in step 4 below.
  6. Execute the node. A 200 means the token is live.

Every other call is that same node with a different URL. Account-scoped routes put the creator id in the path — /accounts/42/lists, /accounts/42/mailings, /accounts/42/scheduled/posts — so one credential drives a whole roster and only a number in the URL changes between creators. The route list lives in the API documentation, and the HTTP Request node's Import cURL button will fill method, URL and headers from any snippet in the live /docs reference.

2. Expressions have to go through data

A success body is {"data": …} and an error body is {"error": {"code", "message"}}. So in n8n every downstream expression starts one level deeper than people expect:

{{ $json.data.list }}               // array of records
{{ $json.data.hasMore }}            // another page exists
{{ $json.data.nextOffset }}         // offset to request next
{{ $json.data.list[0].username }}

A subscriber record is id, username, name, avatar and subscribedOn — there is no spend field on it, so do not write expressions against one. To turn one response into one n8n item per fan, add a Split Out node with Field to Split Out set to data.list. Every node after it then runs once per fan, which is what makes an If or Switch branch per fan readable.

3. Pagination: configure it, don't hand-roll it

Reads take limit (default 20, clamped to 1–100) and offset (default 0). The HTTP Request node can walk that itself — Options → Pagination:

Keep limit=100 in the URL so each page is as large as the API allows. Message history is the exception: /chats/{chatId}/messages pages on the cursor olderThanMessageId, so set the parameter name to that and feed it the id of the oldest message from the previous page instead of an offset.

Use Loop Over Items for the other kind of loop — fanning a list of fans out into one write each. Set Batch Size to 1 and put a Wait node inside the loop so a 300-fan list does not fire 300 requests in one minute.

4. Rate limits and errors, in node settings

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds); a 429 adds Retry-After in seconds. The ceiling is per token per minute: 60 on Free, 100 on Starter, 500 on Pro — a naive Loop Over Items across a subscriber list will hit it, so read the budget rather than guess at it. Details are on rate limits.

With response headers enabled, the branch is plain n8n wiring: node Settings → On Error → Continue (using error output), then an If node on {{ $json.statusCode === 429 }}, then a Wait node with Wait Amount {{ $json.headers['retry-after'] }} seconds looping back into the request. For everything else, Settings → Retry On Fail with Max Tries 3 is enough. The codes you will actually see are 401 UNAUTHENTICATED, 402 INSUFFICIENT_CREDITS, 403 NO_ACTIVE_SUBSCRIPTION, 409 IDEMPOTENCY_CONFLICT and 429 RATE_LIMIT_EXCEEDED; a malformed query parameter comes back as 400 with a plain string body rather than the error envelope, so don't parse $json.error.code before checking the status. See error codes.

5. Idempotency-Key, because you will re-run this by hand

Any POST accepts an Idempotency-Key header. A repeat with the same key replays the original response, adds Idempotency-Replayed: true, and is not charged again. In the HTTP Request node, turn on Send Headers, add a header parameter named Idempotency-Key, and derive the value from the event, not the run:

welcome-{{ $json.data.list[0].id }}

Using {{ $execution.id }} defeats the whole point — it changes on every execution, so a hand re-run sends the DM twice and bills twice. A key built from the fan is what makes a greeting that fires exactly once per subscriber survive the re-runs you will do while building it.

Worked example: self-hosted auto-reply loop

message.received is the only webhook event that exists, and it is exactly what an auto-reply needs.

  1. Webhook node: method POST, path ofapis-dm, Respond Immediately. Under Options, enable Raw Body so the signature is checked against the exact bytes delivered.

  2. Register that production URL as your webhook endpoint — the registration flow is in webhook setup.

  3. Code node to verify X-Ofapis-Signature, which arrives as t=<unix>,v1=<hex> over "<t>.<raw body>". On self-hosted n8n you need NODE_FUNCTION_ALLOW_BUILTIN=crypto set on the instance for this to run:

    const crypto = require('crypto');
    const item = $input.first();
    const raw = Buffer.from(item.binary.data.data, 'base64').toString('utf8');
    const [t, v1] = item.json.headers['x-ofapis-signature'].split(',').map(p => p.split('=')[1]);
    const mac = crypto.createHmac('sha256', $env.OFAPIS_WEBHOOK_SECRET)
      .update(`${t}.${raw}`).digest('hex');
    if (mac !== v1) throw new Error('bad signature');
    return [{ json: { event: JSON.parse(raw), delivery: item.json.headers['x-ofapis-delivery'] } }];
    
  4. Delivery is at-least-once, so dedupe before acting. A Remove Duplicates node in Keep Items Where → Value Is New mode, keyed on {{ $json.delivery }}, drops the replays.

  5. If node to decide whether this message deserves a reply at all.

  6. HTTP Request node, POST https://api.ofapis.com/api/public/v1/accounts/42/chats/{{ $json.event.data.chatId }}/messages, Send Body on, Body Content Type JSON, with Idempotency-Key set to {{ $json.delivery }}:

    { "text": "Thanks for the message — give me a few minutes.", "price": 0 }
    

One reply through the account-scoped route is two credits. A 429 never reaches metering and a 424 or 5xx is refunded, but a send rejected as malformed — 400, 404, 413, 415, 422 — is charged. Swap the final node for POST /accounts/42/lists/{listId}/users and the same workflow becomes a tagger that drops anyone who messages into a fan list for a later campaign. The same HTTP Request node reaches everything else in the integrations catalogue too.

If the Code nodes keep growing until there is more JavaScript than canvas, that is a signal: the same reply bot written straight in Node.js has fewer moving parts than n8n wrapped around it, and the DM automation walkthrough sets out the decisions that apply either way.

FAQ

Do I need the Header Auth credential, or can I just type the header in?

Both work, because it is only one header. The credential is better: it is encrypted at rest, it does not appear in an exported workflow JSON, and rotating the token updates every node at once instead of node by node.

Which pagination mode matches ofapis offsets?

Pick Update a Parameter in Each Request, with a query parameter named offset and the value {{ $pageCount * 100 }}. Stop with the complete expression {{ $response.body.data.hasMore === false }}. Chat message history is the one route that uses a cursor, olderThanMessageId, instead.

How do I verify a webhook signature inside n8n?

In a Code node, HMAC-SHA256 the string "<t>.<raw body>" with your signing secret and compare the hex against the v1 part of X-Ofapis-Signature. Enable Raw Body on the Webhook node first — re-serialising the parsed JSON changes the bytes and the comparison will always fail.

What happens when a workflow hits the rate limit?

The request returns 429 RATE_LIMIT_EXCEEDED with a Retry-After header in seconds. Set the node to continue on error, branch on the status code, and route through a Wait node that reads that header. A 429 is refused before metering, so a throttled loop costs you time and no credits — unlike a malformed request, which runs and is billed.

Can one workflow manage several creator accounts?

Yes. Account-scoped routes take the id in the path, so put the account id in a workflow variable or an input item and build the URL with an expression. One token and one credential can address every creator linked to it.

Related: Make.com · Zapier · all integrations

Related guides

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