Skip to content

How do I give my AI agent its own scoped API key?

By , founder of Apier

Mint a sub-key under one of your account keys, from the dashboard or with one call to POST /api/v1/account/keys/{id}/subkeys. A sub-key carries a subset of the parent key's scopes, a mandatory expiry (30 days by default, 365 at most) and its own daily and monthly spend caps, and every metered call it makes is paid from the parent's wallet. The agent never sees your account key. Revoke the sub-key, or let it expire, and the agent is refused from its next request.

A chain: your account key stays with you and mints a sub-key through POST /api/v1/account/keys/{id}/subkeys with scopes, expiry and caps; the sub-key is handed to the agent, whose metered calls settle on the parent's wallet and caps; a dashed arrow from the owner marks DELETE revoking the sub-key alone.Your account keystays with youPOST .../keys/{id}/subkeysscopes, expiry, capsSub-keyhanded to the agentMetered callsby the agentThe parent's wallet and capsthe sub-key spends, never holdsDELETE revokes the sub-key only
The agent holds only the right-hand box. Everything it spends lands on the parent's wallet and caps, and one DELETE from the owner cuts the chain at the sub-key without touching the account key.

Why not just share my account key with the agent?

Because an account key is the whole account's API access: every scope the account holds, the account and billing routes, top-up requests. An agent that holds it can do all of that too, by design, by a prompt it was fed, or by a bug. (Minting and revoking keys stays a signed-in dashboard action, never something a bearer key does.) A leaked account key is a leaked account. A sub-key is refused on every account, key-management and billing route with 403 SUBKEY_ROUTE_FORBIDDEN before any handler runs, so a leaked sub-key can only use the scopes it was given, up to its cap, until its expiry or your revoke. A sub-key can never carry subscribe:webhooks or admin:keys, so it cannot register webhooks; give an agent only the read scopes its job needs.

The limits work in your favour too. An account has at most three active account keys, one per environment rather than one per agent. Sub-keys sit outside that count: each account key may hold up to ten live sub-keys, so one key can carry a fleet of agents, each with its own name in the audit log, its own expiry and budget. Rate limits and the unique-organisation backoff are counted on the root key across the whole family, so ten sub-keys never multiply the parent's allowance or hide a scraping pattern.

There is also a one-level rule. A sub-key cannot mint sub-keys, so the chain of trust is always two links long: you, and the agent you handed a key to. Every credential in your account is one of your three keys or a child of one that you minted yourself. The issuance-token flow for agents that fetch their own first key still exists; a sub-key is the choice when you, the owner, set the scope and budget up front.

What can I limit on a sub-key?

Three things, and the server enforces all of them inside one database function under a per-parent lock, so a race between two mints cannot slip past a rule. Scopes must be a subset of the parent's. A parent holding read:* can hand down any read scope, a parent holding only read:brreg can hand down only that, and admin:keys is never issued to a sub-key whatever the parent holds. The effective scope is computed at every call as the intersection of the sub-key's stored scopes and the parent's current scopes, so narrowing the parent later narrows every sub-key on its next request.

Expiry is mandatory. Omit expires_at and the sub-key lives 30 days; name one and it may be at most 365 days out, computed in UTC milliseconds so no timezone arithmetic can stretch it. When the instant passes, the agent's next call answers 401 AUTH_SUBKEY_EXPIRED. An agent that should live longer gets a new sub-key, not an extension: rotation is the normal path, and the dashboard keeps the expired row as history so you can see what the old key was allowed to do.

Caps are the agent's own daily and monthly spend limits in øre. Where the parent has no cap of a kind, the sub-key must name one, so an uncapped account key can never produce an uncapped agent; where the parent has one, the sub-key's may not exceed it. The sub-key has no wallet of its own. Each metered call is checked against the sub-key's caps and against the parent's caps counted over the parent and all its sub-keys, and only then debited from the parent's balance. A refusal is 402 SPEND_CAP_REACHED with cap_scope set to own or parent, so the agent can tell whose limit it hit. Topping up never lifts a cap; raising it is the owner's call.

How do I revoke a sub-key, and what does the agent see?

Click Revoke beside the sub-key in the dashboard, or call DELETE /api/v1/account/keys/{id}/subkeys/{subkeyId}. The revoke is one conditional update keyed on the sub-key, its parent, your account and the not-yet-revoked state, so it is exactly-once: a repeat, a wrong owner or an unknown id all answer one 404 and write no second audit row. The parent is untouched. Revoking the parent needs no per-sub-key call at all: from the next request on, every sub-key under it answers 401 AUTH_PARENT_KEY_REVOKED.

The plaintext sub-key is shown exactly once, in the 201 body of the mint and in the dashboard reveal, and Apier keeps only its SHA-256 hash. The mint route deliberately takes no Idempotency-Key: a replay cache would keep the plaintext recoverable for a day, which is the opposite of what a one-time secret needs. A repeated mint is simply a new sub-key, bounded by the per-parent limit. Hand the plaintext to the agent out of band, in its secret store, and never in a prompt or a log line.

A mint writes an audit row naming the agent, its scopes, expiry, caps and parent id; a revoke names the sub-key and parent ids; neither holds key material, and the list route returns revoked and expired sub-keys as history. That is what a reviewer asks for: which agent could read what, under which budget, when, and who switched it off. For how an agent should hold and present credentials, see the guide on integrating an agent over MCP.

Make the first call

The mint runs from your signed-in dashboard session, so the easiest path is the Sub-keys button beside a key on the dashboard. The curl below is the same request. The TypeScript snippet is the agent's side, holding only the sub-key.

# From your signed-in dashboard session (the route is session-cookie auth).
# Body: a subset of the parent's scopes and the caps the agent gets. expires_at
# is omitted here, so the sub-key expires 30 days from now (the default; up to
# 365 days when you send an RFC 3339 instant).
curl -s -X POST https://www.apier.no/api/v1/account/keys/<your-key-id>/subkeys \
  -H "content-type: application/json" \
  --cookie "<your dashboard session cookie>" \
  -d '{
    "name": "invoice-checker",
    "scopes": ["read:brreg"],
    "daily_cap_ore": 2000,
    "monthly_cap_ore": 20000
  }'
# 201: data.plaintext_key is shown ONCE. Hand it to the agent out of band.
// The agent's side. It holds only the sub-key and never sees your account key.
const res = await fetch(
  "https://www.apier.no/api/v1/company/999999999/context",
  { headers: { Authorization: `Bearer ${process.env.APIER_SUBKEY}` } },
);

if (res.status === 402) {
  // Two 402s share the status; error_code tells them apart.
  const body = await res.json();
  if (body.error_code === "SPEND_CAP_REACHED") {
    // cap_scope says whether the agent's own cap or the parent's
    // family-wide cap refused the call. Topping up does not help; wait
    // for resets_at or have the owner raise the cap.
    console.warn(`cap reached (${body.cap_scope}); resets at ${body.resets_at}`);
  } else {
    // INSUFFICIENT_CREDITS: the parent's wallet is empty. Only the owner
    // can top it up, at body.top_up_url; a sub-key is refused on every
    // billing route, so hand the link to the owner instead of retrying.
    console.warn(`wallet empty; the owner tops up at ${body.top_up_url}`);
  }
}

if (res.status === 401) {
  // AUTH_SUBKEY_EXPIRED or AUTH_PARENT_KEY_REVOKED: the owner decides
  // what happens next. The agent cannot mint itself a new key.
}

Frequently asked questions

What is a sub-key, and how is it different from my API key?
A sub-key is a credential minted under one of your account keys for one agent. It carries a subset of the parent's scopes, a mandatory expiry and its own spend caps, and it spends the parent's wallet. It cannot mint, list or revoke keys, cannot reach billing, and never counts toward your three account keys.
Can a sub-key have more access than the key it was minted from?
No, in any dimension. Every requested scope must be held by the parent, a cap may never exceed the parent's cap of the same kind, and the expiry is at most 365 days. If you later narrow the parent's scopes, every sub-key is narrowed on its next call, because the effective scope is computed as the intersection at call time.
What happens when I revoke a sub-key or let it expire?
The agent is refused from its next request. An expired sub-key answers 401 AUTH_SUBKEY_EXPIRED, a revoked one is simply no longer valid, and if you revoke the parent every sub-key under it answers 401 AUTH_PARENT_KEY_REVOKED without a per-sub-key call. Revocation is a soft flag, so the history stays listable.
Who pays for the agent's calls, and how do caps work?
The parent key's wallet pays. Each call is checked against the sub-key's own daily and monthly caps and against the parent's caps counted over the parent and all its sub-keys, inside one database function, before anything is charged. A refusal is 402 SPEND_CAP_REACHED with cap_scope set to own or parent.
How many sub-keys can one key have?
At most 10 live sub-keys per parent key, where live means not revoked and not expired. The eleventh mint answers 409 MAX_SUBKEYS_REACHED. Revoked and expired sub-keys stay in the list as history but do not take a slot, so rotating an agent's key never needs a bigger limit.