> ## Documentation Index
> Fetch the complete documentation index at: https://runinfra.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> API key scopes, creation, rotation, and expiration for the RunInfra inference API.

## Two key scopes

RunInfra supports two API key types. Pick the one that matches your integration shape.

### Workspace-scoped (recommended)

A single key reaches verified deployed models in your workspace. The `model` field in the request body selects the target:

```bash theme={"dark"}
curl https://api.runinfra.ai/v1/chat/completions \
  -H "Authorization: Bearer YOUR_RUNINFRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"llama-3.3-70b","messages":[{"role":"user","content":"Hello"}]}'
```

This follows the OpenAI SDK base URL convention for supported endpoints. Set `base_url` once, then call a deployed model ID with the supported request fields. Ideal for:

* Dashboards with multiple model endpoints
* Backend services that route to different models at runtime
* Team-level integrations where one key covers all workspace pipelines

### Pipeline-scoped (legacy)

A key bound to **one specific pipeline**. The pipeline id sits in the URL path:

```bash theme={"dark"}
curl https://api.runinfra.ai/v1/{pipelineId}/chat/completions \
  -H "Authorization: Bearer YOUR_RUNINFRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"Hello"}]}'
```

Good for:

* Clients that should only touch one pipeline
* Per-team keys with hard isolation
* Existing integrations built before the workspace-scoped flow

## Creating a key

<Steps>
  <Step title="Go to Settings → API Keys">
    Open [runinfra.ai/settings/api-keys](https://runinfra.ai/settings/api-keys).
  </Step>

  <Step title="Click 'Create API Key'">
    Pick the scope:

    * **Workspace**, key reaches any verified deployed model
    * **Pipeline**, pick a specific pipeline
  </Step>

  <Step title="Copy the full key immediately">
    The key is shown **once**. After you close the dialog only the hashed form is stored; we cannot recover the plaintext.
  </Step>

  <Step title="(Optional) Set rate limit + expiration">
    Rate limit defaults to your plan's allocation. You can lower it per-key for principle-of-least-privilege. Expiration defaults to never, set a date for compliance-sensitive workflows.
  </Step>
</Steps>

## Key format

Every key starts with `rp_live_` followed by 64 hex chars:

```text theme={"dark"}
rp_live_1212f66a95275a344d4042dcc9ce2c018c5251cb5be8504cd65874d776dd0d50
└──┬───┘└─────────────────────────────── 64 hex chars ──────────────────┘
 prefix
```

At rest the key is stored as a **SHA-256 hash**; the plaintext exists only in your environment. Even database compromise cannot leak usable keys directly.

## Rotation

Rotate a compromised or aging key without downtime:

<Steps>
  <Step title="Click 'Rotate' on the key row">
    The UI issues a new secret and links it to the old one via `rotated_from_id`.
  </Step>

  <Step title="Update your environment with the new key">
    Both old and new keys work during the grace window, deploy the new secret, roll out, verify traffic.
  </Step>

  <Step title="Click 'Revoke' on the old key">
    Once traffic has drained, deactivate the prior key. All future requests with it return `401`.
  </Step>
</Steps>

The rotation chain is tracked in `audit_logs` for SOC2 CC6.6 forensic trail.

## Expiration

Keys default to **never expire**. For compliance or ephemeral-CI use cases, set `expires_at` on creation:

```bash theme={"dark"}
curl https://runinfra.ai/api/apikeys \
  -H "Authorization: Bearer <session-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ci-runner",
    "expiresAt": "2026-12-31T23:59:59Z"
  }'
```

A daily cron revokes expired keys (`/api/cron/api-key-expiry`), if it lags, the gateway still refuses the key at the first authenticated request post-expiration, so there is no window of use after `expires_at`.

## Revocation

`DELETE /api/apikeys/{id}` or click **Revoke** in the UI. The key's `is_active` flips to `false` and every future request returns `401` with a structured JSON body:

```json theme={"dark"}
{
  "error": {
    "message": "API key is deactivated",
    "type": "auth_error"
  }
}
```

## Environment variable conventions

We recommend:

```bash theme={"dark"}
# Production
RUNINFRA_GATEWAY_KEY=rp_live_...
RUNINFRA_BASE_URL=https://api.runinfra.ai/v1

# Per-env keys (isolate blast radius if one env leaks)
RUNINFRA_GATEWAY_KEY_DEV=rp_live_...
RUNINFRA_GATEWAY_KEY_STAGING=rp_live_...
RUNINFRA_GATEWAY_KEY_PROD=rp_live_...
```

Most SDKs pick up `OPENAI_API_KEY` automatically, for zero-config drop-in, map `OPENAI_API_KEY=$RUNINFRA_GATEWAY_KEY` + `OPENAI_BASE_URL=$RUNINFRA_BASE_URL`.

<Note>
  `RUNINFRA_GATEWAY_KEY` is the managed gateway key (`rp_live_...`) shown in the dashboard reveal panel and snippets. It is not the same variable as `RUNINFRA_API_KEY`, which the self-hosted export kit reserves for its own self-minted secret. Earlier snippets suggested `RUNINFRA_API_KEY` for the gateway key; existing environments keep working, but new setups should use `RUNINFRA_GATEWAY_KEY` so the two secrets never share a name.
</Note>

## Security posture

<Columns cols={2}>
  <Card title="Hashed at rest" icon="shield-check">
    SHA-256 on insert; plaintext never written to disk.
  </Card>

  <Card title="Constant-time compare" icon="shield-check">
    Hash lookup is indexed, not string-compared, timing attacks can't enumerate keys.
  </Card>

  <Card title="SOC2 audit log" icon="file-text">
    Every failed auth, deactivation, rotation, and expiration logs to `audit_logs` (CC6.6).
  </Card>

  <Card title="Per-key rate limit" icon="gauge">
    Upstash sliding-window per `keyId`, separate from network-layer Vercel DDoS protection.
  </Card>
</Columns>
