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

# Authentication - Examino API Keys, Scopes and Rotation

> Authenticate every Examino API request with a Bearer token, understand key scopes, and handle all authentication rejection scenarios.

Every request to the Examino API must carry a valid API key. There are no cookies, no OAuth flows, and no session tokens, just a single `Authorization` header that you include on every call.

## The Authorization Header

Send your API key as a Bearer token:

```http title="Request header" theme={null}
Authorization: Bearer exa_<prefix>_<secret>
```

The key format is always `exa_<prefix>_<secret>`. The prefix is a short lookup identifier; the secret portion is never retrievable after creation.

<Warning>
  Your secret is shown **only once**, immediately after the key is created.
  Copy it into a secrets manager (e.g., AWS Secrets Manager, HashiCorp Vault,
  or a CI/CD environment variable) before closing the creation dialog. If you
  lose it, you cannot recover it, revoke the key and create a new one.
</Warning>

## Creating an API Key

<Steps>
  <Step title="Open Administration → API">
    In the Examino web app, navigate to **Administration → API**. You need
    admin rights on the team to see this section.
  </Step>

  <Step title="Click Create Key">
    Fill in the creation form with the settings below.
  </Step>

  <Step title="Copy the secret immediately">
    The full key (including secret) is displayed once. Store it in your secrets
    manager before clicking away.
  </Step>
</Steps>

### Key Settings

<ParamField body="name" type="string" required>
  An internal label for this key (e.g., `"LMS production"`, `"Nightly import
      job"`). Not sent in requests, purely for your own bookkeeping.
</ParamField>

<ParamField body="team" type="UUID" required>
  The team this key is scoped to. The key can only access resources that belong
  to this team or any team in its subtree.
</ParamField>

<ParamField body="scopes" type="string[]" required>
  One or more permission scopes. See the [Scopes](#scopes) table below.
</ParamField>

<ParamField body="expiration" type="ISO 8601 date">
  Optional expiry date. After this date the key is automatically rejected with
  `401 unauthorized`. Leave blank for a non-expiring key.
</ParamField>

## Machine identity and audit trail

An API key is an independent machine identity. The administrator who creates it is stored only as historical metadata and is shown in the administration dashboard; their user account, memberships, preferences, and lifecycle do not authorize or own subsequent API operations.

Resources and financial movements created through the API are attributed to the exact API key used. Team scope and scopes authorize each request. Deleting or changing the user who originally created a key does not transfer its activity to another user and does not revoke it; revoke the key explicitly when an integration must stop.

In the Examino interface, organization members see API-created exams as **Created by API**. Administrators can inspect the exact key name and public prefix on audit surfaces, including exams, copies, correction credit movements, and transfers. The secret is never stored or displayed.

## Key Rotation

You can have **multiple active keys at the same time**. This is the recommended approach for zero-downtime rotation:

1. Create the new key and deploy it to your production environment.
2. Verify the new key is working correctly.
3. Revoke the old key.

Revocation is **immediate**, the old key stops working the moment you click Revoke.

<Tip>
  Give each key a name that identifies its purpose and generation (e.g.,
  `"LMS import v2"`). It makes auditing and rotation much easier.
</Tip>

## Scopes

Scopes are the permissions attached to an API key. A request that requires a scope the key doesn't have is rejected with `403 forbidden`.

| Scope               | Authorizes                             |
| ------------------- | -------------------------------------- |
| `teams:read`        | List teams in scope                    |
| `teams:write`       | Create teams                           |
| `credits:read`      | View credit balance                    |
| `credits:write`     | Transfer credits                       |
| `exams:read`        | List and read exams                    |
| `exams:write`       | Create and update exams                |
| `copies:read`       | List and read copies                   |
| `copies:write`      | Create files, create and delete copies |
| `corrections:read`  | Read correction results                |
| `corrections:write` | Launch a correction                    |

<Tip>
  Write scopes do **not** automatically include their read counterpart, grant
  both `domain:read` and `domain:write` when you need full access to a domain.
  The reverse is also true: `domain:read` does not grant write access.
</Tip>

Grant only the scopes your integration actually needs. A key used only to fetch
results, for example, only needs `corrections:read` and `copies:read`.

## Team Scope & Visibility

Each API key is bound to exactly one team and covers that team's **entire subtree** (all nested folders and workspaces). Resources outside the scope are completely hidden.

<Note>
  A request for a resource outside your key's scope returns `404 not_found`,
  **not** `403 forbidden`. This is intentional: the API never confirms or
  denies the existence of resources your key cannot see.
</Note>

## Rejection Reference

Use this table to diagnose why a request was rejected:

| Situation                                           | HTTP Status | Error Code         |
| --------------------------------------------------- | ----------- | ------------------ |
| `Authorization` header missing or malformed         | `401`       | `unauthorized`     |
| Key unknown, revoked, or expired                    | `401`       | `unauthorized`     |
| `rest_api` feature not enabled on the team          | `403`       | `feature_disabled` |
| Key is valid but missing the required scope         | `403`       | `forbidden`        |
| Resource exists but is outside the key's team scope | `404`       | `not_found`        |

### Example: missing `Authorization` header

```http title="401 response" theme={null}
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "error": {
    "code": "unauthorized",
    "message": "No authorization credentials were provided."
  },
  "requestId": "a3c1e7f2-0012-4b8d-bc44-9e3f11d20c55"
}
```

### Example: missing scope

```http title="403 response" theme={null}
HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": {
    "code": "forbidden",
    "message": "The API key does not have the required scope."
  },
  "requestId": "b9d4f823-11ac-4e07-a219-0e6c38b1de90"
}
```
