# Authentication

> Authenticate with the INFRO API using sk_infro_ keys and the Authorization header. Create, label, rotate, and revoke keys, and set spend limits.

Source: https://infro.io/docs/authentication

> **Note:** INFRO is in private early access. Running these examples requires an early-access account, an active trial or subscription entitlement, and your own provider connection. Self-serve purchasing is not generally open. See [Early access](https://infro.io/early-access).

Every INFRO API request is authenticated with an API key sent in the `Authorization` header. Keys are created in the [console](https://dash.infro.io), start with `sk_infro_`, and work identically on every endpoint.

Each key carries its own label and optional spend limit, so you can give every service its own key, see what it spends, and cap the damage if one leaks. If you're making your first request, start with the [quickstart](https://infro.io/docs/quickstart).

## API keys

Keys are `sk_infro_` followed by a random string. Copy the full key into your secrets manager when you create it and treat it like a password. If you lose one, revoke it and create a replacement.

Create keys in the [console](https://dash.infro.io) under **Keys**; each gets a label, an optional spend limit, a rate-limit tier, and optionally an environment, a project and an expiry. An INFRO key reaches the models served by the provider accounts you have connected under **Providers** — your provider credentials stay sealed in INFRO, and applications only ever hold the INFRO key.

## The Authorization header

Pass the key as a bearer token. The header is the only authentication mechanism — there is no query-parameter or cookie alternative.

**curl**

```bash
curl https://api.infro.io/v1/chat/completions \
  -H "Authorization: Bearer $INFRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-5",
    "messages": [{"role": "user", "content": "Hello"}]
  }'
```

**Python**

```python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.infro.io/v1",
    api_key=os.environ["INFRO_API_KEY"],
)

completion = client.chat.completions.create(
    model="anthropic/claude-sonnet-5",
    messages=[{"role": "user", "content": "Hello"}],
)
print(completion.choices[0].message.content)
```

**TypeScript**

```typescript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.infro.io/v1",
  apiKey: process.env.INFRO_API_KEY,
});

const completion = await client.chat.completions.create({
  model: "anthropic/claude-sonnet-5",
  messages: [{ role: "user", content: "Hello" }],
});
console.log(completion.choices[0].message.content);
```

A missing, malformed, revoked, or expired key returns `401` with type `authentication_error`. A key that belongs to an archived project returns `403 permission_denied` with code `project_archived` instead, a key that was disabled returns `403` with code `key_disabled`, and a key called from outside its [allowed networks](#ip-allowlist) returns `403` with code `ip_not_allowed`. The examples above are the standard OpenAI clients pointed at INFRO's base URL; see [SDKs](https://infro.io/docs/sdks) for other languages and frameworks.

## Labels and spend limits

Labels identify keys in the console and in usage analytics. Spend limits are hard ceilings on estimated provider cost: once a key's usage reaches its limit, its requests return `402 budget_exceeded` while your other keys keep working. A limit can reset daily, weekly or monthly, or never (a lifetime cap), and can be changed later with **Edit spend limit**. Rate limits are also enforced per key — see [rate limits](https://infro.io/docs/api/limits).

- `label` (string) — Human-readable name shown in the console, in usage analytics, and in the `GET /v1/key` response. Use one key per service or environment so spend stays attributable.
- `limit` (number | null) — Hard ceiling in USD on the estimated provider cost of this key's requests, as reported by `GET /v1/key`. Requests return `402` once `usage` reaches it. `null` means no per-key cap, in which case only the organization, project and member budgets apply.

## Inspect the current key

`GET /v1/key` returns the label, spend limit, usage, and remaining budget of whichever key made the request — useful for verifying that a deployment picked up the right key. The full reference (request, response fields, code samples) lives in [Rate & spend limits](https://infro.io/docs/api/limits).

## Rotating and revoking keys

Revoking a key in the console takes effect immediately — subsequent requests with it return `401`. Revoke on any suspected exposure; it can't be undone.

To stop a key for a while instead, turn it off with the switch on its page in the console (or `PATCH` it with `"disabled": true`). Requests with a disabled key return `403` with code `key_disabled`; its secret, scope and limits are kept, and turning it back on makes the same secret work on the next request.

**Rotate** replaces a key in one step: in the console choose **Rotate…** on the key, or call `POST /console/keys/{id}/rotate` (management API: `POST /manage/v1/gateway_keys/{id}/rotate`). The new key gets a fresh secret, shown once, and every setting of the old one: label, project, limits, rate limit, model and route allowlists, environment, expiry, history access and allowed networks. You choose when the old key stops:

| Overlap | What happens to the old key |
| --- | --- |
| Immediately (`overlap_seconds: 0`) | Revoked at once, exactly as **Revoke** does. Use this if the key leaked. |
| 1 hour, 24 hours (the default) or 7 days | Keeps working until the overlap ends, then returns `401` like any expired key. It never outlives its own expiry. |

1. Rotate the key and copy the new secret.
2. Deploy it — update the environment variable or secrets manager entry your service reads.
3. Confirm traffic moved: `GET /v1/key` from the deployed service answers with the new key. In the console the old key shows how long it has left and links to its replacement.
4. Let the overlap run out, or revoke the old key early.

**Limits follow the key across a rotation.** The old and new key share one spend limit and one lifetime cap: spend recorded on the old key counts against the new one, and while both work their requests are admitted against a single ceiling. Rotating is never a way to get fresh spending room; raise the limit instead. In Analytics, a rotated key's history is shown on its replacement's row. Rotation is recorded in the audit log as `api_key.rotated`.

## IP allowlist

A key can be limited to the networks your servers call from. Set **Allowed networks** when you create the key or later from its menu, or send `allowed_ips` on `POST` / `PATCH /console/keys` and the management API. Each entry is an IPv4 or IPv6 address or CIDR range — `203.0.113.0/24`, `2001:db8::/48` — up to 32 per key. An empty list accepts any address. Every plan includes it.

- Entries are stored in canonical form: a bare address becomes `/32` or `/128`, IPv6 is lowercased and compressed. A range with host bits set (`203.0.113.7/24`) is refused with the network it probably meant (`203.0.113.0/24`).
- The check uses the full client address the Cloudflare edge reports, never `X-Forwarded-For`. An IPv4-mapped IPv6 address (`::ffff:203.0.113.7`) matches IPv4 entries; otherwise IPv4 and IPv6 never match each other.
- It fails closed: when no trusted client address is available, a key with an allowlist is refused.
- A refused request returns `403` with type `permission_denied` and code `ip_not_allowed`, before any budget, routing or provider call. The response never lists the allowed networks. The console shows how many requests each key's allowlist refused today.
- A change applies from the key's next request.
- It applies to every request a gateway key authenticates — inference, `/v1/usage`, `/v1/requests`, `/v1/key` and the FOCUS export. Management tokens and console sign-in are not affected.
- Use the addresses of the servers that call INFRO. Your browser's address is not one of them.

## Providers a key may use

A gateway key is INFRO's, not a provider's: your application sends it to INFRO, and INFRO calls your providers with your own provider keys. By default a gateway key may use **every provider** you connect, including ones you connect later. To limit it, choose **Only selected** under Providers when you create the key or later from its menu, or send `allowed_providers` on `POST` / `PATCH /console/keys` and the management API — provider ids such as `openai` or `openrouter`, as `GET /console/providers/kinds` lists them. An empty list means every provider. Every plan includes it.

- It holds on every path: a model name, a provider's own id (`laozhang/<model id>`), a route and per-request `fallbacks`. A route target on another provider is skipped; the route still serves from the rest.
- It narrows what the key's environment already reaches. A provider key scoped to another environment stays out of reach.
- When nothing the key may use can serve a request, the gateway answers `503` with code `connection_out_of_scope` and a sentence naming the providers the key allows and the one that could have served it. No retry clears it.
- A change applies from the key's next request. A rotated key keeps the same providers.

## Security best practices

> **Warning:** Never ship an API key in client-side code. A key in a browser bundle, mobile app, or public repo can be extracted by anyone and spent against your account. Treat any key that reaches a client as compromised: revoke it and route those requests through your own backend.

- Read keys from environment variables or a secrets manager. Never commit them to source control — `.env` files belong in `.gitignore`.
- Use one key per service and per environment. Separate keys make usage attributable and let you revoke one integration without touching the rest.
- Set a spend limit on every key. A leaked key with a limit is a bounded problem; a leaked key without one can spend against your provider account until you notice.
- Rotate on any suspicion of exposure. With an overlap both keys work while you deploy, so there's no reason to wait for proof; if the key leaked, rotate with no overlap.
- Restrict production keys to your servers' networks with an [IP allowlist](#ip-allowlist).

## 401 vs 402

Both statuses mean the request never reached a model, but the fixes differ. Both use the standard error envelope:

```json
{
  "error": {
    "message": "Invalid API key provided.",
    "type": "authentication_error",
    "code": null
  }
}
```

| Status | Error type | What it means | What to do |
| --- | --- | --- | --- |
| 401 | `authentication_error` | The key is missing, malformed, revoked, or expired | Check the `Authorization` header format and confirm the key is active in the console |
| 402 | `budget_exceeded` | A budget you set — on the key, the member, the project or the organization — has no room for this request | Raise or remove that budget in the console; `error.code` names which one (for example `key_budget` or `organization_budget`). The same status also covers `subscription_required` and `trial_payment_failed` — see [errors](https://infro.io/docs/api/errors). |

Neither is retryable — backing off and retrying a `401` or `402` just burns requests. Handle both as configuration errors and alert on them. The full status list and retry guidance are in [errors](https://infro.io/docs/api/errors).
