# API keys API reference

> HTML version: https://storeshot.art/docs/api/api-keys/

Create, list and revoke API keys. Contract 1.18.0; base URL `https://api.storeshot.art/v1`. Schemas: https://storeshot.art/docs/api/schemas.md

## GET /api-keys

**List API keys** (operation `listApiKeys`). Auth: API key with scope `keys:manage`, or access token.

Requires `keys:manage`. Non-revoked keys, newest first (max 100). Secrets are never returned. An API key caller sees only keys of its own environment.

**Credits.** Free: no credits.

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | Keys. | `ApiKeyList` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

Example 200 response:

```json
{
  "data": [
    {
      "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
      "name": "string",
      "prefix": "ss_live_AbCd",
      "environment": "live",
      "scopes": [],
      "status": "active",
      "createdAt": "2026-10-21T12:00:00.000Z",
      "lastUsedAt": "2026-10-21T12:00:00.000Z",
      "expiresAt": "2026-10-21T12:00:00.000Z"
    }
  ]
}
```

## POST /api-keys

**Create an API key** (operation `createApiKey`). Auth: API key with scope `keys:manage`, or access token.

Requires `keys:manage`. The full key is in this response only. An API key acting as the manager can only create keys for its own environment with a subset of its own scopes.

**Credits.** Free: no credits.

**Request body** (`application/json`, required, schema `CreateApiKeyRequest`):

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | yes | 1–64 characters after trimming; need not be unique. |
| `environment` | `ApiKeyEnvironment` | yes |  |
| `scopes` | `array of ApiKeyScope` | no | Defaults to every scope except `keys:manage`, `billing:read`, `billing:write`, `webhooks:read` and `webhooks:write`. |
| `expiresAt` | `string` | no | Optional RFC 3339 expiry, in the future and at most 365 days ahead. Format: date-time |

Example request body:

```json
{
  "name": "string",
  "environment": "live",
  "scopes": [
    "generations:write"
  ],
  "expiresAt": "2026-10-29T12:00:00.000Z"
}
```

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `201` | Created. Store `key` now; it is never shown again. | `CreatedApiKey` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `413` | `PAYLOAD_TOO_LARGE`. | `ErrorEnvelope` | `X-Request-Id` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

Example 201 response:

```json
{
  "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
  "name": "string",
  "prefix": "ss_live_AbCd",
  "environment": "live",
  "scopes": [
    "generations:write"
  ],
  "status": "active",
  "createdAt": "2026-10-21T12:00:00.000Z",
  "lastUsedAt": "2026-10-21T12:00:00.000Z",
  "expiresAt": "2026-10-21T12:00:00.000Z",
  "key": "string"
}
```

## DELETE /api-keys/{id}

**Revoke an API key** (operation `revokeApiKey`). Auth: API key with scope `keys:manage`, or access token.

Requires `keys:manage`. Idempotent: revoking an already-revoked key returns 204. Takes effect on the next request.

**Credits.** Free: no credits.

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | `string` | yes | Format: uuid |

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `204` | Revoked. | — | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `404` | `NOT_FOUND`: no such resource for this caller. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

## POST /auth/device

**Start device authorization** (operation `createDeviceAuthorization`). No authentication.

No authentication. For an agent or tool without an API key (RFC 8628): returns a `user_code` and a console link for its human, and a secret `device_code` to poll `POST /auth/device/token` with every `interval` seconds. The human signs in to the developer console, checks the request and approves or denies it; on approval the next poll returns a new API key. Codes expire after `expires_in` seconds (10 minutes) and work once. Accepts JSON or `application/x-www-form-urlencoded`. Device authorization may be disabled on a server: then this answers 404 `NOT_FOUND`. Limited per IP address (429 `RATE_LIMITED` with `Retry-After`).

**When to use.** When `STORESHOT_API_KEY` is not set: start here, show your human `verification_uri_complete` and `user_code`, then poll `POST /auth/device/token`.

**Credits.** Free: no credits.

**Request body** (`application/json`, optional, schema `DeviceAuthorizationRequest`):

| Field | Type | Required | Description |
|---|---|---|---|
| `client_name` | `string` | no | Your tool's name, shown to the human as an unverified label. 1–64 printable ASCII characters. When absent, `client_id` is used instead (generic OAuth libraries send one). |
| `scope` | `string` | no | Space-separated scopes for the new key. Default `generations:write generations:read uploads:write usage:read`. `keys:manage`, `billing:write` and `webhooks:write` cannot be requested. The human may narrow the list, or add `billing:read` and `webhooks:read`. |
| `environment` | `ApiKeyEnvironment` | no | Environment of the new key. Default `live`. |

Example request body:

```json
{
  "client_name": "Claude Code",
  "scope": "generations:write generations:read uploads:write usage:read",
  "environment": "live"
}
```

**Request body** (`application/x-www-form-urlencoded`, optional, schema `DeviceAuthorizationRequest`):

| Field | Type | Required | Description |
|---|---|---|---|
| `client_name` | `string` | no | Your tool's name, shown to the human as an unverified label. 1–64 printable ASCII characters. When absent, `client_id` is used instead (generic OAuth libraries send one). |
| `scope` | `string` | no | Space-separated scopes for the new key. Default `generations:write generations:read uploads:write usage:read`. `keys:manage`, `billing:write` and `webhooks:write` cannot be requested. The human may narrow the list, or add `billing:read` and `webhooks:read`. |
| `environment` | `ApiKeyEnvironment` | no | Environment of the new key. Default `live`. |

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | Started. Show `verification_uri_complete` and `user_code` to the human. | `DeviceAuthorization` | `X-Request-Id`, `Cache-Control` |
| `400` | `INVALID_INPUT`: see `details.issues`. | `ErrorEnvelope` | `X-Request-Id` |
| `404` | `NOT_FOUND`: device authorization may be disabled on this server, and then both operations answer the `/v1` 404 envelope. | `ErrorEnvelope` | `X-Request-Id` |
| `429` | `RATE_LIMITED`: too many requests from this IP address, or too many pending ones (`details.reason` `too_many_pending`). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id` |

Example 200 response:

```json
{
  "device_code": "string",
  "user_code": "BCDF-GHJK",
  "verification_uri": "https://console.storeshot.art/device",
  "verification_uri_complete": "https://console.storeshot.art/device?code=BCDF-GHJK",
  "expires_in": 600,
  "interval": 5
}
```

## POST /auth/device/token

**Poll for the device authorization result** (operation `createDeviceToken`). No authentication.

No authentication. Send `grant_type=urn:ietf:params:oauth:grant-type:device_code` and the `device_code` from `POST /auth/device`, as `application/x-www-form-urlencoded` (RFC 8628 §3.4) or JSON. Poll every `interval` seconds until the human decides. Approved: 200 with the new API key in `access_token`, returned once; the code then answers `invalid_grant`. **Errors here are the one deliberate exception to `ErrorEnvelope`:** refusals are HTTP 400 `{ "error", "error_description" }` (RFC 6749 §5.2) with `error` `authorization_pending` (keep polling), `slow_down` (add 5 seconds to your interval; `Retry-After` gives the new one), `access_denied`, `expired_token` (start again), `invalid_grant`, `invalid_request` or `unsupported_grant_type`. The `X-Request-Id` header is on every response: quote it to support. Other failures (404 while device authorization is disabled on this server, 413, 5xx) use `ErrorEnvelope`.

**When to use.** Every `interval` seconds after `POST /auth/device`, until it returns the key; add 5 s on `slow_down`; stop on `access_denied` or `expired_token`.

**Credits.** Free: no credits.

**Request body** (`application/x-www-form-urlencoded`, required, schema `DeviceTokenRequest`):

| Field | Type | Required | Description |
|---|---|---|---|
| `grant_type` | `string` | yes | One of: "urn:ietf:params:oauth:grant-type:device_code" |
| `device_code` | `string` | yes |  |

**Request body** (`application/json`, required, schema `DeviceTokenRequest`):

| Field | Type | Required | Description |
|---|---|---|---|
| `grant_type` | `string` | yes | One of: "urn:ietf:params:oauth:grant-type:device_code" |
| `device_code` | `string` | yes |  |

Example request body:

```json
{
  "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
  "device_code": "string"
}
```

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | Approved: the new API key. Store `access_token` now; it is never shown again. | `DeviceToken` | `X-Request-Id`, `Cache-Control`, `Pragma` |
| `400` | OAuth error (RFC 6749 §5.2), not `ErrorEnvelope`. `slow_down` also carries `Retry-After`. | `DeviceTokenError` | `X-Request-Id`, `Retry-After` |
| `404` | `NOT_FOUND`: device authorization may be disabled on this server, and then both operations answer the `/v1` 404 envelope. | `ErrorEnvelope` | `X-Request-Id` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id` |

Example 200 response:

```json
{
  "access_token": "string",
  "token_type": "Bearer",
  "scope": "string",
  "key_id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
  "environment": "live"
}
```
