Get going in 30 seconds
export NI_API_KEY=ni_live_YOUR_KEY_HERE
curl https://northerninference.ca/v1/chat/completions \
-H "Authorization: Bearer $NI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"claude-haiku-4-5-20251001-t4","messages":[{"role":"user","content":"hi"}],"max_tokens":20}'
The model field is the route contract. Copy exact route IDs from northerninference.ca/portal (Models page) or from curl https://northerninference.ca/api/billing/models | jq '.models[].model_id'.
Basic chat completion
curl https://northerninference.ca/v1/chat/completions \
-H "Authorization: Bearer $NI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-haiku-4-5-20251001-t4",
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 20
}'
Pick a route
curl https://northerninference.ca/v1/chat/completions \
-H "Authorization: Bearer $NI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-6-t4",
"messages": [{"role": "user", "content": "Which jurisdiction am I in?"}],
"max_tokens": 80
}'
The model ID is the route contract. Copy the exact routing key from /models or /api/billing/models. API key allowed route tiers authorize which routes the key can use.
Read the custody chain from response headers
curl -sS -D /tmp/headers https://northerninference.ca/v1/chat/completions \
-H "Authorization: Bearer $NI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"claude-haiku-4-5-20251001-t4","messages":[{"role":"user","content":"hi"}],"max_tokens":5}' \
> /dev/null
grep -i '^x-ni-' /tmp/headers
You'll see something like:
x-ni-request-id: 8f2e3b7d9a1c4f08
x-ni-resolved-tier: managed_canadian_cloud
x-ni-resolved-provider: Bedrock
x-ni-resolved-region: ca-central-1
x-ni-resolved-jurisdiction: CA
x-ni-custody-path: NI-CA -> Bedrock-CA
x-ni-credential-source: platform
x-ni-resolved-litellm-model: claude-haiku-4-5-20251001-t4
Headers that appear only when we changed something
The headers above come back on every request. These two are different: they appear only when Northern Inference adjusted your request so it would run instead of failing. If you never see them, nothing was adjusted.
They matter because the response still arrives as a normal HTTP 200. Without reading them there is no way to tell an answer produced exactly as you asked for it from one produced under an adjusted setting.
| Header | Meaning |
|---|---|
x-ni-max-output-tokens-clamped | Your max_tokens was above this route's hard provider cap, and we lowered it to the value shown rather than rejecting the call. |
x-ni-reasoning-effort-clamped | Your reasoning_effort was above the strongest tier this route supports, and we ran it at the effort shown instead. |
Both carry the value that was actually applied, not the one you sent, since you already know what you sent.
If you would rather these cases fail loudly than be adjusted, send "strict_max_tokens": true for the output limit. A reasoning effort this route does not support at all, including a misspelling, is always rejected with a 400 that names the values the route accepts.
Streaming
curl -sS https://northerninference.ca/v1/chat/completions \
-H "Authorization: Bearer $NI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-haiku-4-5-20251001-t4",
"messages": [{"role": "user", "content": "count to five"}],
"max_tokens": 30,
"stream": true,
"stream_options": {"include_usage": true}
}'
Full custody pull after the call
curl https://northerninference.ca/api/usage/custody/8f2e3b7d9a1c4f08 \
-H "Authorization: Bearer $NI_API_KEY"
Disable fallbacks
curl https://northerninference.ca/v1/chat/completions \
-H "Authorization: Bearer $NI_API_KEY" \
-d '{
"model": "claude-sonnet-4-6-t4",
"messages": [{"role":"user","content":"hi"}],
"max_tokens": 10,
"allow_fallbacks": false
}' \
-H "Content-Type: application/json"
Enable PII substitution
curl https://northerninference.ca/v1/chat/completions \
-H "Authorization: Bearer $NI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-haiku-4-5-20251001-t4",
"messages": [{"role": "user", "content": "My name is Jane Doe, email jane@example.com"}],
"max_tokens": 30,
"pii_substitution": true
}'
Entities (name, email, phone, SSN, credit card, IP, location) are replaced with deterministic Faker-generated substitutes before leaving NI infrastructure, and the originals are restored in the response before it reaches you. The pii_audit_log table records what was substituted; admins see it under portal → Admin → PII Audit.
Vision (image input)
Send the image as an image_url content block to any route whose supports_vision is true:
B64=$(base64 -i screenshot.png | tr -d '\n')
curl -sS https://northerninference.ca/v1/chat/completions \
-H "Authorization: Bearer $NI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-6-t4",
"messages": [
{"role": "user", "content": [
{"type": "text", "text": "What does this image say?"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,'"$B64"'"}}
]}
],
"max_tokens": 300
}'
See vision-and-capabilities.md for the full guide, including how a client with a hardcoded model table can be told an NI route supports images.
Listing models: two endpoints, two shapes
NI has two model-list endpoints. They return DIFFERENT JSON shapes on purpose, so match your jq path to the endpoint:
| Endpoint | Auth | Wrapper | ID field |
|---|---|---|---|
GET /v1/models | API key required | .data[] | .id |
GET /api/billing/models | none (public) | .models[] | .model_id (also .id) |
/v1/models is the OpenAI-compatible list. It requires a key and returns the routes that key is allowed to call, in the standard OpenAI shape ({"object": "list", "data": [{"id": ...}]}). Point any OpenAI SDK at https://northerninference.ca/v1 and client.models.list() just works.
curl -sS https://northerninference.ca/v1/models \
-H "Authorization: Bearer $NI_API_KEY" \
| jq '.data[] | {id, supports_vision, recommended_for_agentic, capabilities}'
/api/billing/models is the unauthenticated public catalog. It returns every public route with full metadata (pricing, privacy tier, data residency, capabilities), wrapped in {"models": [ ... ]}.
curl -sS https://northerninference.ca/api/billing/models \
| jq -c '.models[] | {id: .model_id, name: .display_name, tier: .privacy_tier.label, in: .input_cost_per_m_usd, out: .output_cost_per_m_usd}'
Common mistake: /v1/models puts the array under .data (not .models), and each item uses .id (there is no .name). Using .models[] against /v1/models yields Cannot iterate over null. An unauthenticated /v1/models call returns a 401 whose body links back to the public /api/billing/models catalog.