Download your ChatGPT Image Library

 Download your ChatGPT Image Library

HOW-TO: Download all your generated images from ChatGPT Library

For one reason or another, and in my opinion because OpenAI wants to lock you into their ecosystem OR MAYBE because they are simply bad at prioritizing their work items OR MAYBE because they spent too much money on Money Ive ($6.5B) that they have no money left to hire talented and skillful Web developers to implement high-priority features despite believing that they are worth $500B, ChatGPT does not let you export ALL your data from your account, including the images you’ve uploaded to ChatGPT AND the images you’ve generated using their GPT-4o/gpt-image-1 models.

So I opened https://chatgpt.com/library in Chrome, opened the Developer Tools, and tried to find a way to download all images in my library by injecting and running some custom JavaScript. I was skeptical at first. I thought I, at the very list, need Playwright to make this possible.

BUT: I was VERY surprised when I found out that they don’t validate the limit parameter for the backend API that returns their recent images:

GET https://chatgpt.com/backend-api/my/recent/image_gen?limit={limit}
Who in the World Vibe Web uses backend-api for the base path of their backend APIs? Of course it is a backend API. Perhaps BfFE backend for front-end, but even so, it is just an api/

So, the lazy insomniac in me wrote a script to download all the images in my library, then passed it to one of my LLM buddies/agents to improve it a bit, and he over-engineered the script, but I used it to successfully in downloading all the ~1050 images in my library in less than 10 minutes. The code is a bit too conservative in terms or downloads per second and its retry logic, but it works and does the job quite well. You can tweak it to download your images faster.

Here I show you how to use it, so that you can download all your images from your ChatGPT Library, too. Hopefully sooner or later Sam Hitman’s men implement this feature right into the product for a better UX for all of us.

Step 1: Open ChatGPT in Chrome (or Firefox, or Vivaldi, or any other browser with decent-enough developer tools

  • Then go to your library page at https://chatgpt.com/library.
  • Switch to the Network Tab and select this request that is sent to https://chatgpt.com/backend-api/my/recent/image_gen:
    Network Tab
  • Click on the request and the click on Copy -> Copy as fetch:
    Copy as fetch

The copied content will look something like this:

fetch("https://chatgpt.com/backend-api/my/recent/image_gen?limit=20&after=WJ353YvVjRFUEIR0EbUYGU8O6UeRhdIy23eR3GeStF8mtFVJGa0V3hpJGQxPTFpVmmRhqh310dVFVUMIS-1SOOdy4UTiVYjWvPOI9Yyj1YYyXnvPjGUj2nFi8z0OcUKGMz3OmkayR11YW5VyM9Jab0m2emytjTrUNpm03PhpaInjjNECT3E4jm1bTVkeZGFRJTFVUMabST3Fp9kD8IWyM5ShTFYjNRU3mTFVzeX8DObQMR1GNcVRU20bGU8bbDR6X0PJitJjkdFmSkIdSZMpSr65YVYDa46VXN0SQmNVmQKOZ5IOeqNaZrI1tVzNjT6UpGqkVjJTJFWjdnlJEUablIWK5Zb3ln2BSIWVZ1jjRd2yVy5Z-aUcsaTSAmRIUDZ5DyKpd0vyKk0dVYzkOXW", {
  "headers": {
    "accept": "*/*",
    "accept-language": "en-US,en;q=0.9",
    "authorization": "Bearer ...",
    "oai-client-version": "prod-...",
    "oai-device-id": "abc-...",
    "oai-language": "en-US",
    "priority": "u=1, i",
    "sec-ch-ua": "...",
    "sec-ch-ua-arch": "Power 11",
    "sec-ch-ua-bitness": "\"256\"",
    "sec-ch-ua-full-version": "\"420.0.6969.420\"",
    "sec-ch-ua-full-version-list": "...",
    "sec-ch-ua-mobile": "¿69?",
    "sec-ch-ua-model": "\"\"",
    "sec-ch-ua-platform": "\"AIX\"",
    "sec-ch-ua-platform-version": "\"69.69.0\"",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-origin"
  },
  "referrer": "https://chatgpt.com/library",
  "body": null,
  "method": "GET",
  "mode": "cors",
  "credentials": "include"
});

Step 2

  1. Change the URL of the copied fetch function call from its original value (e.g. https://chatgpt.com/backend-api/my/recent/image_gen?limit=20&after=WJ353YvVjRFUEIR0EbUYGU8O6UeRhdIy23e...) to https://chatgpt.com/backend-api/my/recent/image_gen?limit=9000 (I had 1025 images so I set limit to 2000. If you have over 9000 images, increase limit according to your needs).
    NOTE: in a well-engineered back-end, something companies like OpenAI afford to implement, limit is validated not to accept large values, but luckily ChatGPT’s “backend API” doesn’t do so.
  2. Paste the modified fetch code into the console, and assign its results to a local variable named library:
    let library = await fetch("https://chatgpt.com/backend-api/my/recent/image_gen?limit=9001", {
      "headers": {
        "accept": "*/*",
        "accept-language": "en-US,en;q=0.9",
        "authorization": "Bearer ...",
        "oai-client-version": "prod-...",
        "oai-device-id": "abc-...",
        "oai-language": "en-US",
        "priority": "u=1, i",
        "sec-ch-ua": "...",
        "sec-ch-ua-arch": "Power 11",
        "sec-ch-ua-bitness": "\"256\"",
        "sec-ch-ua-full-version": "\"420.0.6969.420\"",
        "sec-ch-ua-full-version-list": "...",
        "sec-ch-ua-mobile": "¿69?",
        "sec-ch-ua-model": "\"\"",
        "sec-ch-ua-platform": "\"AIX\"",
        "sec-ch-ua-platform-version": "\"69.69.0\"",
        "sec-fetch-dest": "empty",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "same-origin"
      },
      "referrer": "https://chatgpt.com/library",
      "body": null,
      "method": "GET",
      "mode": "cors",
      "credentials": "include"
    });
    
  3. Finally, paste the following code and lets get the downloads started. Beware that Chrome may ask you for permission for simultaneous downloading of multiple files and you should give it the permission to do so.. You can revoke the permission afterwards if you want to be very precautious.
async function downloadFile(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status} ${response.statusText}`);
  }

  const blob = await response.blob();
  const blobUrl = URL.createObjectURL(blob);

  const filename = extractFilenameFromUrl(url);

  const anchorElement = document.createElement('a');
  anchorElement.href = blobUrl;
  anchorElement.download = filename;

  document.body.appendChild(anchorElement);
  anchorElement.click();
  anchorElement.remove();

  // Clean up blob URL after a short delay to ensure download starts
  setTimeout(() => URL.revokeObjectURL(blobUrl), 1300 + Math.random() * 2100);
}

function extractFilenameFromUrl(url) {
  const pathSegments = url.split('/');
  const lastSegment = pathSegments[pathSegments.length - 1];
  const filenameWithoutQuery = lastSegment.split('?')[0];

  return filenameWithoutQuery || 'download';
}

async function downloadWithRetry(downloadFunction, maxAttempts = 5) {
  const fibonacciCache = new Map();

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      await downloadFunction();
      return;
    } catch (error) {
      console.error(`Download attempt ${attempt} failed:`, error);

      if (attempt === maxAttempts) {
        throw new Error(`Download failed after ${maxAttempts} attempts: ${error.message}`);
      }

      const backoffDelay = calculateBackoffDelay(attempt, fibonacciCache);
      await sleep(backoffDelay);
    }
  }
}

function calculateBackoffDelay(attemptNumber, cache) {
  const baseDelay = 1000;
  const fibonacciMultiplier = getFibonacci(attemptNumber, cache);
  const jitter = Math.random() * 5000;

  return baseDelay * fibonacciMultiplier + jitter;
}

function getFibonacci(n, cache = new Map()) {
  if (cache.has(n)) {
    return cache.get(n);
  }
  const result = getFibonacci(n - 1, cache) + getFibonacci(n - 2, cache);
  cache.set(n, result);
  return result;
}

function sleep(milliseconds) {
  return new Promise(resolve => setTimeout(resolve, milliseconds));
}

async function downloadAllItems(items) {
  const results = {
    successful: [],
    failed: []
  };

  for (const item of items) {
    try {
      await downloadWithRetry(
        () => downloadFile(item.url),
        5 // max attempts
      );

      console.log(`✓ Downloaded: ${item.url}`);
      results.successful.push(item.url);

      // Rate limiting: wait 1-2 seconds between downloads
      const delayBetweenDownloads = 1000 + Math.random() * 1000;
      await sleep(delayBetweenDownloads);

    } catch (error) {
      console.error(`✗ Failed to download ${item.url}:`, error.message);
      results.failed.push({ url: item.url, error: error.message });
    }
  }

  // Log summary
  console.log(`
Download Summary:
- Successful: ${results.successful.length}
- Failed: ${results.failed.length}
  `);

  return results;
}

let jsonData = await library.json();
await downloadAllItems(jsonData.items);

Step 3

If you liked this article and you managed to download your images, any help is highly appreciated. Recently my father needed a leg surgery, my mother needed a cataract surgery, and anything I had saved depleted again. Maybe you can buy my icon pack for WezTerm.

Looking forward to The Day

Comments