OnlyFans API JavaScript SDK
Full JavaScript and Node.js integration for the OnlyFans API. Uses native fetch (Node 18+) with async/await patterns. No extra dependencies required.
On This Page
Installation
The OnlyFans API uses standard HTTP/REST. In Node.js 18 and later, fetch is available globally — no additional HTTP client is required. For Node.js 16 or earlier, install node-fetch:
# Node 16 or earlier only — Node 18+ has fetch built-in
npm install node-fetch
# For ESM usage with node-fetch v3
npm install node-fetch@3 fetch.
Configuration
All requests require your API key passed as the x-api-key header. Store your key in an environment variable — never hard-code it in source files.
// ofapi.js — shared API client config
const BASE_URL = 'http://157.180.79.226:4024/api/v1';
const headers = {
'x-api-key': process.env.OFAPI_KEY,
'Content-Type': 'application/json',
};
async function ofapi(path, options = {}) {
const res = await fetch(`${BASE_URL}${path}`, {
...options,
headers: { ...headers, ...options.headers },
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message ?? `HTTP ${res.status}`);
}
return res.json();
}
export { ofapi, BASE_URL, headers }; OFAPI_KEY=your_api_key_here Get Revenue Statistics
Retrieve a full revenue breakdown for any creator on your account — subscriptions, tips, PPV messages, and post unlocks. Ideal for building P&L dashboards and payout calculators.
GET /statistics/overview
import { ofapi } from './ofapi.js';
async function getRevenueStats(creatorId) {
const data = await ofapi(
`/statistics/overview?creator_id=${creatorId}`
);
return data;
}
// Usage
const stats = await getRevenueStats('creator_abc123');
console.log(`Total earnings: $${stats.total_earnings}`);
console.log(`Subscriptions: $${stats.subscriptions}`);
console.log(`Tips: $${stats.tips}`);
console.log(`PPV: $${stats.ppv_messages + stats.ppv_posts}`); {
"creator_id": "creator_abc123",
"total_earnings": 12847.50,
"subscriptions": 4200.00,
"tips": 3100.25,
"ppv_messages": 3847.00,
"ppv_posts": 1200.25,
"referrals": 500.00,
"chargebacks": -0.00,
"period": "all_time",
"currency": "USD"
} List Top Fans
Return a ranked list of your highest-spending fans by total spend. Use this to power VIP fan identification, high-value outreach campaigns, and churn-risk monitoring for big spenders.
GET /stats/fans/top
import { ofapi } from './ofapi.js';
async function getTopFans(creatorId, limit = 10) {
const data = await ofapi(
`/stats/fans/top?creator_id=${creatorId}&limit=${limit}`
);
return data.fans;
}
// Get top 5 fans for a creator
const fans = await getTopFans('creator_abc123', 5);
fans.forEach((fan, i) => {
console.log(
`#${i + 1} ${fan.username} — $${fan.total_spent} total`
);
}); {
"fans": [
{
"user_id": "fan_001",
"username": "bigspender99",
"total_spent": 3420.00,
"tips_total": 1800.00,
"ppv_total": 1420.00,
"subscriptions_total": 200.00,
"subscriber_since": "2024-03-15",
"is_active": true
},
{
"user_id": "fan_002",
"username": "whale_dan",
"total_spent": 2150.50,
"tips_total": 950.50,
"ppv_total": 1100.00,
"subscriptions_total": 100.00,
"subscriber_since": "2024-07-02",
"is_active": true
}
],
"total_count": 847
} Track Payouts
Retrieve the full payout transaction history for a creator. Use this to reconcile earnings, automate agency commission splits, and build payout forecasting tools.
GET /payouts/transactions
import { ofapi } from './ofapi.js';
async function getPayouts(creatorId, page = 1) {
const data = await ofapi(
`/payouts/transactions?creator_id=${creatorId}&page=${page}&limit=20`
);
return data;
}
// Calculate total paid out this month
const { transactions } = await getPayouts('creator_abc123');
const now = new Date();
const thisMonth = transactions.filter(t => {
const d = new Date(t.date);
return d.getMonth() === now.getMonth()
&& d.getFullYear() === now.getFullYear();
});
const total = thisMonth.reduce((sum, t) => sum + t.amount, 0);
console.log(`Paid out this month: $${total.toFixed(2)}`); {
"transactions": [
{
"id": "txn_8f3a91",
"amount": 3240.00,
"status": "completed",
"method": "bank_transfer",
"date": "2026-03-03T09:00:00Z",
"period_start": "2026-02-24",
"period_end": "2026-03-02"
},
{
"id": "txn_7c2b14",
"amount": 2980.50,
"status": "completed",
"method": "bank_transfer",
"date": "2026-02-24T09:00:00Z",
"period_start": "2026-02-17",
"period_end": "2026-02-23"
}
],
"page": 1,
"total_pages": 8,
"total_count": 156
} Get Subscriber Analytics
Pull ranked subscriber data sorted by spend. Combine with fan behavior data to identify high-LTV profiles and build automated retention workflows.
GET /subscribers/top
import { ofapi } from './ofapi.js';
async function getTopSubscribers(creatorId, limit = 20) {
const data = await ofapi(
`/subscribers/top?creator_id=${creatorId}&limit=${limit}`
);
return data.subscribers;
}
// Flag subscribers who haven't purchased PPV in 14+ days
const subs = await getTopSubscribers('creator_abc123');
const cutoff = Date.now() - 14 * 24 * 60 * 60 * 1000;
const atRisk = subs.filter(s =>
s.total_spent > 200 &&
new Date(s.last_purchase_at).getTime() < cutoff
);
console.log(`${atRisk.length} high-value subs at churn risk`); {
"subscribers": [
{
"user_id": "fan_001",
"username": "bigspender99",
"total_spent": 3420.00,
"subscription_price": 14.99,
"subscribed_since": "2024-03-15",
"last_purchase_at": "2026-03-01T14:22:10Z",
"rebill_count": 24,
"is_active": true
}
],
"total": 847
} Send a Message
Send a direct message to a fan — optionally with a PPV price lock and media attachment. Power your chatting team's automation tools, drip sequences, and re-engagement blasts.
POST /messages/send
import { ofapi } from './ofapi.js';
async function sendMessage({ creatorId, fanId, text, ppvPrice, mediaIds }) {
const body = {
creator_id: creatorId,
fan_id: fanId,
text,
...(ppvPrice && { ppv_price: ppvPrice }),
...(mediaIds?.length && { media_ids: mediaIds }),
};
const result = await ofapi('/messages/send', {
method: 'POST',
body: JSON.stringify(body),
});
return result;
}
// Send a PPV re-engagement message
const result = await sendMessage({
creatorId: 'creator_abc123',
fanId: 'fan_001',
text: 'Hey! I made something special just for you 🔥',
ppvPrice: 12,
mediaIds: ['media_xyz789'],
});
console.log(`Message sent: ${result.message_id}`); {
"message_id": "msg_4d9f2a",
"status": "sent",
"creator_id": "creator_abc123",
"fan_id": "fan_001",
"ppv_price": 12,
"has_media": true,
"sent_at": "2026-03-05T11:42:00Z"
} TypeScript Types
If you are using TypeScript, add these interfaces to a types/ofapi.d.ts file for full type safety across all API calls.
/** Revenue overview for a single creator */
export interface RevenueStats {
creator_id: string;
total_earnings: number;
subscriptions: number;
tips: number;
ppv_messages: number;
ppv_posts: number;
referrals: number;
chargebacks: number;
period: string;
currency: string;
}
/** A single fan ranked by total spend */
export interface TopFan {
user_id: string;
username: string;
total_spent: number;
tips_total: number;
ppv_total: number;
subscriptions_total: number;
subscriber_since: string;
is_active: boolean;
}
/** A payout transaction record */
export interface PayoutTransaction {
id: string;
amount: number;
status: 'pending' | 'completed' | 'failed';
method: string;
date: string;
period_start: string;
period_end: string;
}
/** Outbound message result */
export interface MessageResult {
message_id: string;
status: 'sent' | 'queued' | 'failed';
creator_id: string;
fan_id: string;
ppv_price?: number;
has_media: boolean;
sent_at: string;
} Error Handling
All API errors return a JSON body with a message field and a standard HTTP status code. Wrap calls in try/catch and branch on the status code for retries and alerting.
import { BASE_URL, headers } from './ofapi.js';
async function safeApiCall(path, options = {}) {
try {
const res = await fetch(`${BASE_URL}${path}`, {
...options,
headers: { ...headers, ...options.headers },
});
if (res.status === 401) {
throw new Error('Invalid API key — check OFAPI_KEY env var');
}
if (res.status === 403) {
throw new Error('Creator not on your account');
}
if (res.status === 429) {
// Respect rate limits — back off and retry
const retryAfter = res.headers.get('Retry-After') ?? '5';
await new Promise(r => setTimeout(r, parseInt(retryAfter) * 1000));
return safeApiCall(path, options); // one retry
}
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message ?? `HTTP ${res.status}`);
}
return res.json();
} catch (err) {
if (err.name === 'TypeError') {
// Network error — API unreachable
throw new Error(`Network error reaching OFAPI: ${err.message}`);
}
throw err;
}
} | Status | Meaning | Action |
|---|---|---|
| 200 | OK | Parse and use the response body |
| 400 | Bad Request | Check required params; read message |
| 401 | Unauthorized | Verify x-api-key header is correct |
| 403 | Forbidden | Creator not linked to your API account |
| 429 | Rate Limited | Back off using Retry-After header |
| 500 | Server Error | Retry with exponential backoff; contact support |