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:
| Credential | Prefix | What it does |
|---|---|---|
| Management credential | ni_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 key | ni_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
- Make a random PKCE verifier and its S256 challenge.
- Send
POST /api/agent/v1/enrollmentswith the owner's email address, a label for your agent, the scopes you want and the challenge. - The owner receives an email, opens the approval page and approves. A new owner sets a password and accepts the legal documents first.
- Poll
GET /api/agent/v1/enrollments/{id}until the status isapproved. - Send
POST /api/agent/v1/enrollments/{id}/claimwith the verifier. The response carries theni_mgmt_credential, shown once. - Create an inference key with
POST /api/agent/v1/keys. Theni_live_secret is shown once. - 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. - 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:
delivery | Meaning |
|---|---|
owner_email | The email went to the account owner. |
email_failed | The 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. |
sending | Another request is sending it right now. Check again shortly. |
not_sent | No 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
| Scope | What the owner is told | Allows |
|---|---|---|
account:read | See your account details and balance | GET /account |
keys:read | List your API keys | GET /keys |
keys:write | Create, change, rotate and revoke API keys | POST /keys, PATCH /keys/{id}, POST /keys/{id}/rotate, DELETE /keys/{id} |
billing:read | See whether a payment method is set up, and your payments | GET /billing, GET /billing/topups/{id}, GET /billing/setup-sessions/{id} |
billing:setup | Start adding a payment method, which you complete yourself on Stripe | POST /billing/setup-sessions |
billing:topup | Start a credit purchase, which you pay for yourself on Stripe | POST /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
| Limit | Value |
|---|---|
| Enrollment requests, all callers combined | 5 per hour |
| Enrollment requests per email address | 5 per hour |
| Enrollment requests per IP address | 10 per hour |
| Distinct email addresses per IP address | 1 per 24 hours |
| Enrollment waiting for the owner | 24 hours, one per email address |
| Management credential | 90 days |
| Claim attempts | 10 per enrollment and 30 per IP address, per 10 minutes |
| Idempotency records | 7 days |
| New payment links | 10 per hour per credential and 30 per day per account |
| Open payment links | 3 top-ups and 3 card setups per account |
| Payment link lifetime | 60 minutes by default |
| Largest agent top-up | CA$500 by default, never above the platform maximum |
| Live saved-card reads | 4 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.
| Status | error.code | Meaning and what to do |
|---|---|---|
| 401 | credential_required | No bearer credential was sent. |
| 401 | wrong_credential_class | The token is not a ni_mgmt_ credential, for example an inference key. |
| 401 | invalid_credential | Unknown, expired or revoked credential. Start a new enrollment. |
| 401 | invalid_code_verifier | The verifier does not match the challenge. Send the original verifier. |
| 403 | insufficient_scope | The credential was not granted the scope the call needs. Start a new enrollment that asks for it. |
| 403 | agent_registration_disabled | New agent sign-ups are switched off on this service. |
| 403 | agent_access_blocked | The operator paused all agent access. Your credential is not revoked; retry later. |
| 403 | agent_billing_disabled | Agent payments are switched off on this service. |
| 404 | enrollment_not_found | No such enrollment. |
| 404 | not_found | No such key on this account. |
| 404 | payment_not_found | No such top-up for this account. |
| 404 | setup_not_found | No such card setup for this account. |
| 409 | idempotency_key_reused | The key was already used for a different request. Use a new key. |
| 409 | enrollment_already_pending | A request for this address is already waiting for its owner. Poll that one, or wait for it to expire. |
| 409 | request_in_progress | A request with this Idempotency-Key is still being processed. Retry shortly with the same key. |
| 409 | checkout_not_created | The payment page for this key could not be created in time. Retry with a new Idempotency-Key. |
| 409 | checkout_not_available | A purchase cannot be started for this account right now. |
| 409 | account_unavailable | The account is not available for billing, for example because it is being deleted. |
| 409 | not_approved | Claim before approval, or after decline or expiry. Check the status. |
| 409 | already_claimed | The credential was already issued and is never shown again. Start a new enrollment. |
| 422 | invalid_scopes | An unknown scope, or none. The message lists the available scopes. |
| 422 | scope_not_available | A payment scope was requested while agent payments are switched off. Ask for the other scopes. |
| 422 | amount_out_of_range | amount_cents is outside the allowed range. The message states it. |
| 422 | unsupported_currency | Only the account currency is supported. Omit currency or send cad. |
| 422 | invalid_agent_label | agent_label must be 1 to 80 characters. |
| 422 | invalid_code_challenge | code_challenge must be the 43-character unpadded base64url SHA-256 of the verifier. |
| 422 | invalid_idempotency_key | Idempotency-Key must be 8 to 200 printable ASCII characters. |
| 429 | rate_limit | Too many calls. Wait for Retry-After. |
| 429 | billing_rate_limited | Too many payment links were requested. Wait for Retry-After. |
| 429 | too_many_open_checkouts | The account already has the most open payment links allowed. Wait for one to be paid or to expire. |
| 429 | card_read_limited | The saved card was read too often. Poll the purchase or setup instead. |
| 429 | agent_enrollment_busy | Too many enrollment requests across all callers this hour. Retry after Retry-After. |
| 429 | agent_enrollment_ip_email_limit | This IP address already requested access for a different email address in the last 24 hours. |
| 502 | payment_provider_unavailable | Stripe refused to create the payment page. Retry with a new Idempotency-Key. |
| 503 | payment_provider_unavailable | The outcome is unknown. Retry with the same Idempotency-Key; it cannot create two payment pages. |
| 503 | payment_provider_not_configured | Payments are not configured on this service. |
| 503 | approval_email_failed | The 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.
- The approval email names the agent and lists its scopes in plain language. If you did not expect it, ignore it or choose Decline on the approval page. The request expires after 24 hours.
- Approving never shares your password. A new account gets a password you choose and the legal documents you accept yourself.
- The agent receives a credential that can manage API keys and read the account. If you also approve the payment permissions, it can ask to add a card or buy credits, but it can never pay for you. It cannot call models or change your password.
- Open the API Keys page in the portal and find Agent access to see every agent you approved and revoke any of them at once. Revoking ends that agent's access. Keys it already created keep working until you revoke them on the same page.
- Changing your password, suspending the account or deleting it also ends every agent credential.
- An account created by an agent starts with no credit. You can add funds in the portal, or, if you approved the payment permissions, the agent can ask: Northern Inference then emails you the Stripe link. Nothing is charged unless you open it and pay on Stripe. The agent never sees that link, and you never share your card with it. If you did not expect the email, ignore it; the link expires in 60 minutes.
More
- OpenAPI 3.1 description of /api/agent/v1/
- llms.txt, the plain-text summary for agents
- Calling inference with curl and the other integration guides