Files API and PDFs
The OpenAI-compatible GetVibe API Files API and PDF input: base64, a link or a file_id on chat/completions, responses and messages, which models see the pictures inside a PDF, limits and errors.
OpenAI-compatible Files API for uploading, reading and deleting files. Every operation requires an API key; a file is accessible only to the same client account that uploaded it.
Upload a file
Send multipart/form-data with the required file and purpose fields. Allowed purposes: assistants, batch, fine-tune, vision and user_data. The maximum file size is 50 MiB (52 428 800 bytes); exceeding it returns 413.
curl -X POST https://getvibeapi.com/v1/files \
-H "Authorization: Bearer $GETVIBE_API_KEY" \
-F "purpose=user_data" \
-F "file=@./report.pdf"Get metadata, content or delete
Upload returns a file object with an opaque id, name, size, purpose and creation time. GET without /content returns this metadata; GET /content downloads the raw bytes. After a successful DELETE both GETs return 404. Another account's existing file returns 403, and an unknown identifier 404.
Safe workflow
- Upload the file and keep only the returned
file_id. - Verify the metadata with the same key; when working with Responses pass this id in
input_file.file_id. - Do not upload secrets and delete the file after you finish. A deleted file cannot be restored.
PDF in model requests
Every text model accepts a PDF on all three API formats. In short: send the file in any of the three ways below — by default we extract its text for free and pass that to the model. The model reads the text and tables, but not the pictures inside the PDF or scans. When pictures, layout or a scan matter, pick a model from the table below and turn text extraction off. Everything on this page was verified with live requests on November 2, 2026, using a three-page PDF with text, a table and a picture.
Three ways to send a PDF
| Method | How | When to use it |
|---|---|---|
| base64 in the request | Chat: {"type":"file","file":{"filename":"report.pdf","file_data":"data:application/pdf;base64,…"}}; Responses: {"type":"input_file","filename":"report.pdf","file_data":"data:…"}; Messages: {"type":"document","source":{"type":"base64","media_type":"application/pdf","data":"…"}} | A one-off document. Works on all three endpoints. Files up to 20 MiB. |
| Public link | Chat: "file":{"file_url":"https://…"}; Responses: {"type":"input_file","file_url":"https://…"}; Messages: "source":{"type":"url","url":"https://…"} | The file is already online. We download it ourselves: HTTPS only, no login or password, the server must answer with application/pdf, up to 20 MiB, within 10 seconds and at most 3 redirects. |
file_id after upload | Upload with POST /v1/files and purpose=user_data, then: Chat "file":{"file_id":"file-pv-…"}, Responses {"type":"input_file","file_id":"file-pv-…"}, Messages "source":{"type":"file","file_id":"file-pv-…"} | One document across several requests: no need to resend megabytes each time, and an identical request prefix is cached. The file is kept for 7 days and is visible only to your account. |
Ready-to-run examples
The key is read from the GETVIBE_API_KEY environment variable. The Python examples use the official openai and anthropic SDKs unchanged — only the address differs.
/v1/chat/completions
# PDF inline (base64); jq builds the body so a large file does not hit the command-line length limit
# macOS: replace base64 -w0 report.pdf with base64 -i report.pdf
jq -n --rawfile b64 <(base64 -w0 report.pdf) '{model: "deepseek-v4.1-flash", messages: [{role: "user", content: [
{type: "text", text: "Summarise the document"},
{type: "file", file: {filename: "report.pdf", file_data: ("data:application/pdf;base64," + $b64)}}]}]}' |
curl https://getvibeapi.com/v1/chat/completions \
-H "Authorization: Bearer $GETVIBE_API_KEY" -H "Content-Type: application/json" -d @-/v1/responses
# FILE_ID comes from the upload, see the "curl · file_id" tab above
curl https://getvibeapi.com/v1/responses \
-H "Authorization: Bearer $GETVIBE_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"gpt-6.1-sol","input":[{"role":"user","content":[
{"type":"input_text","text":"What were the sales in Kazan in August?"},
{"type":"input_file","file_id":"'"$FILE_ID"'"}]}]}'
# base64 instead of file_id:
# {"type":"input_file","filename":"report.pdf","file_data":"data:application/pdf;base64,…"}/v1/messages
curl https://getvibeapi.com/v1/messages \
-H "x-api-key: $GETVIBE_API_KEY" -H "anthropic-version: 2023-06-01" -H "Content-Type: application/json" \
-d '{"model":"claude-sonnet-5.5","max_tokens":1024,"messages":[{"role":"user","content":[
{"type":"document","source":{"type":"file","file_id":"'"$FILE_ID"'"}},
{"type":"text","text":"Summarise the document"}]}]}'
# other sources:
# "source":{"type":"base64","media_type":"application/pdf","data":"<base64>"}
# "source":{"type":"url","url":"https://example.com/report.pdf"}What the model sees: text or the file itself
By default, extracted text. We take the PDF's text layer and pass it to the model instead of the file. This is free and works with any model, even one without PDF support. The model sees words and numbers, including tables (without borders, but with the right values). What it does not see: pictures, charts, stamps, signatures, colours and layout. Up to 60,000 characters of text per request are passed on (about 28 dense pages); the rest is cut and the model is told the document was truncated. The x-pv-document-shim response header says what happened: applied:text_extracted means the model got the text, not-needed:lane_reads_pdf means it got the file itself.
The file itself. To have the model see whole pages, with pictures and layout, add "plugins":[{"id":"documents","enabled":false}] to the request body (the header then reads not-needed:disabled_by_client). Only do this with models marked "yes" in the table: the others answer 400 or say they see no file, and the file's input tokens are still billed.
# curl: add the field to the request body
"plugins": [{"id": "documents", "enabled": false}]
# Python (openai SDK)
r = client.chat.completions.create(
model="gpt-6.1-sol",
messages=[...],
extra_body={"plugins": [{"id": "documents", "enabled": False}]},
)| Model | By default | Sees the file itself (extraction off) |
|---|---|---|
gpt-6.1-sol | the file itself | yes — on /chat/completions and /responses |
gpt-6-luna | text | yes — on /chat/completions and /responses |
gpt-5.6-sol | text | not confirmed — keep the default |
claude-sonnet-5.5, claude-opus-5.5 | text | no — the model cannot see the file, so even with the plugin off we extract the text (header x-getvibe-warnings: plugin_overridden:documents); for the file itself use gpt-6.1-sol |
claude-haiku-5.5 | text | no — the model does not see the file |
gemini-4-argon, gemini-3.8-flash | text | no — the model does not see the file, or 400 |
qwen3.8-flash | text | yes — on /chat/completions; 400 on /responses |
deepseek-v4.1-flash, glm-5.3, kimi-k3 | text | no — 400 |
How we tested: the model was asked for a word from page 1, a number from a table cell, and to describe a picture (shapes, colours and a number drawn as an image). "Text" means it named the word and the number correctly and said it could not see the picture. "Yes" means it described the picture correctly too. Send PDFs to GPT, Gemini and Qwen models through /v1/chat/completions or /v1/responses: on /v1/messages they do not see the file itself.
not-needed:no_text_layer_unmeasured_lane). It then depends on the model: gpt-6.1-sol and qwen3.8-flash read our scan, DeepSeek models answer 400. If your model cannot read a scan, run OCR on your side and send the text, or send the pages as PNG/JPEG images via image_url to a model that accepts images.Limits, tokens and time
| What | Limit |
|---|---|
| A PDF in a model request (base64, link or file_id) | up to 20 MiB per file; the file must start with %PDF- |
Upload to POST /v1/files | up to 50 MiB (52,428,800 bytes), but only a PDF up to 20 MiB can go into a model request |
file_id lifetime | 7 days (the expires_at field), then the file is deleted |
| Pages | no separate limit; with text extraction, up to 60,000 characters per request; with the file itself, up to the model’s context window |
| Download by link | up to 10 seconds and up to 3 redirects |
How tokens are counted. A PDF is billed as ordinary input tokens of the chosen model at its price; there is no separate fee for the file or for text extraction. What it comes to (measured, including a short question):
- Extracted text: roughly 700 tokens per dense page of text (10 pages — 7,300 tokens; 40 pages cut to 60,000 characters — 20,300 tokens). A page with a short table — 100–200 tokens.
- The file itself on GPT models: about 600–700 tokens per page (10 pages — 5,900; 40 — 23,300); on
gemini-4-argonabout 1,200 per page; on Claude about 1,600 per page. A scanned page ongpt-6.1-sol— about 5,800 tokens. - Repeating the same document hits the cache when the start of the request does not change: a second request with the same
file_idread 7,168 of 7,309 tokens from the cache.
Time. Text extraction takes a fraction of a second; an answer about a 40-page document came back in 4–12 seconds. For large documents and long answers use stream: true and set the client timeout to at least 300 seconds.
Errors and how to fix them
| Response | Cause | What to do |
|---|---|---|
400 invalid_pdf | The PDF must start with the %PDF- signature. | This is not a PDF, or the base64 is broken. Make sure you encode the file itself, not its path, and that the type is application/pdf. |
400 invalid_pdf | The PDF size must not exceed 20 MiB. | Split the document or compress the images inside it. |
413 invalid_request_error | On POST /v1/files: the file is larger than 52,428,800 bytes | The file is over 50 MiB: split it. |
400 invalid_request_error | The file_id is unavailable | The file was deleted, expired (7 days), was uploaded by another account, or the id comes from a different service. Upload the file again with the same key. |
400 invalid_pdf | The PDF URL must be a public HTTPS URL without credentials. | The link is http://, has a login and password in it, uses a non-standard port or a domain that does not exist. Give a public HTTPS link or send base64. |
400 invalid_pdf | Could not safely fetch the PDF from the URL. | The link returns 404, a login page, HTML instead of a PDF, or the server did not answer within 10 seconds. Open the link in a private browser window; if the file does not download right away, send base64. |
400 unsupported_input | The model cannot read PDFs and the file has no text (a scan). | Use a model that reads scans (see above), or run OCR on your side. |
400 invalid_request_error | Invalid request… with text extraction turned off, or with a scan. | The model does not accept a PDF file. Remove plugins documents enabled:false so the model gets the text, or pick a model marked "yes" in the table. |
| 200, but "I see no file" | Extraction is off, and the model or channel does not read the file. | Go back to the default text extraction, or pick a model marked "yes" in the table. |
Best practices
- Need the document in several requests? Upload it once and pass the
file_id: the request is smaller, and the repeated prefix is cached and costs less. - Put the PDF and standing instructions at the start of the message and the changing question at the end: the cache then hits more often.
- Split documents longer than ~28 dense pages by chapter: with text extraction, anything past 60,000 characters never reaches the model.
- For contracts, reports and exports with a text layer, text extraction is enough — the cheapest option on any model, including
deepseek-v4.1-flash. - Need charts, stamps or scans? Turn extraction off and use a model marked "yes" in the table, for example
gpt-6.1-sol. - Non-Latin text (Russian, Chinese and others) in a PDF is read without any settings. Remove the password from a PDF before sending it.
- Delete uploaded files with
DELETE /v1/files/{file_id}once you no longer need them.
file_id with a third-party identifier.