How Do LLM APIs Accept Files From Your Users?

Posted on

Your app’s “ask about this document” feature works with the test PDF on your laptop. Then the first real user uploads a Word lease and a photo straight off an iPhone, and neither one is in a format every model reads.

LLM APIs accept a file in three ways. You can send it inline as base64 inside the request, pass a URL the provider fetches, or upload it to the provider’s files endpoint and send the file ID it returns. Each provider sets its own size, format and retention limits for each route. Before any of them, your app still has to receive the user’s file, check it, convert it to a format the model reads, and store it somewhere you control. Those four steps are a small file processing pipeline.

Filestack is our API for uploading, processing and delivering files, and its features cover those steps. The picker receives files from a device or a cloud account, Virus Detection and content moderation check them, and processing tasks convert a HEIC photo to JPEG or a DOCX to PDF. Every result is stored at an HTTPS URL on our CDN, the kind of URL a model API fetches. For this post we wrote two short Node scripts, one that turns an uploaded file into a model-ready URL and one that asks an OpenAI model about it.

Key takeaways

  • Gemini, Claude and OpenAI all take a file inline, by URL, or by an ID from their own files endpoint.
  • A URL keeps the request small and the file in your storage, which suits files users upload to your app.
  • Claude and OpenAI don’t read HEIC photos, Gemini doesn’t fetch them by URL, and Claude’s document block reads only PDF and plain text, so photos and Word files are converted first.
  • Gemini deletes files from its files endpoint after 48 hours. Claude and OpenAI keep them until you delete them or an expiry you set.
  • Scanning, converting and storing happen before the model call, and the stored URL is what the model receives.

How do Gemini, Claude and OpenAI accept files?

Gemini, Claude and OpenAI each accept a file inline, by URL or by file ID, and each route has its own size limit. These are the limits in each provider’s documentation on November 2, 2026:

Provider Inline (base64) By URL By file ID
Gemini Up to 100 MB per request, 50 MB for a PDF Public HTTPS or pre-signed URLs, up to 100 MB per request Gemini Files API, 2 GB per file and 20 GB per project
Claude Inside the 128 MB request limit, 10 MB per image once base64-encoded "type": "url" sources for images and PDFs Claude Files API, 500 MB per file and 1 TB per organization
OpenAI file_data for documents, a base64 data URL for images file_url for documents, image_url for images file_id from the Files API, 512 MB per file

OpenAI file inputs have one more limit. Each document must be under 50 MB, and the documents in one request together must also stay under 50 MB.

Gemini’s URL route needs a URL that opens without a login, and it runs a safety check on what it fetches. A URL that fails the check returns URL_RETRIEVAL_STATUS_UNSAFE.

Inline, URL or file ID, which one fits your app?

A URL fits most apps that take files from their own users. The file stays in storage you control, the request stays small, and the same URL works with all three providers.

Route Fits when The catch
Inline Small files, one request per file, nothing to clean up The file travels in every request, and base64 adds about a third to its size
By URL Files already stored where the provider can fetch them The URL has to be reachable from the provider’s servers, or pre-signed
By file ID One large file asked about many times, such as a long contract in a chat A second copy lives at the provider, with its own retention to manage

The same trade-off appears with any API that takes files, where multipart, raw binary and URL ingestion are the three upload transports.

Claude’s documentation adds a rule for the file ID route. Files are scoped to the workspace, so any API key in that workspace can read any file in it. Keep the mapping from your users to their file IDs on your server, and never accept a file ID from the browser.

Which formats does each model accept, and what do you convert first?

PDFs and common image formats work with all three providers. Office documents and phone photo formats are where they differ:

  • Claude reads PDF and plain text in a document block, and JPEG, PNG, GIF and WebP in an image block, up to 8000 by 8000 pixels. Claude’s documentation says to convert a .docx file to PDF, or to plain text.
  • OpenAI reads PDFs as text and page images on vision models. It also reads Word, PowerPoint and spreadsheet files, as extracted text only, so charts in a .docx are lost unless you convert it to PDF. Images can be PNG, JPEG, WebP or non-animated GIF.
  • Gemini reads PNG, JPEG, WebP, HEIC and HEIF images inline or through its Files API. By URL, it fetches text types, PDF, and BMP, JPEG, PNG and WebP images.

HEIC is the default photo format on iPhones. Only Gemini reads it, and not from a URL, so converting it to JPEG gives one file every provider takes. Size can be a limit too. In our example, a 6240 by 4160 photo is 7.4 MB as HEIC and 10.9 MB as a full-size JPEG, which is over Claude’s 10 MB per-image limit before base64 adds to it. Resized to 2000 pixels wide, the JPEG is 0.97 MB. Claude downscales an image larger than its working resolution, and OpenAI rejects an image that is still too large after its own resizing.

Scanned PDFs are a separate question, because the text is in an image of the page. The trade-offs of LLM OCR versus an OCR API for scans are in a separate post.

How long does each provider keep an uploaded file?

Gemini keeps a file from its files endpoint for 48 hours. Claude and OpenAI keep it until you delete it, unless you set an expiry when you upload:

Provider Default Expiry you can set
Gemini Deleted after 48 hours None
Claude Kept until you delete it expires_in_seconds, from 1 hour to 90 days
OpenAI Kept until you delete it, except batch files, which expire after 30 days expires_after, from 1 hour to 30 days

Inline files and URLs don’t create a file at the provider, and Gemini fetches a URL again for each request. Where your user’s file lives, and when it’s deleted, is then up to your storage.

Filestack Discord

What happens to a user’s file before the model call?

Before the model call, your app receives the file, checks it, converts it and stores it. The model sees only what comes out of the fourth step.

Five steps from a user’s file to a model call. 1 Receive with the picker or upload API. 2 Check for malware and unsafe images with Virus Detection and content moderation. 3 Convert a HEIC photo to JPEG, a DOCX to PDF and a 26 MP photo to 2000 px. 4 Store a URL you control. 5 Into the model API by URL, inline or by file ID
Five steps from a user’s file to a model call. 1 Receive with the picker or upload API. 2 Check for malware and unsafe images with Virus Detection and content moderation. 3 Convert a HEIC photo to JPEG, a DOCX to PDF and a 26 MP photo to 2000 px. 4 Store a URL you control. 5 Into the model API by URL, inline or by file ID
  1. Receive. Users pick files from their device, a phone camera, or cloud upload sources such as Google Drive and Dropbox. Size and type limits apply here.
  2. Check. Scan documents for malware and screen images with a content moderation API before anything reads them, the model included.
  3. Convert. Turn the file into a format and size the model accepts.
  4. Store. Keep the converted file at a URL you control, with its ID in your database, and delete it on your schedule.

The model call usually runs on your server once the file is ready. File upload webhooks tell your server when an upload or a scan finishes, so the call can start there.

What are the options before the model call?

There are three common ways to handle the receive, check, convert and store steps:

Option What you run The catch
Your own upload endpoint Multipart parsing, ClamAV, libheif for HEIC and LibreOffice for DOCX Each converter and the scanner are services to install, update and scale
Cloud storage with presigned uploads and functions, such as S3 and Lambda Uploads to a bucket, and a function per conversion The conversion code and the scan are still yours to write
A managed file API such as Filestack The picker, Virus Detection, moderation, conversion tasks and stored URLs The model call and what your app does with the answer stay in your code

We built the conversion tasks so they run inside a URL, which is why the third row needs no converter on your server.

How does Filestack prepare a file for an LLM API?

Preparing a file takes two steps. The picker uploads it and returns a handle and a MIME type, and a processing URL ending in /store/ converts the file and saves the result as a new file with its own URL.

This page opens the picker for the file types the feature takes:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <script src="https://static.filestackapi.com/filestack-js/4.x.x/filestack.min.js"></script>
</head>
<body>
  <script>
    const client = filestack.init('YOUR_API_KEY');

    client.picker({
      fromSources: ['local_file_system', 'googledrive', 'dropbox'],
      accept: ['image/*', '.heic', 'application/pdf', '.docx'],
      maxFiles: 5,
      maxSize: 50 * 1024 * 1024,
      onUploadDone: (result) => {
        // Send these to your server. Here they're logged.
        for (const file of result.filesUploaded) console.log(file.handle, file.mimetype);
      },
    }).open();
  </script>
</body>
</html>

For a Word lease and a HEIC photo, the console shows:

LzmKrxbnSZWd0YQfLPk7 application/vnd.openxmlformats-officedocument.wordprocessingml.document
0nhJwZYLRsiAnY05uf1H image/heic

prepare.mjs runs on your server. It picks the processing tasks for each MIME type and stores the result. It’s plain Node 18 or later, with no packages:

// Turns an uploaded file into a stored copy that a model API can read by URL
const RECIPES = {
  'image/heic': 'resize=width:2000,fit:max/output=format:jpg',
  'image/jpeg': 'resize=width:2000,fit:max',
  'image/png': 'resize=width:2000,fit:max',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'output=format:pdf',
  'application/pdf': null, // models read PDFs as they are
};

const [handle, mimetype] = process.argv.slice(2);
if (!(mimetype in RECIPES)) throw new Error(`no recipe for ${mimetype}`);

const tasks = RECIPES[mimetype];
if (!tasks) {
  console.log({ url: `https://cdn.filestackcontent.com/${handle}`, type: mimetype });
} else {
  // /store saves the converted file under a new handle and returns its URL
  const res = await fetch(`https://cdn.filestackcontent.com/${tasks}/store/${handle}`);
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  const { url, type, size } = await res.json();
  console.log({ url, type, size });
}

fit:max resizes only images wider than 2000 pixels, so an 800-pixel PNG keeps its size. Run it with each handle and type:

node prepare.mjs 0nhJwZYLRsiAnY05uf1H image/heic
node prepare.mjs LzmKrxbnSZWd0YQfLPk7 application/vnd.openxmlformats-officedocument.wordprocessingml.document

It prints:

{
  url: 'https://cdn.filestackcontent.com/QGby6s1QZCIwgbGpIHwa',
  type: 'image/jpeg',
  size: 969636
}
{
  url: 'https://cdn.filestackcontent.com/XTzhxTnvQqaGEBmch1lF',
  type: 'application/pdf',
  size: 21796
}

The photo comes out 2000 by 1333 pixels. A type with no recipe throws no recipe for text/csv, and a handle that doesn’t exist throws 404 file not found. With application security turned on, the processing URL also carries a policy signed on your server, and so does the URL you give the model. Sign a read policy for that one handle with a short expiry, and put security=p:POLICY,s:SIGNATURE in front of the handle so the provider can fetch it.

With security off, the stored URLs go into the model request as they are. ask.mjs sends both to the OpenAI Responses API, the lease as an input_file and the photo as an input_image:

// Asks an OpenAI model about the stored lease and photo, by URL
const [pdfUrl, imageUrl, question] = process.argv.slice(2);

const res = await fetch('https://api.openai.com/v1/responses', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
  },
  body: JSON.stringify({
    model: 'gpt-6-astra',
    input: [{
      role: 'user',
      content: [
        { type: 'input_file', file_url: pdfUrl },
        { type: 'input_image', image_url: imageUrl },
        { type: 'input_text', text: question },
      ],
    }],
  }),
});
const json = await res.json();
if (!res.ok) throw new Error(`${res.status} ${json.error?.message}`);

const message = json.output.find((item) => item.type === 'message');
console.log(message.content.map((part) => part.text).join(''));

Run it with the two URLs prepare.mjs printed and your OpenAI key in the environment:

OPENAI_API_KEY=YOUR_OPENAI_KEY node ask.mjs \
  https://cdn.filestackcontent.com/XTzhxTnvQqaGEBmch1lF \
  https://cdn.filestackcontent.com/QGby6s1QZCIwgbGpIHwa \
  "When is the renewal notice due, and what does the photo show? One sentence each."

The model reads the converted lease and the resized photo from their URLs:

The renewal notice is due November 30, 2026.

The photo shows a traditional Japanese-style building with ornate tiled roofs, white walls, and a stone foundation.

A wrong key throws 401 Incorrect API key provided. Claude takes the same URLs in a url source on a document or image block, and Gemini takes them as a uri with a mime_type, which is the type that prepare.mjs prints. To delete a file when your app is done with it, call the File API with a signed remove policy for that handle.

Questions people ask

Can I upload a file to the Gemini API?

You can upload a file to the Gemini API through its Files API, up to 2 GB per file, and Gemini deletes it after 48 hours. Gemini also fetches a public or pre-signed HTTPS URL of up to 100 MB, and takes files inline up to 100 MB per request.

Can I send a Word document to Claude?

Claude’s document block reads PDF and plain text, not .docx. Convert the Word file to PDF first, which keeps its images and tables, or extract its text and send that.

Does the OpenAI API keep files I upload?

The OpenAI API keeps uploaded files until you delete them, except batch files, which expire after 30 days. Set expires_after when you upload to have a file deleted after 1 hour to 30 days.

Can Ollama take a file through its API?

Ollama’s chat API takes images as base64 strings in a message’s images array. It has no files endpoint, so a document goes in as extracted text inside the prompt.

Try it

Create a free account for an API key, upload a photo and a Word file through the picker, and pass each handle to prepare.mjs. To build the same steps from inside your editor, our Claude Code plugin runs uploads and transformations as tool calls.

Sign up free for Filestack

Read More →