BetterFans Link SDK — OnlyFans APIBetterFans Link SDK
API Reference

Revenue

Pull OnlyFans transactions from a REST endpoint with your API key, or receive them as signed webhooks. Both carry the same transaction shape.

Two ways to get transactions into your own system: pull them from a REST endpoint, or have them pushed to a webhook URL you set on the API Keys page. Both use the same transaction shape.

Pull

GET https://api3.betterfans.link/v1/revenue/transactions
x-service-api-key: <your API key>
QueryMeaning
sinceRFC 3339 instant. Return rows first seen at or after it. Default: the last 24 hours.
cursornextCursor from a previous page. Continue where that page ended.
limit1 to 1000. Default 500.
accountOnlyFans user id. Restrict to one account the key may act as. Required for keys that cannot list their accounts.
{
  "transactions": [
    {
      "id": "1234567890",
      "accountId": "55321703",
      "fanId": "88112233",
      "kind": "tips",
      "description": "Tip",
      "status": "done",
      "grossCents": 1000,
      "netCents": 800,
      "feeCents": 200,
      "vatCents": 0,
      "currency": "USD",
      "occurredAt": "2026-09-07T11:58:02Z",
      "observedAt": "2026-09-07T11:58:40.120Z",
      "deleted": false
    }
  ],
  "count": 1,
  "hasMore": false,
  "nextCursor": "",
  "checkedAt": "2026-09-07T12:00:00.000Z"
}

Rows are ordered by observedAt, the time the row was first seen or seen to change. A transaction can appear more than once with the same id when its status or amounts change, or when it is deleted. Treat the latest row for an id as current.

To poll, store nextCursor and pass it back as cursor. When hasMore is false you are caught up. Do not combine since and cursor.

StatusMeaning
401Missing or invalid key.
403The key may not act as account.
400Bad since or cursor, or the key needs account.

Push

Agency owners and admins can set one webhook URL under API Keys. Every new or changed transaction for the agency's accounts is POSTed once as JSON:

{
  "id": "e7c9d2d4-...",
  "type": "transaction.observed",
  "createdAt": "2026-09-07T11:58:41.003Z",
  "data": { "...": "same fields as a pull row" }
}
HeaderMeaning
x-ofm-eventtransaction.observed, or test for the Send test button.
x-ofm-deliveryUnique id of this delivery. Use it to drop duplicates.
x-ofm-signaturet=<unix seconds>,v1=<hex>, where hex is HMAC-SHA256 over <t>.<raw body> with your signing secret.

Verify with the secret shown by Reveal secret:

import { createHmac, timingSafeEqual } from "node:crypto"

export function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")))
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex")
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300
  return fresh && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""))
}

Respond with any 2xx within 10 seconds. Anything else is retried four more times over about three hours, then the delivery is marked failed. The card shows the endpoint as failing until a delivery succeeds again. The URL must be public https.

On this page