API endpoint https://northerninference.ca/v1 Use this as the base URL in every integration.

curl / HTTPie

Call Northern Inference directly with curl or HTTPie: a complete request for both wire formats on a sovereign Canadian AI API for government and regulated teams.

Northern Inference is a sovereign Canadian AI API for government, defence, health and regulated teams, including organizations under the Controlled Goods Program. Every route carries PIPEDA-aligned Canadian data residency, an exact path to the latest frontier and open-weight models, and a per-request chain-of-custody record.

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

Headers that appear only when we changed something

The headers above come back on every request. These are different: they appear only when Northern Inference adjusted your request so it would run instead of failing, when part of it could not reach the model, or, for x-ni-reasoning-state, when you sent reasoning items back. 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.

HeaderMeaning
x-ni-max-output-tokens-clampedYour 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-clampedYour reasoning_effort was outside the tiers this route supports (above its strongest, below its weakest such as minimal, or none on a route whose model always reasons), and we ran it at the effort shown instead.
x-ni-reasoning-stateYou sent reasoning items back on /v1/responses. replayed means NI sent all of them on to Claude. skipped=<reason> means none were sent on, and the request ran without that earlier reasoning. partial;skipped=<reason> means some were left out.
x-ni-params-droppedThe model behind this route refuses the request settings listed (for example temperature or top_p), so we left them out and the model used its own defaults instead of failing the call. A setting listed with a value (for example thinking.display=updates) means the route refuses that value, so we sent the nearest value it accepts instead. A /v1/responses reply shows left-out fields as null.
x-ni-params-not-forwardedOn /v1/responses, the parts of text listed (for example structured output or verbosity) are not sent to the model on this route, so the answer was produced without them.
x-ni-params-forwarding-unknownWe could not determine whether the settings listed reach the model on this route. Treat the answer as possibly produced without them.

The two clamp headers 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"

Tag a request with your own reference

Send X-NI-Reference and Northern Inference records the value on that request, shows it in the portal (Requests, Spend by reference, the monthly statement), and binds it into the signed custody record served by /api/usage/custody/{id}. Use it to attribute AI spend to a case file, a contract or a WBS code.

curl -sS -D /tmp/headers https://northerninference.ca/v1/chat/completions \
  -H "Authorization: Bearer $NI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-NI-Reference: 7c1f0a9e4b2d8e11" \
  -d '{"model":"claude-haiku-4-5-20251001-t4","messages":[{"role":"user","content":"hi"}],"max_tokens":5}' \
  > /dev/null

grep -i '^x-ni-reference' /tmp/headers

The response echoes it back as x-ni-reference so you can confirm what was recorded. Rules:

with a 400 invalid_reference before the request runs; a reference is never silently dropped.

send it to a provider. It is not in the request body the provider sees.

personal information in it. A keyed HMAC of your own identifier (for example HMAC-SHA256(secret, case_id) truncated to 32 hex characters) lets you match a request back to the file without the file number ever leaving your systems, which is what a court or an auditor needs.

If your client cannot set headers, send "ni_reference": "..." at the top level of the JSON body (or inside extra_body with an SDK). We remove it before dispatch. When both are present the header wins.

With the OpenAI or Anthropic SDKs, pass it per request with extra_headers, or once for the whole client with default_headers:

client = OpenAI(api_key=NI_API_KEY, base_url="https://northerninference.ca/v1",
                default_headers={"X-NI-Reference": reference_for(case_id)})

To read spend back per reference: GET /api/usage/by-reference?days=30 (add &format=csv for a file), and GET /api/usage/by-reference/statements?month=2026-08 for a closed month's statement.

Retries and billing

Each independent inference request receives its own NI-generated X-NI-Request-ID. A retry from your client is another request and may incur another charge, even when you reuse a reference or request header.

NI prevents repeated internal callbacks or settlement retries from charging the same execution twice. Stopping or disconnecting does not waive charges for substantiated provider usage, including reasoning already performed.

Use X-NI-Reference to group related requests and inspect their individual billing outcomes in request history. The former inference Idempotency-Key feature is removed: that header is ignored and is not forwarded to providers. NI does not store responses for replay.

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.

Tool calling on Gemini 3 routes

Gemini 3 models attach a thought signature to every function call they make. Google requires that signature back on the next request and rejects the turn with a 400 (Function call is missing a thought_signature in functionCall parts) when it is missing. Northern Inference carries the signature inside the tool call id. Replay the assistant message and its tool_calls[].id exactly as received, which every OpenAI-compatible SDK does by default, and the rule is satisfied without extra work. Don't shorten, regenerate or reformat tool call ids, and send the same id back as tool_call_id on the tool message. The signature is also exposed under provider_specific_fields on the message and on each tool call, and in streaming it arrives with the tool call delta. A text-only turn may carry a signature as well; replaying it is recommended, not enforced.

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:

EndpointAuthWrapperID field
GET /v1/modelsAPI key required.data[].id
GET /api/billing/modelsnone (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.


Spot a problem? Write to support at northerninference dot ca.

↑