Sending a curl multipart file upload llm api request is the most direct way to push a document or image to a model server without standing up a client library. This cookbook gives you copy-pasteable commands for the OpenAI-compatible Files endpoint, shows how to verify the upload, and covers the failure modes that waste an afternoon.
Step 1: Confirm the endpoint expects multipart
Most LLM APIs speak JSON over POST. File ingestion is the exception. The OpenAI Files API at POST /v1/files accepts multipart/form-data with two fields: purpose and file. Some open-weight servers and gateways replicate this exact shape. If you are hitting a chat completion endpoint, multipart is usually wrong—you either base64 the bytes inside JSON or host the file and pass a URL.
Check the API reference. If it says multipart/form-data, proceed. If it says application/json, skip to Step 8.
A multipart body is a sequence of parts separated by a boundary string. Each part has its own Content-Disposition header with a name and optional filename. curl builds this for you when you use -F; you should never hand-write the boundary unless you are debugging.
Step 2: Prepare the file and purpose
Ensure the file exists and is readable by the shell. For OpenAI-compatible servers, purpose is a constrained string: fine-tune, assistants, vision, or batch. The server rejects unknown values with a 400.
ls -l ./training.jsonl
echo "file size: $(wc -c < ./training.jsonl) bytes"
file ./training.jsonl
If the file is binary (PDF, PNG), confirm the extension matches content. Some providers sniff the MIME type from the part; others trust the filename.
Step 3: Execute the curl multipart file upload llm api call
Use -F (alias --form). curl sets Content-Type: multipart/form-data; boundary=... automatically and streams the file bytes. Prefix the path with @ to send the file contents rather than the literal string.
curl https://api.openai.com/v1/files \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F purpose="assistants" \
-F file="@./training.jsonl"
If your gateway uses an OpenAI-compatible route, swap the base URL. For example, n4n.ai exposes one endpoint that addresses 240+ models; the same /v1/files path forwards to providers that support it, but check per-provider capability before assuming uniform behavior.
You can force a MIME type on the part if the server is strict:
curl https://api.openai.com/v1/files \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F purpose="assistants" \
-F "file=@./doc.pdf;type=application/pdf"
Use --form-string instead of -F when a field value starts with @ or < and you do not want curl to read a file.
Step 4: Verify the upload succeeded
A 200 with JSON containing an id means the server accepted the file. Pipe through jq to extract fields and assert status:
RESP=$(curl -s https://api.openai.com/v1/files \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F purpose="assistants" \
-F file="@./training.jsonl")
echo "$RESP" | jq '{id, filename, bytes, purpose, status}'
To confirm the server persisted it, fetch the specific file record:
FILE_ID=$(echo "$RESP" | jq -r '.id')
curl https://api.openai.com/v1/files/$FILE_ID \
-H "Authorization: Bearer $OPENAI_API_KEY" | jq '.status'
Expect status: "uploaded" immediately, and processed after a short delay for assistants documents. Any 4xx returns an error object; print the whole response with jq '.' to read the message.
For scripted checks, use -w '%{http_code}' and --fail-with-body:
curl -f --fail-with-body -w '\nHTTP %{http_code}\n' https://api.openai.com/v1/files \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F purpose="assistants" -F file="@./training.jsonl"
Step 5: Attach the file to a model run
For the Assistants API, the upload and the reference are separate concerns. You cite the file ID in a JSON message:
curl https://api.openai.com/v1/threads/thread_123/messages \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"role": "user",
"content": "Summarize the attached doc",
"file_ids": ["file-abc123"]
}'
Do not try to re-upload the bytes here. The multipart step already placed the file in the provider’s storage; the chat endpoint only takes identifiers.
Step 6: Upload multiple files in one request
Some endpoints accept repeated file fields. Use multiple -F flags:
curl https://api.openai.com/v1/files \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F purpose="assistants" \
-F file="@./a.txt" \
-F file="@./b.txt"
If the server expects an array, the field name may be file[]. Test with -v; a 400 complaining about file being a string tells you to switch to file[]. Note that purpose is a single value per request—you cannot mix fine-tune and assistants in one multipart body.
Step 7: Debug common failures
Use -v to inspect the generated boundary and response headers. Typical pitfalls:
- 413 Request Entity Too Large: file exceeds the provider cap (OpenAI Files allows up to 512 MB, but many gateways impose lower limits). Split or compress.
- 415 Unsupported Media Type: you manually set
-H "Content-Type: application/json". Remove that header;-Fsets the correct multipart type. - 401 Unauthorized: key missing, malformed, or lacking the
files.writescope. - 400 invalid purpose: typo in the
purposefield.
Capture the raw exchange:
curl -v --retry 2 https://api.openai.com/v1/files \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F purpose="assistants" \
-F file="@./big.pdf" 2>&1 | grep -i "boundary\|HTTP/\|error"
If the upload truncates on flaky networks, add --retry 3 --retry-delay 2. curl resumes only if the server supports Range, which most file APIs do not, so a full retry is the safe path.
Step 8: When the endpoint rejects multipart
Most chat completion routes do not accept multipart/form-data. To send an image, base64 it inside the JSON content array:
import base64, requests
with open("image.png","rb") as f:
b64 = base64.b64encode(f.read()).decode()
resp = requests.post("https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {OPENAI_API_KEY}"},
json={"model":"gpt-4-vision-preview",
"messages":[{"role":"user","content":[
{"type":"image_url","image_url":{"url":f"data:image/png;base64,{b64}"}}
]}]})
print(resp.status_code, resp.json())
From pure curl, build the payload with jq to avoid shell escaping pain:
B64=$(base64 -i image.png | tr -d '\n')
jq -n --arg b64 "$B64" '{
model:"gpt-4-vision-preview",
messages:[{role:"user",content:[
{type:"image_url",image_url:{url:"data:image/png;base64,\($b64)"}}]}]
}' > payload.json
curl https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d @payload.json
Base64 inflates size by ~33% and costs CPU. If the provider accepts a public URL, host the file and pass image_url with an https:// string instead.
Step 9: Gateway and routing notes
If you front your calls with a gateway, the curl multipart file upload llm api pattern stays identical at the client, but the gateway must forward the raw stream without buffering limits. A gateway like n4n.ai honors client routing directives and forwards provider cache-control hints, but file storage is provider-specific—automatic fallback when a provider is degraded applies to inference, not to /v1/files writes. Per-token metering only accrues on completion calls, not on upload bandwidth.
Step 10: Clean up test artifacts
Providers bill for stored files. Delete them after the test:
curl -X DELETE https://api.openai.com/v1/files/file-abc123 \
-H "Authorization: Bearer $OPENAI_API_KEY"
Wrap the full loop in a shell function for repeatable tests:
upload_and_check() {
local fp=$1 purpose=$2
local id
id=$(curl -s https://api.openai.com/v1/files \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F purpose="$purpose" -F file="@$fp" | jq -r '.id')
echo "uploaded $fp as $id"
curl -s https://api.openai.com/v1/files/$id \
-H "Authorization: Bearer $OPENAI_API_KEY" | jq '.status'
}
# usage: upload_and_check ./a.txt assistants
Quick reference card
| Goal | Flag |
|---|---|
| Send file part | -F "file=@path" |
| Send scalar field | -F "purpose=assistants" |
| Set part MIME | -F "file=@path;type=application/pdf" |
Literal @ value |
--form-string "name=@notafile" |
| Inspect wire format | -v or --trace-ascii - |
| Fail on HTTP error | --fail-with-body |
The multipart path is boring but reliable. Use it for the upload step, then switch back to JSON for inference.