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

AI agent self-service

How an AI agent requests access to a Northern Inference account, gets the owner's approval and manages its own inference keys through a documented API.

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.

What agent self-service is

An AI agent can ask for access to a Northern Inference account, get the account owner's approval, and then manage that account's inference keys itself. When the operator has agent payments switched on, the agent can also start a card setup or a credit purchase that the owner completes on Stripe. The agent never holds the owner's password, a browser session or card details. The owner stays in control: nothing is granted until the person who owns the email address approves the request, and nothing is charged unless the owner pays on Stripe.

The agent API lives under https://northerninference.ca/api/agent/v1/. It uses two different credentials, and neither works in place of the other:

CredentialPrefixWhat it does
Management credentialni_mgmt_Manages the account through /api/agent/v1/: reads the account, creates, lists, edits, rotates and revokes inference keys, and, with the payment scopes, starts card setups and credit purchases for the owner to complete. It cannot call models.
Inference keyni_live_Calls models on the OpenAI-compatible and Anthropic-compatible API at https://northerninference.ca/v1. It cannot change the account.

The machine-readable description of every endpoint on this page is agent-openapi.yaml (OpenAPI 3.1). A short summary for agents is in llms.txt.

The steps

  1. Make a random PKCE verifier and its S256 challenge.
  2. Send POST /api/agent/v1/enrollments with the owner's email address, a label for your agent, the scopes you want and the challenge.
  3. The owner receives an email, opens the approval page and approves. A new owner sets a password and accepts the legal documents first.
  4. Poll GET /api/agent/v1/enrollments/{id} until the status is approved.
  5. Send POST /api/agent/v1/enrollments/{id}/claim with the verifier. The response carries the ni_mgmt_ credential, shown once.
  6. Create an inference key with POST /api/agent/v1/keys. The ni_live_ secret is shown once.
  7. Fund the account: start a credit purchase with POST /api/agent/v1/billing/topups. Northern Inference emails the Stripe link to the owner, who pays on Stripe.
  8. Poll the purchase until it is paid, then call models with the ni_live_ key.

1. Make a verifier and a challenge

The verifier is a random string of 43 to 128 characters from A-Z a-z 0-9 - . _ ~. Keep it secret: it is what proves the claim comes from the agent that made the request. The challenge is the unpadded base64url SHA-256 of the verifier, always 43 characters.

CODE_VERIFIER=$(openssl rand -hex 32)
CODE_CHALLENGE=$(printf %s "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
echo "$CODE_CHALLENGE"

The same in Python:

import base64, hashlib, secrets

code_verifier = secrets.token_urlsafe(64)  # 86 characters
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode("ascii")).digest()
).rstrip(b"=").decode("ascii")

2. Request access

Send an Idempotency-Key of 8 to 200 printable ASCII characters. Repeating the same request with the same key returns the same enrollment and sends no second email. The same key with a different request is refused with 409 idempotency_key_reused.

curl -sS https://northerninference.ca/api/agent/v1/enrollments \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "email": "owner@example.org",
    "agent_label": "Release notes agent",
    "scopes": ["account:read", "keys:read", "keys:write"],
    "code_challenge": "'"$CODE_CHALLENGE"'",
    "code_challenge_method": "S256",
    "account_name": "Example Org"
  }'

agent_label is 1 to 80 characters and is shown to the owner. account_name is optional and only names a new account. The response is 202 Accepted and has the same shape whether or not an account already exists for that address:

{
  "enrollment_id": "0b5c1d2e-7a54-4f55-9d3e-2c7f1d6a8e10",
  "status": "pending",
  "expires_at": "2026-10-13T15:04:05+00:00",
  "agent_label": "Release notes agent",
  "scopes": ["account:read", "keys:read", "keys:write"],
  "owner_action": "Nothing is granted until the owner of this email address approves the request from the email Northern Inference sends them. Poll status_url, then claim.",
  "status_url": "/api/agent/v1/enrollments/0b5c1d2e-7a54-4f55-9d3e-2c7f1d6a8e10",
  "claim_url": "/api/agent/v1/enrollments/0b5c1d2e-7a54-4f55-9d3e-2c7f1d6a8e10/claim"
}

The request stays open for 24 hours. Only one request per email address can be waiting at a time.

3. The owner approves

Northern Inference emails the owner a single-use link to /agent-approve.html. The page shows the agent's label and what each scope allows. Nothing happens when the page loads; the owner has to click. A new owner sets a password and accepts the current Terms of Service, Privacy Policy and Acceptable Use Policy. An existing owner signs in first, and completes the second-factor check if one is set up. The owner can also decline. The agent cannot approve its own request.

4. Poll the status

curl -sS https://northerninference.ca/api/agent/v1/enrollments/$ENROLLMENT_ID

The response has the same fields as step 2. status is one of pending, approved, declined, expired or claimed. Poll every few seconds at most; the status endpoint allows 240 calls per hour per IP address. Stop on declined or expired and start a new request if the owner still wants one.

5. Claim the credential

curl -sS -X POST https://northerninference.ca/api/agent/v1/enrollments/$ENROLLMENT_ID/claim \
  -H "Content-Type: application/json" \
  -d '{"code_verifier": "'"$CODE_VERIFIER"'"}'
{
  "credential": "ni_mgmt_...",
  "token_type": "Bearer",
  "credential_id": "5f0e3c1a-9b1d-4c0e-8a55-61c7d0f2b3a4",
  "key_prefix": "ni_mgmt_AbCdEfGh",
  "scopes": ["account:read", "keys:read", "keys:write"],
  "expires_at": "2027-01-10T15:09:41+00:00",
  "note": "Shown once. Northern Inference keeps only a hash and cannot show it again."
}

Store the credential at once. Northern Inference keeps only a hash and shows the credential a single time; a second claim answers 409 already_claimed with no secret. A credential lasts 90 days. The scopes cannot be widened later; to get more, start a new enrollment.

6. Manage keys

Send the credential as Authorization: Bearer ni_mgmt_....

export NI_MGMT=ni_mgmt_YOUR_CREDENTIAL

# Read the account (account:read)
curl -sS https://northerninference.ca/api/agent/v1/account \
  -H "Authorization: Bearer $NI_MGMT"

# List keys (keys:read)
curl -sS https://northerninference.ca/api/agent/v1/keys \
  -H "Authorization: Bearer $NI_MGMT"

# Create a key (keys:write). Idempotency-Key is required.
curl -sS https://northerninference.ca/api/agent/v1/keys \
  -H "Authorization: Bearer $NI_MGMT" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "release-notes-agent",
    "allowed_privacy_tiers": ["managed_canadian_cloud"],
    "max_rpm": 60,
    "max_budget_cents": 500
  }'

A first create answers 201 with the new key's settings and, once, the secret in key:

{
  "id": "c9d8e7f6-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
  "key_prefix": "ni_live_AbCd",
  "name": "release-notes-agent",
  "allowed_privacy_tiers": ["managed_canadian_cloud"],
  "max_rpm": 60,
  "max_budget_cents": 500,
  "...": "other key settings",
  "key": "ni_live_...",
  "secret_already_issued": false
}

If the response is lost, retrying with the same Idempotency-Key and the same body answers 200 with the key's settings and "secret_already_issued": true. It never returns the secret again and never makes a second key. Recover by rotating the key.

# Change settings (keys:write). Only the fields you send change.
curl -sS -X PATCH https://northerninference.ca/api/agent/v1/keys/$KEY_ID \
  -H "Authorization: Bearer $NI_MGMT" \
  -H "Content-Type: application/json" \
  -d '{"max_rpm": 30}'

# Rotate (keys:write). Idempotency-Key is required. The new key keeps the old
# key's settings. The old key keeps working for 24 hours, then expires.
curl -sS -X POST https://northerninference.ca/api/agent/v1/keys/$KEY_ID/rotate \
  -H "Authorization: Bearer $NI_MGMT" \
  -H "Idempotency-Key: $(uuidgen)"

# Revoke a key (keys:write)
curl -sS -X DELETE https://northerninference.ca/api/agent/v1/keys/$KEY_ID \
  -H "Authorization: Bearer $NI_MGMT"

# End this credential (any scope)
curl -sS -X DELETE https://northerninference.ca/api/agent/v1/credential \
  -H "Authorization: Bearer $NI_MGMT"

An agent can set a key's name, allowed privacy tiers (managed_canadian_cloud for Tier 3 Canadian routes, provider_api for Tier 4), route allow-list, rate limits (max_rpm, max_tpm), spend cap (max_budget_cents), zero-data-retention mode (enforce, prompt or allow) and, on create, an expiry. Bring-your-own provider keys and the per-key display switches stay with the owner in the portal.

7. Call models

Use the ni_live_ key exactly as in the curl guide:

curl https://northerninference.ca/v1/chat/completions \
  -H "Authorization: Bearer $NI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"ROUTE_ID","messages":[{"role":"user","content":"hi"}],"max_tokens":20}'

Copy the exact route from https://northerninference.ca/api/billing/models (no key needed) or GET /v1/models.

Funding

An account created by an agent starts with no credit, and inference needs a funded balance. An agent that holds the payment scopes can start the funding, but the owner always completes it on Stripe. Northern Inference sends the owner the Stripe link by email. The agent never receives the link and never sees or enters card details. If agent payments are switched off on this service, billing calls answer 403 agent_billing_disabled and the payment scopes cannot be requested.

The payment scopes are billing:read (see whether a payment method is set up, and the payments), billing:setup (start adding a payment method) and billing:topup (start a credit purchase). Ask for the ones you need when you request access; the owner sees each in plain language.

Start a credit purchase

curl -sS https://northerninference.ca/api/agent/v1/billing/topups \
  -H "Authorization: Bearer $NI_MGMT" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"amount_cents": 2500}'

amount_cents is the credits to buy, a whole number from 2500 (CA$25.00) up to CA$500 by default. Send nothing else: a body with a card field is refused. The owner receives an email with the credits, the service fee and the total, and a button to pay on Stripe. The answer is 201:

{
  "payment_id": "165d9622-8413-4ac1-9d89-22e199b6b8ca",
  "display_status": "open_unpaid",
  "session_id": "cs_live_...",
  "expires_at": "2026-10-13T03:39:38+00:00",
  "currency": "cad",
  "amount_cents": "2500",
  "fee_cents": "225",
  "charge_cents": "2725",
  "replayed": false,
  "delivery": "owner_email"
}

Money fields are exact decimal strings. charge_cents is the credits plus the service fee before tax, which Stripe adds on its page. The response has no payment link: do not look for one. delivery says what happened to the email:

deliveryMeaning
owner_emailThe email went to the account owner.
email_failedThe purchase exists but the email could not be sent. Repeat the exact same request with the same Idempotency-Key to try the email again. No second purchase is made.
sendingAnother request is sending it right now. Check again shortly.
not_sentNo email went out and none will, because the purchase ended first. Start a new one.

A retry with the same Idempotency-Key and the same body returns 200 with the same purchase and sends no second email. The same key with a different body is refused with 409 idempotency_key_reused.

Poll the purchase

curl -sS https://northerninference.ca/api/agent/v1/billing/topups/$PAYMENT_ID \
  -H "Authorization: Bearer $NI_MGMT"

This needs billing:read and reads Northern Inference's own records, never Stripe, so poll it every few seconds. display_status is open_unpaid while the owner has not paid, processing, then succeeded with credited: true once the credits are on the balance. expired_unpaid means the link ended without a payment (links last 60 minutes by default); start a new purchase if the owner still wants one. last_attempt_failed: true means the owner's card was declined and the link is still open. GET /api/agent/v1/account then reports the new available_balance_cents.

Add a payment method

curl -sS -X POST https://northerninference.ca/api/agent/v1/billing/setup-sessions \
  -H "Authorization: Bearer $NI_MGMT" \
  -H "Idempotency-Key: $(uuidgen)"

# Poll it (billing:read). status: open, processing, completed or expired.
curl -sS https://northerninference.ca/api/agent/v1/billing/setup-sessions/$SETUP_ID \
  -H "Authorization: Bearer $NI_MGMT"

This needs billing:setup and sends the owner an email with a button to add a card on Stripe. Neither the agent nor Northern Inference sees the card number, and adding a card does not charge it. The response and the delivery rules are the same as for a purchase. Agents cannot turn on automatic top-up and cannot charge a saved card: every purchase is completed by the owner.

Read the billing status

curl -sS https://northerninference.ca/api/agent/v1/billing \
  -H "Authorization: Bearer $NI_MGMT"

This needs billing:read and shows the balance, the top-up terms, open purchases, the 10 latest payments and the saved card as brand, last four digits and expiry. The card is a live read from Stripe and is limited to 4 per hour per account, so poll the purchase or setup endpoints for completion, not this one.

What the owner sees after Stripe

Stripe sends the owner to https://northerninference.ca/agent-payment.html, a temporary page that shows whether the payment was received, is still processing, or has expired. The page serves the owner, not the agent. It asks Northern Inference one question about the Stripe session and shows no amount, name or card. The agent should poll the endpoints above instead.

Scopes

ScopeWhat the owner is toldAllows
account:readSee your account details and balanceGET /account
keys:readList your API keysGET /keys
keys:writeCreate, change, rotate and revoke API keysPOST /keys, PATCH /keys/{id}, POST /keys/{id}/rotate, DELETE /keys/{id}
billing:readSee whether a payment method is set up, and your paymentsGET /billing, GET /billing/topups/{id}, GET /billing/setup-sessions/{id}
billing:setupStart adding a payment method, which you complete yourself on StripePOST /billing/setup-sessions
billing:topupStart a credit purchase, which you pay for yourself on StripePOST /billing/topups

The payment scopes can be requested only while agent payments are switched on. Otherwise the request is refused with 422 scope_not_available.

Ending your own credential with DELETE /credential needs a valid credential but no particular scope.

Limits and lifetimes

LimitValue
Enrollment requests, all callers combined5 per hour
Enrollment requests per email address5 per hour
Enrollment requests per IP address10 per hour
Distinct email addresses per IP address1 per 24 hours
Enrollment waiting for the owner24 hours, one per email address
Management credential90 days
Claim attempts10 per enrollment and 30 per IP address, per 10 minutes
Idempotency records7 days
New payment links10 per hour per credential and 30 per day per account
Open payment links3 top-ups and 3 card setups per account
Payment link lifetime60 minutes by default
Largest agent top-upCA$500 by default, never above the platform maximum
Live saved-card reads4 per hour per account

A throttled call answers 429 with a Retry-After header in seconds. Respect it. Turning off agent sign-ups refuses new enrollments, status checks, approvals and claims with 403 agent_registration_disabled, but does not cut off a credential that was already issued. The operator can also pause all agent access: every call with a ni_mgmt_ credential then answers 403 agent_access_blocked until the pause ends, and the credential is not revoked. From one IP address, a second distinct email address inside 24 hours answers 429 agent_enrollment_ip_email_limit.

Errors

Errors use the platform error envelope: the HTTP status, an error object with code, message, ni_request_id and ni_guidance, and the same request ID in the X-NI-Request-ID header. Branch on the status and error.code.

Statuserror.codeMeaning and what to do
401credential_requiredNo bearer credential was sent.
401wrong_credential_classThe token is not a ni_mgmt_ credential, for example an inference key.
401invalid_credentialUnknown, expired or revoked credential. Start a new enrollment.
401invalid_code_verifierThe verifier does not match the challenge. Send the original verifier.
403insufficient_scopeThe credential was not granted the scope the call needs. Start a new enrollment that asks for it.
403agent_registration_disabledNew agent sign-ups are switched off on this service.
403agent_access_blockedThe operator paused all agent access. Your credential is not revoked; retry later.
403agent_billing_disabledAgent payments are switched off on this service.
404enrollment_not_foundNo such enrollment.
404not_foundNo such key on this account.
404payment_not_foundNo such top-up for this account.
404setup_not_foundNo such card setup for this account.
409idempotency_key_reusedThe key was already used for a different request. Use a new key.
409enrollment_already_pendingA request for this address is already waiting for its owner. Poll that one, or wait for it to expire.
409request_in_progressA request with this Idempotency-Key is still being processed. Retry shortly with the same key.
409checkout_not_createdThe payment page for this key could not be created in time. Retry with a new Idempotency-Key.
409checkout_not_availableA purchase cannot be started for this account right now.
409account_unavailableThe account is not available for billing, for example because it is being deleted.
409not_approvedClaim before approval, or after decline or expiry. Check the status.
409already_claimedThe credential was already issued and is never shown again. Start a new enrollment.
422invalid_scopesAn unknown scope, or none. The message lists the available scopes.
422scope_not_availableA payment scope was requested while agent payments are switched off. Ask for the other scopes.
422amount_out_of_rangeamount_cents is outside the allowed range. The message states it.
422unsupported_currencyOnly the account currency is supported. Omit currency or send cad.
422invalid_agent_labelagent_label must be 1 to 80 characters.
422invalid_code_challengecode_challenge must be the 43-character unpadded base64url SHA-256 of the verifier.
422invalid_idempotency_keyIdempotency-Key must be 8 to 200 printable ASCII characters.
429rate_limitToo many calls. Wait for Retry-After.
429billing_rate_limitedToo many payment links were requested. Wait for Retry-After.
429too_many_open_checkoutsThe account already has the most open payment links allowed. Wait for one to be paid or to expire.
429card_read_limitedThe saved card was read too often. Poll the purchase or setup instead.
429agent_enrollment_busyToo many enrollment requests across all callers this hour. Retry after Retry-After.
429agent_enrollment_ip_email_limitThis IP address already requested access for a different email address in the last 24 hours.
502payment_provider_unavailableStripe refused to create the payment page. Retry with a new Idempotency-Key.
503payment_provider_unavailableThe outcome is unknown. Retry with the same Idempotency-Key; it cannot create two payment pages.
503payment_provider_not_configuredPayments are not configured on this service.
503approval_email_failedThe approval email could not be sent and the request was cancelled. Retry with a new Idempotency-Key.

For account owners

An agent can only ask. You decide, and you see exactly what it wants before you decide.

More


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

↑