> ## Documentation Index
> Fetch the complete documentation index at: https://docs.onlyfansapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Downloading Media (/introduction/guides/downloading-media)

OnlyFans API makes it easy to download media files directly from OnlyFans and Fansly CDN URLs. You can download photos, videos, and audio files by calling our download endpoint with the CDN URL.

Each platform has its own endpoint: [OnlyFans](#download-media-from-onlyfans-cdn) below, or jump to [Fansly](#downloading-media-from-the-fansly-cdn).

## Download media from OnlyFans CDN

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

Media can be downloaded by calling GET `https://app.onlyfansapi.com/api/{account}/media/download/{ONLYFANS_CDN_URL}`.

The `{ONLYFANS_CDN_URL}` parameter should be a URL from any OnlyFans CDN subdomain (i.e. anything matching `https://cdn*.onlyfans.com/*`, such as `cdn2.onlyfans.com`, `cdn3.onlyfans.com`, etc.). A typical URL looks like `https://cdn2.onlyfans.com/files/e/e5/123/600x400_123.jpg?Tag=2&u=123&Policy=123&Signature=signature&Key-Pair-Id=123`. You can just enter the OnlyFans CDN URL after the `.../media/download/{ONLYFANS_CDN_URL}` without any need for encoding the URL.

<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/download/https://cdn2.onlyfans.com/files/e/e5/123/600x400_123.jpg?Tag=2&u=123&Policy=123&Signature=signature&Key-Pair-Id=123' \
         --header 'Authorization: Bearer {token}' \
         --output 'downloaded-media.jpg'
    ```
  </CodeBlockTab>

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

    const cdnUrl =
      "https://cdn2.onlyfans.com/files/e/e5/123/600x400_123.jpg?Tag=2&u=123&Policy=123&Signature=signature&Key-Pair-Id=123";

    const requestOptions = {
      method: "GET",
      headers: myHeaders,
    };

    fetch(
      `https://app.onlyfansapi.com/api/{account}/media/download/${cdnUrl}`,
      requestOptions
    )
      .then((response) => response.blob())
      .then((blob) => {
        // Save the blob to a file or process it
        const url = window.URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = "downloaded-media.jpg";
        a.click();
      })
      .catch((error) => console.error(error));
    ```
  </CodeBlockTab>

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

    const cdnUrl =
      "https://cdn2.onlyfans.com/files/e/e5/123/600x400_123.jpg?Tag=2&u=123&Policy=123&Signature=signature&Key-Pair-Id=123";

    let config = {
      method: "get",
      url: `https://app.onlyfansapi.com/api/{account}/media/download/${cdnUrl}`,
      headers: {
        Authorization: "Bearer {token}",
      },
      responseType: "stream",
    };

    axios
      .request(config)
      .then((response) => {
        response.data.pipe(fs.createWriteStream("downloaded-media.jpg"));
      })
      .catch((error) => {
        console.log(error);
      });
    ```
  </CodeBlockTab>

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

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

    $cdnUrl = "https://cdn2.onlyfans.com/files/e/e5/123/600x400_123.jpg?Tag=2&u=123&Policy=123&Signature=signature&Key-Pair-Id=123";

    $request = new Request('GET', "https://app.onlyfansapi.com/api/{account}/media/download/{$cdnUrl}", $headers);

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

    file_put_contents('downloaded-media.jpg', $res->getBody());
    ```
  </CodeBlockTab>
</CodeBlockTabs>

The endpoint streams the media file directly as a binary response, forwarding the upstream `Content-Type` from the OnlyFans CDN (e.g. `image/jpeg`, `video/mp4`, `audio/mpeg`). Save the response bytes to disk or process them as needed. There's no intermediate URL or re-hosted copy.

<Callout title="Detecting file type" type="info">
  Signed CDN URLs often carry query parameters (e.g. `?Tag=…&Signature=…`) and may not have a clean file extension. Inspect the response's `Content-Type` header to determine how to handle the file rather than parsing the URL.
</Callout>

## Downloading DRM-protected videos

Some Media Vault videos have **DRM (Widevine)** enabled. These return `files.full.url = null` from the Vault endpoints and have no plain CDN URL to pass to the endpoint above, so they use a separate endpoint that takes the **vault media ID** (`data.id`) instead of a CDN URL:

Call GET `https://app.onlyfansapi.com/api/{account}/media/download/drm/{media_id}`. It resolves the Widevine license, decrypts the video and audio, and returns a standard MP4.

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

  <CodeBlockTab value="cURL">
    ```bash
    curl --location 'https://app.onlyfansapi.com/api/{account}/media/download/drm/2381923812' \
         --header 'Authorization: Bearer {token}' \
         --output 'downloaded-video.mp4'
    ```
  </CodeBlockTab>
</CodeBlockTabs>

A few things differ from the regular download endpoint above:

* The response is a `302` redirect to `dl.fansapi.com`, which streams the decrypted file. Your client must follow redirects (`curl` needs `--location`).
* Expect roughly 8 to 15 seconds before the first byte arrives while the license exchange and decrypt run. Repeat downloads of the same video are faster, because the content keys are cached.
* Credits are charged for the bytes actually delivered.

<Callout title="Which videos are DRM-protected?" type="info">
  A video is DRM-protected when its Media Vault item returns `files.full.url = null` and a `files.drm` block. Non-DRM media keeps working through the regular [Download Media from the OnlyFans CDN](/api-reference/media/download-media-from-the-only-fans-cdn) endpoint. See [Download DRM-protected Media](/api-reference/media/download-drm-protected-media) for the full reference.
</Callout>

## Downloading media from the Fansly CDN

Fansly media works the same way in spirit, but through a separate endpoint that takes a `fansly_acct_…` ID:

Call GET `https://app.onlyfansapi.com/api/fansly/{fanslyAccount}/media/download/{FANSLY_CDN_URL}`.

The `{FANSLY_CDN_URL}` parameter should be a URL from any Fansly CDN subdomain (anything matching `https://cdn*.fansly.com/*`, such as `cdn3.fansly.com`). As with OnlyFans, paste the URL straight after `.../media/download/` — no encoding needed.

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

  <CodeBlockTab value="cURL">
    ```bash
    curl --location 'https://app.onlyfansapi.com/api/fansly/{fanslyAccount}/media/download/https://cdn3.fansly.com/100000000000000001/300000000000000001.jpeg?ngsw-bypass=true&Expires=1782917529&Key-Pair-Id=K23PG5J1AWEZX5&Signature=signature' \
         --header 'Authorization: Bearer {token}' \
         --output 'downloaded-media.jpg'
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Two differences from the OnlyFans endpoint are worth knowing before you build against it:

* **It always returns a `302` redirect**, never a binary body. If we already have the file cached it redirects to a signed `cdn.fansapi.com` URL; if not, it redirects to the original Fansly URL and queues the file for caching in the background, so the next call is served from our edge. Your client must follow redirects (`curl` needs `--location`).
* **The signed query parameters are required.** Fansly CDN URLs carry `Expires`, `Key-Pair-Id` and `Signature`. Strip any of them and the endpoint returns a `422` telling you which are missing, because we cannot fetch the file to cache it.

<Callout title="Fansly URLs in API responses may already point at our CDN" type="info">
  Endpoints like [List Chat Messages](/api-reference/fansly/chat-messages/list-chat-messages) and [Get Account Media](/api-reference/fansly/media/get-account-media) rewrite media URLs we have already cached to short-lived `cdn.fansapi.com` URLs. Those are fetchable directly — no need to route them back through the download endpoint (though doing so is harmless; it just redirects them straight back). Uncached URLs are returned as the original `cdn*.fansly.com` URL. See [Endpoint Data Sources](/introduction/essentials/endpoint-data-sources#media-urls--file-downloads) for the full picture.
</Callout>

<Callout title="Fansly does not use DRM" type="info">
  There is no Fansly equivalent of the OnlyFans [DRM download endpoint](#downloading-drm-protected-videos), and none is needed — Fansly video is not Widevine-protected. Every Fansly media file goes through the endpoint above.

  Streaming manifests (`.m3u8` / `.mpd`) are the one thing we redirect but never cache: a manifest is a playlist of separately-signed segment URLs rather than a media file, so caching it would serve you a list of links that no longer work.
</Callout>

## 🚀 Bulk downloads: export your entire Media Vault

Downloading the full Media Vault one CDN URL at a time is slow and rate-limited. For bulk use cases, use our [Data Exports](/data-exports) feature instead, which packages your entire Media Vault (every photo, video, and audio file) into a single ZIP file you can download once it's ready.

<Callout type="info">
  Data Exports currently covers **OnlyFans accounts only**. For Fansly, page through [Get Account Media](/api-reference/fansly/media/get-account-media) and download each URL individually.
</Callout>

This is the recommended approach for:

* **AI / LoRA training datasets**: Get every asset in one archive instead of stitching together thousands of individual downloads. See [Download Media and Chats for LLM and LoRA Training](/data-exports/use-cases#download-media-and-chats-for-llm-and-lora-training) for a worked example.
* **Backups and platform migration**: Snapshot the full vault for disaster recovery or before switching platforms. See [Complete Data Backup and Migration](/data-exports/use-cases#complete-data-backup-and-migration).
* **Bulk content repurposing**: Pull your full media library at original quality for use elsewhere.

You can kick off a Media export from the [Data Exports dashboard](https://app.onlyfansapi.com/tools/data-export) (no code) or programmatically via the [Create Data Export API](/api-reference/data-exports/create-data-export). Big vaults may take several hours to package; you can close the browser and come back later to download the result.

## Migrating from the deprecated scrape endpoint

The previous [`POST /api/{account}/media/scrape`](/api-reference/media/deprecated-scrape-media-from-the-only-fans-cdn) endpoint is deprecated. If you have an existing integration built against it, here's what changes when you switch to the new download endpoint:

* **Response shape**: The deprecated endpoint re-hosted the file on the OnlyFans API CDN and returned a URL. The new endpoint streams the file binary directly in the response, so you'll need to save the bytes (not a URL) on your side.
* **File size limit**: The new endpoint has **no file size limit**. The deprecated endpoint capped uploads at 500MB.
* **Input format**: The new endpoint accepts a **CDN URL only**. Vault Media IDs (which the deprecated endpoint accepted) are not supported here.