Uploading media
Learn how to upload photos/videos, and include them in your posts or messages.
OnlyFans API makes it really easy to include photos and videos in your posts and messages. You can upload media files directly to our API, and then reference them in our relevant endpoints.
Prepare the relevant photo, video or audio
- If you want to use a new photo or video, you may upload it to our API.
- Alternatively, if you want to use a photo or video from your Vault, you may reference its ID directly in the relevant endpoints.
Upload your photo or video to our API
The full media upload endpoint documentation can be found here.
Submit a POST request to our https://app.onlyfansapi.com/api/{account}/media/upload endpoint. You can upload media in one of two ways:
- File upload — send the file directly as a multipart form upload using the
filefield. - URL upload — provide a
file_urlfield containing an HTTPS URL pointing to the media file. Our server will download it and upload it to the OnlyFans CDN on your behalf. This is useful when the media already exists at a remote URL and you want to avoid downloading it locally first.
You must provide exactly one of file or file_url — not both, and not neither. Providing both or neither will result in a 422 validation error.
Option 1: Upload a file directly
curl --location 'https://app.onlyfansapi.com/api/{account}/media/upload' \
--header 'Authorization: Bearer {token}' \
--form 'file=@"/Users/me/Documents/MyVideo.mp4"'Option 2: Upload from a URL
If your media is already hosted at a remote URL, you can pass it via file_url instead of uploading the file directly:
curl -X POST 'https://app.onlyfansapi.com/api/{account}/media/upload' \
-H 'Authorization: Bearer {token}' \
-H 'Content-Type: application/json' \
-d '{"file_url": "https://example.com/media/photo.jpg"}'If the upload was successful, the response will be something as follows:
{
"prefixed_id": "ofapi_media_123",
"file_name": "MyVideo.mp4",
"processId": "abc123",
"host": "convert1.onlyfans.com",
"sourceUrl": "https://of2transcoder.s3.amazonaws.com/upload/xxx/xxx/MyVideo.mp4?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Security-Token=xxx&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=xxx&X-Amz-Date=xxx&X-Amz-SignedHeaders=host&X-Amz-Expires=604800&X-Amz-Signature=xxx",
"extra": "xxx",
"additional": {
"user": "169224851"
},
"thumbs": [
{
"id": 1,
"url": "https://cdn2.onlyfans.com/files/f/f0/xxx/300x300_xxx.jpg?Expires=xxx&Signature=xxx&Key-Pair-Id=xxx"
}
],
"note": "Maximum file size is 1 GB. Need higher limits? Contact us for enterprise options."
}You may use the prefixed_id (e.g., ofapi_media_123) in our relevant endpoints to include this media file.
Important!
The prefixed_id from the above request can only be used once. After you use it in a post or message, it will no longer be valid for future use. If you want to reuse the same media file, you must upload it again to get a new prefixed_id.
Important for files over 50 MB!
We STRONGLY recommend uploading large files (either from file_url or file) to use the async=true body parameter.
This will result in a much faster and more reliable upload process as we handle the uploading in the background.
Please refer to docs for:
Validation rules
| Scenario | Result |
|---|---|
file provided, no file_url | Valid |
file_url provided, no file | Valid |
| Both provided | 422 error |
| Neither provided | 422 error |
File in file exceeds 100 MB (limited by Cloudflare) | 422 error |
File in file_url exceeds 1 GB | 422 error |
Code examples
This tutorial walks through uploading images, videos, or audio into your OnlyFans vault with the API, then sending a chat message with that media attached. Vault uploads use POST https://app.onlyfansapi.com/api/{account}/media/vault with multipart/form-data: send either a file part or a file_url part (exactly one), and optionally async for background processing. See Upload Media to Vault for full payloads and responses.
The snippets below are Node.js (TypeScript) using global fetch (Node 18+). Install form-data for multipart vault uploads (npm install form-data). Streaming multipart bodies require duplex: "half" on the fetch options (shown in the examples). For TypeScript, add npm install -D @types/node.
Direct multipart upload
The maximum size for direct multipart file uploads is 100 MB, enforced by Cloudflare. For larger files, use file_url and/or async=true (see below).
Synchronous vault upload returns 200 with the new vault item under data (use data.id as the vault media ID).
import FormData from "form-data";
import { createReadStream } from "node:fs";
import { basename } from "node:path";
const BASE = "https://app.onlyfansapi.com";
const account = "acct_XXXXXXXXXXXXXXX";
const apiKey = "ofapi_XXXXXXXXXXXXXXXXXXXXXXXXXXXX";
async function uploadVaultFromFile(filePath: string): Promise<number> {
const form = new FormData();
form.append("file", createReadStream(filePath), basename(filePath));
const res = await fetch(`${BASE}/api/${account}/media/vault`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
...form.getHeaders(),
},
body: form as unknown as BodyInit,
duplex: "half",
} as RequestInit & { duplex: "half" });
if (!res.ok) throw new Error(await res.text());
const json = (await res.json()) as { data: { id: number } };
return json.data.id;
}
// Example: upload a local audio/video/image file
// const vaultId = await uploadVaultFromFile("/path/to/clip.mp4");Upload from a remote URL, then send a chat message
After a synchronous vault upload, the vault media ID is data.id. Attach it with Send Message using mediaFiles.
import FormData from "form-data";
const BASE = "https://app.onlyfansapi.com";
const account = "acct_XXXXXXXXXXXXXXX";
const apiKey = "ofapi_XXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const chatId = "123456789"; // fan / chat id from List Chats
async function uploadVaultFromUrl(fileUrl: string): Promise<number> {
const form = new FormData();
form.append("file_url", fileUrl);
const res = await fetch(`${BASE}/api/${account}/media/vault`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
...form.getHeaders(),
},
body: form as unknown as BodyInit,
duplex: "half",
} as RequestInit & { duplex: "half" });
if (!res.ok) throw new Error(await res.text());
const json = (await res.json()) as { data: { id: number } };
return json.data.id;
}
async function sendMessageWithVaultMedia(vaultMediaId: number) {
const res = await fetch(`${BASE}/api/${account}/chats/${chatId}/messages`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "Here is the media you asked for.",
mediaFiles: [vaultMediaId],
}),
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
async function exampleUrlUploadThenMessage() {
const vaultId = await uploadVaultFromUrl("https://example.com/media/photo.jpg");
await sendMessageWithVaultMedia(vaultId);
}(Recommended) Upload asynchronously, then poll until complete, then message
For files over ~50 MB, prefer async=true: the upload runs in the background and you poll for completion instead of holding one long HTTP request that might timeout. See Get Upload Status for statuses (pending, processing, completed, failed).
With async=true, the API responds with 202 and a polling_url. Poll that URL (no extra credits) until status is completed or failed. On success, use prefixed_id (ofapi_media_…) and/or media.id in mediaFiles.
import FormData from "form-data";
import { createReadStream } from "node:fs";
import { basename } from "node:path";
const BASE = "https://app.onlyfansapi.com";
const account = "acct_XXXXXXXXXXXXXXX";
const apiKey = "ofapi_XXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const chatId = "123456789";
type UploadStatus =
| { status: "pending" | "processing"; prefixed_id: string }
| { status: "failed"; prefixed_id: string; error: string }
| {
status: "completed";
prefixed_id: string;
media: { id: number };
credits_used?: number;
};
type AsyncStart = {
status: string;
prefixed_id: string;
polling_url: string;
};
async function uploadVaultAsync(opts: {
filePath?: string;
file_url?: string;
}): Promise<AsyncStart> {
const form = new FormData();
form.append("async", "true");
if (opts.file_url) form.append("file_url", opts.file_url);
else if (opts.filePath)
form.append("file", createReadStream(opts.filePath), basename(opts.filePath));
else throw new Error("Provide filePath or file_url");
const res = await fetch(`${BASE}/api/${account}/media/vault`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
...form.getHeaders(),
},
body: form as unknown as BodyInit,
duplex: "half",
} as RequestInit & { duplex: "half" });
if (res.status !== 202) {
throw new Error(await res.text());
}
return (await res.json()) as AsyncStart;
}
type CompletedUpload = Extract<UploadStatus, { status: "completed" }>;
async function waitForUploadComplete(
pollingUrl: string,
opts?: { intervalMs?: number }
): Promise<CompletedUpload> {
const interval = opts?.intervalMs ?? 2000;
for (;;) {
const res = await fetch(pollingUrl, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) throw new Error(await res.text());
const body = (await res.json()) as UploadStatus;
if (body.status === "completed") return body;
if (body.status === "failed")
throw new Error(body.error ?? "Upload failed");
await new Promise((r) => setTimeout(r, interval));
}
}
async function sendMessageWithPrefixedOrVaultId(done: CompletedUpload) {
const res = await fetch(`${BASE}/api/${account}/chats/${chatId}/messages`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "Large file is ready.",
// `prefixed_id` or `media.id` — both are accepted in mediaFiles
mediaFiles: [done.prefixed_id],
}),
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
// Example: async from remote URL (good for large files)
async function exampleAsyncVaultThenMessage() {
const started = await uploadVaultAsync({
file_url: "https://example.com/media/large-video.mp4",
});
const done = await waitForUploadComplete(started.polling_url);
await sendMessageWithPrefixedOrVaultId(done);
}
// Example: async from a local file path
async function exampleAsyncLocalFileThenMessage() {
const started = await uploadVaultAsync({
filePath: "/path/to/large-video.mp4",
});
const done = await waitForUploadComplete(started.polling_url);
await sendMessageWithPrefixedOrVaultId(done);
}Including your media in a new post
The full send post endpoint documentation can be found here.
Once you have retrieved the correct media IDs (either from the upload response, or from your Vault), you can include them in your post:
{
"text": "The text of your post",
"mediaFiles": ["ofapi_media_123", "1234567890"]
}Including your media in a chat message
The full send chat message endpoint documentation can be found here.
Once you have retrieved the correct media IDs (either from the upload response, or from your Vault), you can include them in your message:
{
"text": "The text of your message",
"mediaFiles": ["ofapi_media_123", "1234567890"]
}Media file array options
You can include two different types of IDs in the mediaFiles array:
-
An OnlyFans API ID starting with
ofapi_media_. This needs to be theprefixed_idof the media file we just uploaded.Important! The
ofapi_media_ID from a media file upload can only be used once.After you use it in a post or message, it will no longer be valid for future use. To use the media again, you must use the OnlyFans Vault Media ID.
-
An OnlyFans Vault Media ID like
1234567890. This is the OnlyFans ID of a media file that already exists in the Vault. Use our List Vault Media endpoint to retrieve this ID.