# Uploading media (/introduction/guides/uploading-media)



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

<Callout>
  The full media upload endpoint documentation can be found
  [here](/api-reference/media/upload-media-to-the-only-fans-cdn).
</Callout>

Submit a POST request to our `https://app.onlyfansapi.com/api/{account}/media/upload` endpoint. You can upload media in one of two ways:

1. **File upload** — send the file directly as a multipart form upload using the `file` field.
2. **URL upload** — provide a `file_url` field 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.

<Callout type="warn">
  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.
</Callout>

<Callout>
  The maximum file size for `file` (direct upload) is **100 MB** - this is limited by Cloudflare. The maximum file size for `file_url` (Remote URL upload) is **1 GB**. Need higher limits? Contact us for enterprise options via [Telegram](https://t.me/onlyfansapi) or [email](mailto:hello@onlyfansapi.com?subject=Need%20my%20Media%20Upload%20File%20Size%20Limit%20Increased).
</Callout>

#### Option 1: Upload a file directly

<CodeBlockTabs defaultValue="cURL">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="cURL">
      cURL
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="JavaScript (Fetch)">
      JavaScript (Fetch)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Node.js (Axios)">
      Node.js (Axios)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="PHP (Guzzle)">
      PHP (Guzzle)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="cURL">
    ```bash
    curl --location 'https://app.onlyfansapi.com/api/{account}/media/upload' \
         --header 'Authorization: Bearer {token}' \
         --form 'file=@"/Users/me/Documents/MyVideo.mp4"'
    ```
  </CodeBlockTab>

  <CodeBlockTab value="JavaScript (Fetch)">
    ```ts
    const myHeaders = new Headers();
    myHeaders.append("Authorization", "Bearer {token}");

    const formdata = new FormData();
    formdata.append("file", fileInput.files[0], "MyVideo.mp4");

    const requestOptions = {
      method: "POST",
      headers: myHeaders,
      body: formdata,
      redirect: "follow",
    };

    fetch("https://app.onlyfansapi.com/api/{account}/media/upload", requestOptions)
      .then((response) => response.text())
      .then((result) => console.log(result))
      .catch((error) => console.error(error));
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Node.js (Axios)">
    ```js
    const axios = require("axios");
    const FormData = require("form-data");
    const fs = require("fs");
    let data = new FormData();
    data.append("file", fs.createReadStream("/Users/me/Documents/MyVideo.mp4"));

    let config = {
      method: "post",
      maxBodyLength: Infinity,
      url: "https://app.onlyfansapi.com/api/{account}/media/upload",
      headers: {
        Authorization: "Bearer {token}",
        ...data.getHeaders()
      },
      data: data
    };

    axios
      .request(config)
      .then((response) => {
        console.log(JSON.stringify(response.data));
      })
      .catch((error) => {
        console.log(error);
      });
    ```
  </CodeBlockTab>

  <CodeBlockTab value="PHP (Guzzle)">
    ```php
    $client = new Client();

    $headers = [
        'Authorization' => 'Bearer {token}'
    ];

    $options = [
        'multipart' => [
        [
            'name' => 'file',
            'contents' => Utils::tryFopen('/Users/me/Documents/MyVideo.mp4', 'r'),
            'filename' => '/Users/me/Documents/MyVideo.mp4',
        ]
    ]];

    $request = new Request('POST', 'https://app.onlyfansapi.com/api/{account}/media/upload', $headers);

    $res = $client->sendAsync($request, $options)->wait();

    echo $res->getBody();
    ```
  </CodeBlockTab>
</CodeBlockTabs>

#### 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:

<CodeBlockTabs defaultValue="cURL">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="cURL">
      cURL
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="JavaScript (Fetch)">
      JavaScript (Fetch)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Node.js (Axios)">
      Node.js (Axios)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="PHP (Guzzle)">
      PHP (Guzzle)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="cURL">
    ```bash
    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"}'
    ```
  </CodeBlockTab>

  <CodeBlockTab value="JavaScript (Fetch)">
    ```ts
    const response = await fetch(
      "https://app.onlyfansapi.com/api/{account}/media/upload",
      {
        method: "POST",
        headers: {
          Authorization: "Bearer {token}",
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          file_url: "https://example.com/media/photo.jpg",
        }),
      }
    );

    const result = await response.json();
    console.log(result);
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Node.js (Axios)">
    ```js
    const axios = require("axios");

    let config = {
      method: "post",
      url: "https://app.onlyfansapi.com/api/{account}/media/upload",
      headers: {
        Authorization: "Bearer {token}",
        "Content-Type": "application/json",
      },
      data: {
        file_url: "https://example.com/media/photo.jpg",
      },
    };

    axios
      .request(config)
      .then((response) => {
        console.log(JSON.stringify(response.data));
      })
      .catch((error) => {
        console.log(error);
      });
    ```
  </CodeBlockTab>

  <CodeBlockTab value="PHP (Guzzle)">
    ```php
    $client = new Client();

    $headers = [
        'Authorization' => 'Bearer {token}',
        'Content-Type' => 'application/json',
    ];

    $body = json_encode([
        'file_url' => 'https://example.com/media/photo.jpg',
    ]);

    $request = new Request('POST', 'https://app.onlyfansapi.com/api/{account}/media/upload', $headers, $body);

    $res = $client->sendAsync($request)->wait();

    echo $res->getBody();
    ```
  </CodeBlockTab>
</CodeBlockTabs>

**If the upload was successful, the response will be something as follows:**

```json
{
  "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.

<Callout type="warn">
  **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`.
</Callout>

<Callout type="warn">
  **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:

  * [Upload Media to OnlyFans CDN](/api-reference/media/upload-media-to-the-only-fans-cdn)
  * [Upload Media to OnlyFans Vault](/api-reference/media-vault/upload-media-to-vault)
  * [Get Upload Status (Polling)](/api-reference/media/get-upload-status)
</Callout>

### 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](/api-reference/media-vault/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

<Callout>
  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).
</Callout>

Synchronous vault upload returns **`200`** with the new vault item under `data` (use `data.id` as the vault media ID).

```ts
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](/api-reference/chat-messages/send-message) using `mediaFiles`.

```ts
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

<Callout type="warn">
  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](/api-reference/media/get-upload-status) for statuses (`pending`, `processing`, `completed`, `failed`).
</Callout>

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`.

```ts
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

<Callout>
  The full send post endpoint documentation can be found
  [here](/api-reference/posts/send-post).
</Callout>

Once you have retrieved the correct media IDs (either from the upload response, or from your Vault), you can include them in your post:

```json Example request body
{
  "text": "The text of your post",
  "mediaFiles": ["ofapi_media_123", "1234567890"]
}
```

## Including your media in a chat message

<Callout>
  The full send chat message endpoint documentation can be found
  [here](/api-reference/chat-messages/send-message).
</Callout>

Once you have retrieved the correct media IDs (either from the upload response, or from your Vault), you can include them in your message:

```json Example request body
{
  "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:

1. An OnlyFans API ID starting with `ofapi_media_`. This needs to be the `prefixed_id` of the media file we just uploaded.

   <Callout type="warn">
     **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.
   </Callout>

2. 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](api-reference/media-vault/list-vault-media) endpoint to retrieve this ID.

## Demo video

<Card>
  <iframe src="https://cap.link/gynx4e5rjtt996q" width="100%" height="400px" frameBorder="0" title="OnlyFans API Media Upload tutorial" allowFullScreen />
</Card>
