n4nAI

Uploading files to an LLM API with curl multipart forms

Learn how to perform a curl multipart file upload llm api request from CLI with step-by-step examples for vision, documents, and errors.

n4n Team4 min read848 words

Audio narration

Coming soon — every post will get a voice note here.

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; -F sets the correct multipart type.
  • 401 Unauthorized: key missing, malformed, or lacking the files.write scope.
  • 400 invalid purpose: typo in the purpose field.

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.

Tagscurlfile-uploadmultipartcli

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All curl llm api cookbook posts →