---
title: Provisioning Per-User API Keys | Scenario Docs
description: Let your own portal create a Scenario project API key for each user with one organization API key, and revoke it when the user leaves.
---

If your studio runs its own internal portal, access-request tool or RBAC in front of Scenario, you don’t have to create API keys by hand in the web app. One **organization API key**, held by your portal’s backend, can create a **project API key** for each user through the API, and revoke it when that user leaves or changes team.

This guide covers the one-time setup, how to create, hand out, list and revoke per-user keys, and the guardrails Scenario applies to them.

## How it works

1. An organization admin creates one organization API key with the **Manage members, project access and API keys** scope, once, in the web app.
2. Your portal stores that key server-side and uses it to call the API.
3. When a user needs access, your portal creates a project API key for them on the right project, with a role and an optional usage cap.
4. Scenario returns the new key’s secret once. Your portal hands it to the user, or keeps it and calls Scenario on the user’s behalf.
5. When the user leaves, your portal revokes their key.

| Key                  | Held by                    | Can do                                                                                                                 |
| -------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Organization key** | Your portal’s backend only | Create and revoke project API keys, manage project members, set member and project usage caps, across the organization |
| **Project key**      | One user (or your portal)  | Work in its one project, with the role it was given                                                                    |

Per-user keys give you **per-user attribution** (each key’s usage, last use and activity are tracked separately) and **per-key usage caps**. They do not raise rate limits or throughput.

## Before you start: create the organization key

An organization admin does this once:

1. In [app.scenario.com](https://app.scenario.com), open **Settings**, then **API Keys** under **Organization**, and click **Add API Key**.
2. Choose the **Organization** type.
3. Turn on the scope **Manage members, project access and API keys** (`team.members.manage` in the API).
4. Save, and copy the key ID and secret into your secret manager.

With this scope, the key can invite and remove members, set their project roles, create and revoke project API keys, and set member and project usage limits, including unlimited. It cannot make anyone an organization admin or a project owner, and cannot change an admin.

Treat this key like an admin credential. It can create keys and lift usage caps across the whole organization. Keep it in your backend’s secret manager, never in a browser, a client app or a repository, and restrict who can read it.

All requests below go to `https://api.cloud.scenario.com/v1` with [Basic authentication](/get-started/documentation/key-concepts-terminology/api-key-and-authentication/index.md), using the organization key. The examples read it from two environment variables:

Terminal window

```
export ORG_KEY_ID="your-organization-key-id"
export ORG_KEY_SECRET="your-organization-key-secret"
AUTH="Authorization: Basic $(printf '%s:%s' "$ORG_KEY_ID" "$ORG_KEY_SECRET" | base64 | tr -d '\n')"
```

`tr -d '\n'` matters on Linux, where `base64` wraps its output at 76 characters and would split the header.

## Find your project IDs

Project keys belong to one project. List the organization’s projects with [`GET /v1/projects`](/api/resources/projects/methods/list/index.md):

Terminal window

```
curl "https://api.cloud.scenario.com/v1/projects" -H "$AUTH"
```

With the organization key, the response lists every project of the organization:

```
{
  "projects": [
    {
      "id": "proj_k9Xq2mWfPz7LrT4vYbN8cD1e",
      "name": "Starforge Concept Art",
      "teamId": "vQ3mT8pLx2RwN6yHb4kZcA"
    }
  ]
}
```

Keep the `teamId` too: you need it to [list keys](#list-keys-and-reconcile).

## Create a key for a user

Call [`POST /v1/projects/{projectId}/api-keys`](/api/resources/projects/subresources/api_keys/methods/create/index.md) with the organization key:

Terminal window

```
curl -X POST "https://api.cloud.scenario.com/v1/projects/proj_k9Xq2mWfPz7LrT4vYbN8cD1e/api-keys" \
  -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "maya.chen@starforge.games",
    "role": "editor",
    "maxConsumption": 5000
  }'
```

| Field            | Type   | Required | Description                                                                                                                                                                                       |
| ---------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`           | string | No       | A label for the key, up to 256 characters. Use something that identifies the user, such as their email, so you can find the key again.                                                            |
| `role`           | string | No       | The key’s project role: `editor` (the default) or `reader`, or a custom `;`-separated scope list such as `assets.read;workflows.run`. `admin` is refused for keys created by an organization key. |
| `maxConsumption` | number | No       | Usage cap for this key over the billing period, in Creative Units: a value of `0` or more caps it, `-1` leaves it uncapped. Omit it to inherit the organization’s default.                        |

The response contains the new key and its secret:

```
{
  "id": "api_V1stGXR8Z5jdHi6BmyT3aB2c",
  "secret": "s3cr3tExampleValue000000",
  "authorId": "apiu_4E63d6989DcD19bBe05AcBa8",
  "userId": "apiu_0A5C6BfCa3dE4Ca8aD77dAcF",
  "ownerId": "proj_k9Xq2mWfPz7LrT4vYbN8cD1e",
  "ownerType": "project",
  "teamId": "vQ3mT8pLx2RwN6yHb4kZcA",
  "createdAt": "2026-10-13T16:10:06.280Z"
}
```

| Field      | Description                                                                                                             |
| ---------- | ----------------------------------------------------------------------------------------------------------------------- |
| `id`       | The new key’s ID, which the user sends as the Basic auth username. You also need it to revoke the key.                  |
| `secret`   | The new key’s secret, the Basic auth password.                                                                          |
| `userId`   | The new key’s member ID, which the [project member routes](/api/resources/projects/subresources/members/index.md) take. |
| `authorId` | The member ID of the organization key that created it.                                                                  |
| `ownerId`  | The project the key was created on, which the revoke route takes.                                                       |
| `teamId`   | The organization the key belongs to, which the [list route](#list-keys-and-reconcile) takes.                            |

The `secret` is returned **only in this response**. Hand it to the user or store it in your secret manager straight away: it cannot be shown again. If it is lost, revoke the key and create a new one.

Store at least the key `id`, its `ownerId` (project) and its `userId` against the user in your portal, so you can revoke or adjust it later.

The new key only sees its own project: `GET /v1/projects` called with it returns that single project. Keys created through the API also show up in the web app’s **API Keys** page and in the activity trail, like keys created by hand.

### Don’t retry key creation automatically

If creation fails after the key was stored, Scenario revokes the half-created key and answers with the header `x-should-retry: false`. Never retry this call blindly: if a timeout or a dropped connection hides a successful response, a retry creates a second key.

- With your own HTTP client, turn automatic retries off for this request.
- With the SDKs, which retry failed requests by default, pass `maxRetries: 0` (TypeScript) or `max_retries=0` (Python), as shown below.
- When the outcome is unknown, [list the organization’s keys](#list-keys-and-reconcile) and look for the user’s key `name` before creating it again.

### With the SDKs

`projects.apiKeys` (`projects.api_keys` in Python) is available from TypeScript SDK 3.3.0 and Python SDK 2.3.0.

- [TypeScript](#tab-panel-0-0)
- [Python](#tab-panel-0-1)

```
import Scenario from '@scenario-labs/sdk';


const client = new Scenario({
  apiKey: process.env.ORG_KEY_ID,
  apiSecret: process.env.ORG_KEY_SECRET,
});


const key = await client.projects.apiKeys.create(
  'proj_k9Xq2mWfPz7LrT4vYbN8cD1e',
  { name: 'maya.chen@starforge.games', role: 'editor', maxConsumption: 5000 },
  { maxRetries: 0 },
);


// key.id and key.secret are the user's credentials; the secret is never shown again.
```

```
import os
from scenario_sdk import Scenario


client = Scenario(
    api_key=os.environ["ORG_KEY_ID"],
    api_secret=os.environ["ORG_KEY_SECRET"],
)


key = client.with_options(max_retries=0).projects.api_keys.create(
    "proj_k9Xq2mWfPz7LrT4vYbN8cD1e",
    name="maya.chen@starforge.games",
    role="editor",
    max_consumption=5000,
)


# key.id and key.secret are the user's credentials; the secret is never shown again.
```

## Revoke a key

When a user leaves, call [`DELETE /v1/projects/{projectId}/api-keys/{keyId}`](/api/resources/projects/subresources/api_keys/methods/delete/index.md) with the organization key, the project the key was created on (its `ownerId`) and the key’s `id`:

Terminal window

```
curl -X DELETE "https://api.cloud.scenario.com/v1/projects/proj_k9Xq2mWfPz7LrT4vYbN8cD1e/api-keys/api_V1stGXR8Z5jdHi6BmyT3aB2c" \
  -H "$AUTH"
```

The response is `200` with an empty object, `{}`. From then on, any request made with the revoked key is refused with `403`:

```
{ "reason": "Given APIKey does not exists" }
```

With the SDKs: `client.projects.apiKeys.delete(keyId, { projectId })` in TypeScript, `client.projects.api_keys.delete(key_id, project_id=...)` in Python.

An organization key cannot revoke a project admin’s key, a key that holds `team.members.manage`, or another organization key.

## List keys and reconcile

To audit keys, or to check what exists after an uncertain create, call [`GET /v1/teams/{teamId}`](/api/resources/teams/methods/retrieve/index.md). The response’s `team.keys` lists every API key of the organization:

Terminal window

```
curl "https://api.cloud.scenario.com/v1/teams/vQ3mT8pLx2RwN6yHb4kZcA" -H "$AUTH"
```

```
{
  "team": {
    "id": "vQ3mT8pLx2RwN6yHb4kZcA",
    "keys": [
      {
        "id": "apiu_0A5C6BfCa3dE4Ca8aD77dAcF",
        "apiKeyId": "api_V1stGXR8Z5jdHi6BmyT3aB2c",
        "name": "maya.chen@starforge.games",
        "apiKeyStatus": "active",
        "scope": "project",
        "ownerId": "proj_k9Xq2mWfPz7LrT4vYbN8cD1e",
        "authorId": "apiu_4E63d6989DcD19bBe05AcBa8",
        "projects": [
          { "projectId": "proj_k9Xq2mWfPz7LrT4vYbN8cD1e", "role": "editor" }
        ],
        "consumption": 1240,
        "maxConsumption": 5000,
        "lastUsedAt": "2026-10-13T17:02:51.114Z",
        "createdAt": "2026-10-13T16:10:06.280Z",
        "updatedAt": "2026-10-13T16:10:06.280Z"
      }
    ]
  }
}
```

| Field            | Description                                                                                    |
| ---------------- | ---------------------------------------------------------------------------------------------- |
| `id`             | The key’s member ID (`apiu_...`), the `userId` returned at creation.                           |
| `apiKeyId`       | The key’s ID (`api_...`), the one you pass to revoke it.                                       |
| `scope`          | `project` for project keys, `team` for organization keys.                                      |
| `projects`       | For a project key, the projects it has access to and its role on each. Empty once it has none. |
| `ownerId`        | For a project key, the project it was created on.                                              |
| `consumption`    | Creative Units used by the key over the current billing period.                                |
| `maxConsumption` | The key’s effective cap for the period. Absent when the key is uncapped.                       |
| `lastUsedAt`     | When the key last authenticated a request. Absent if it has never been used.                   |

A periodic job that compares this list with your portal’s records catches keys that should no longer exist, such as keys of users who left while your portal was down.

## Other things the organization key can do

The same scope covers a few related operations, documented in the API Reference:

- **Cap a project’s usage:** [`PUT /v1/projects/{projectId}`](/api/resources/projects/methods/update/index.md) with a body of only `{ "maxConsumption": <n> }`. Any other field in the body is refused with `403`.
- **Change a key’s cap later:** [`PUT /v1/teams/{teamId}/members/{memberId}`](/api/resources/teams/subresources/members/methods/update/index.md) with the key’s `userId` as `memberId` and `{ "maxConsumption": <n> }`: `-1` for uncapped, `null` to fall back to the organization’s default.
- **Manage project members:** [add](/api/resources/projects/subresources/members/methods/create/index.md), [update](/api/resources/projects/subresources/members/methods/update/index.md) and [remove](/api/resources/projects/subresources/members/methods/delete/index.md) members of a project, with any role except `admin`. Adding works for people only, not for API keys.
- **Change a key’s role or remove it from its project:** the update and remove routes above, with the key’s `userId` (`apiu_...`) as `memberId`, not its `id`. A key’s role can only be changed on the project it was created on, and removing a key from its last project revokes it.

## Limits and errors

| Situation                                                           | Result                                                                                                                     |
| ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `role` is `admin`                                                   | `403` `{"reason":"Team API keys cannot grant project admin privileges"}`                                                   |
| `role` is not a known role or scope list                            | `400`                                                                                                                      |
| `name` is longer than 256 characters                                | `400`                                                                                                                      |
| The organization reached its plan’s API key limit                   | `429`. The limit depends on the plan. On plans with unlimited keys, an organization key can create up to 1,000.            |
| Revoking a key on a project other than the one it was created on    | `404`                                                                                                                      |
| The call is made with a project key, or without the required scope  | `403`                                                                                                                      |
| Revoking a project admin’s key, or a key with `team.members.manage` | `403`                                                                                                                      |
| A request made with a revoked key                                   | `403` `{"reason":"Given APIKey does not exists"}`                                                                          |
| Creation failed after the key was stored                            | An error with `x-should-retry: false`; the key was revoked. [Don’t retry blindly](#dont-retry-key-creation-automatically). |

For the general shape of error responses, see [Understanding API Responses and Errors](/get-started/documentation/understanding-api-responses-and-errors/index.md).
