Most LLM integration bugs show up before the model ever sees your prompt. When you send curl headers openai compatible api requests, you are negotiating authentication, content type, and sometimes routing with a server that mimics OpenAI’s contract. Miss a required header and you get a 401, 415, or a silently dropped cache hint.
The minimal header set that always works
Every OpenAI-compatible endpoint expects two headers to do anything useful: Authorization and Content-Type. Everything else is either optional or provider-specific. When constructing curl headers openai compatible api calls, start with these two and add only what your gateway documents.
curl https://api.example.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "Hello"}]
}'
If you omit Content-Type: application/json, many gateways return 415 Unsupported Media Type. If you pass the key as a query param (?api_key=), you will likely get 401 because that pattern has been deprecated since OpenAI’s v1 migration.
Authorization: treat the key like a password
The Authorization header is non-negotiable. Use a bearer token, and never hardcode it in scripts that live in repos.
# Wrong
curl "https://api.example.com/v1/chat/completions?api_key=sk-123"
# Right
curl https://api.example.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY"
A subtle pitfall: the word Bearer is case-sensitive in practice and must be followed by a single space. Sending Authorization:bearer $KEY (lowercase, no space) fails on strict gateways. Tradeoff: environment variables are fine for local testing but use a secret manager in CI. If you rotate keys, remember that some gateways meter per token and key rotation does not retroactively change billing attribution.
Content-Type and the JSON body contract
OpenAI-compatible APIs speak JSON. The -d flag in curl sends application/x-www-form-urlencoded by default unless you set the header. Always set -H "Content-Type: application/json" and pass a JSON string.
curl https://api.example.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"model":"claude-3-5-sonnet","messages":[{"role":"user","content":"Ping"}]}'
A common pitfall: using -d @file.json without the header still results in form-encoded content-type, because curl does not infer from file extension. The server may try to parse form fields and fail with invalid request. If you want to be explicit, add ; charset=utf-8—it is harmless and clarifies encoding.
Why Accept matters less than you think
You can send Accept: application/json. It’s polite but the server usually ignores it and returns JSON anyway. For streaming, the response Content-Type becomes text/event-stream; you don’t need to request it explicitly, but setting Accept: text/event-stream makes your intent clear to debugging proxies. The fastest way to verify your curl headers openai compatible api setup is with -v to confirm what left the wire.
Routing and cache-control headers
Standard OpenAI does not define routing headers. But if you sit behind a gateway that aggregates providers, you may need to express preferences. Some gateways, including n4n.ai, honor client routing directives and forward provider cache-control hints. That means a header like Cache-Control: max-age=3600 can be passed through to the upstream provider to enable prompt caching, and a custom routing header can pin a specific backend.
curl https://gateway.example.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Cache-Control: max-age=600" \
-H "X-Route-Preference: anthropic" \
-d '{"model":"auto","messages":[{"role":"user","content":"Cached prompt?"}]}'
Check your gateway docs before relying on custom headers. If the gateway does not understand X-Route-Preference, it will ignore it, and you may silently hit a different provider than intended—a real tradeoff when latency or compliance matters. Gateways with automatic fallback when a provider is rate-limited or degraded will treat an unknown routing header as a soft preference, not a hard pin.
Streaming: headers and transport
Set "stream": true in the body. The server responds with chunked transfer. You do not need Transfer-Encoding: chunked in your request (that’s a response header), but you should tell curl to not buffer:
curl -N https://api.example.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"Stream"}],"stream":true}'
The -N flag disables curl’s buffering so tokens appear as they arrive. Without it, you might wait until the connection closes. A pitfall: if you pipe to jq, jq buffers too; use jq -c or a line-oriented parser. Also, some intermediaries require Accept: text/event-stream to avoid buffering the response—add it if you see delays.
Idempotency and retries
Network flakes happen. OpenAI-compatible APIs that implement idempotency accept an Idempotency-Key header. If you retry with the same key, the gateway returns the original response instead of double-charging tokens.
curl https://api.example.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: req-2024-06-01-001" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Retry safe"}]}'
Not all providers support this. If unsupported, the header is ignored and you pay for each attempt. Build retry logic with exponential backoff regardless, and generate the key client-side with a UUID.
Debugging: see the headers you send
Use -v or --trace to inspect the actual header bytes. Engineers often think they set a header but curl merged or dropped it due to ordering or shell quoting.
curl -v https://api.example.com/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[]}' 2>&1 | grep -A1 '>'
Look for > Authorization: and > Content-Type:. If you see > POST /... but no headers, you likely misplaced quotes. Also check the < lines for HTTP/2 401 or 429 to confirm server reaction.
User-Agent and gzip
Some gateways rate-limit by User-Agent or block empty ones. Set a descriptive UA:
-H "User-Agent: my-llm-cli/1.0"
If you send Accept-Encoding: gzip and don’t handle decompression, curl auto-decodes by default, so it’s safe. But if you use --raw, you’ll get binary gzip bytes. Avoid --raw unless debugging at the wire level.
Complete copy-paste template
Here is a skeleton you can adapt. It covers auth, content type, optional cache hint, and streaming off.
#!/bin/bash
set -euo pipefail
API_BASE="https://api.example.com/v1"
MODEL="gpt-4o-mini"
KEY="${OPENAI_API_KEY:?Set OPENAI_API_KEY}"
curl -sS "$API_BASE/chat/completions" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "User-Agent: curl-cookbook/1.0" \
-d '{
"model": "'"$MODEL"'",
"messages": [
{"role": "system", "content": "You are terse."},
{"role": "user", "content": "Explain headers."}
],
"temperature": 0.2
}'
Swap api.example.com for your endpoint. If you use a gateway that addresses 240+ models behind one OpenAI-compatible endpoint, the model field becomes a routing string rather than a single vendor SKU—read its docs for exact names.
Tradeoffs: more headers, more surface area
Every header you add is another thing to misconfigure. The minimal set is Authorization and Content-Type. Add Cache-Control only if your gateway forwards it. Add routing headers only if you have verified they are honored. Add Idempotency-Key only if retries are real.
Over-sending headers like X-Requested-With or random tracing fields wastes bytes and can trigger WAF rules. Keep it lean, log what you send in dev, and strip verbose headers in prod. Remember that curl headers openai compatible api calls should be boring: the fewer moving parts, the easier to debug at 3am.
Common pitfalls checklist
- Forgot
Bearerprefix: sendsAuthorization: sk-...which fails. - Used single quotes inside
-dJSON with unescaped variables: breaks shell. - Set
Content-Type: application/jsonbut passed form data via-F: contradiction. - Assumed
?api_key=works: deprecated. - Streamed without
-N: appeared hung. - Ignored HTTP status: use
curl -w "%{http_code}\n"to catch 429s.
curl -sS -o /dev/null -w "%{http_code}\n" https://api.example.com/v1/models \
-H "Authorization: Bearer $KEY"
If you get 401, check key scope. If 429, back off. If 400, validate JSON with jq empty before sending.
Final note on reliability
The phrase curl headers openai compatible api covers a small surface: auth, content negotiation, and optional gateway directives. Master those and your calls will be boringly reliable. The rest is body schema and model behavior—issues that show up in the JSON, not in the headers.