# HTTP — `/v1/apiKeys`

API keys authenticate `POST /v1/exchange`, the write WebSocket, and the
market-data WebSocket. Key management uses a Para wallet session, not an API
key.

```http
Authorization: Bearer <para-jwt>
X-Tessera-Wallet: 0x... # required when the JWT has multiple EVM wallets
```

Keep the raw API key in a server-side secret store. Browser clients must never
receive it.

## Create a key

```http
POST /v1/apiKeys
Content-Type: application/json
```

```json
{ "keyPrefix": "desk", "expiresAtMs": 1787776000000 }
```

Both fields are optional. Send `{}` to use the defaults.

| Field | Default | Rule |
| --- | --- | --- |
| `keyPrefix` | `tessera` | Must match `[A-Za-z0-9][A-Za-z0-9_-]{0,31}`. |
| `expiresAtMs` | 30 days after the server time | Unix milliseconds. Must be later than the server time and at most 365 days after it. |

The server accepts an `expiresAtMs` up to 5 minutes past the 365-day limit. A
client that computes the expiry from a clock running slightly ahead still gets
its 365-day key. The response returns the requested `expiresAtMs` to the
millisecond.

The response returns the raw `apiKey` once. Creating a key revokes the previous
active key. To extend a key, create a new one.

Each wallet can create 10 keys per hour. Revoking a key does not count against
this budget.

```jsonc
{
  "status": "ok",
  "data": {
    "id": "8f20b2e6-8b7a-4a0b-bf3d-6fb70c622f76",
    "keyPrefix": "desk",
    "createdAtMs": 1780000000000,
    "expiresAtMs": 1787776000000,
    "revokedAtMs": null,
    "status": "active",
    "apiKey": "desk_..."
  },
  "server_time_ms": 1780000000100
}
```

## List keys

```http
GET /v1/apiKeys
```

Returns the 20 most recent keys of the wallet, newest first, without `apiKey`.
At most one key is `active`. `data` is an empty array when the wallet has never
created a key.

```jsonc
{
  "status": "ok",
  "data": [
    { "id": "8f20b2e6-8b7a-4a0b-bf3d-6fb70c622f76", "keyPrefix": "desk", "createdAtMs": 1780000000000, "expiresAtMs": 1787776000000, "revokedAtMs": null, "status": "active" },
    { "id": "2b1d6c0e-54a3-4f6e-9a55-0c8a3f1e7d21", "keyPrefix": "tessera", "createdAtMs": 1779000000000, "expiresAtMs": 1781592000000, "revokedAtMs": 1780000000000, "status": "revoked" },
    { "id": "d3a9e7f4-1c62-4b8e-8f0d-5e4b2a9c6f10", "keyPrefix": "tessera", "createdAtMs": 1776000000000, "expiresAtMs": 1776604800000, "revokedAtMs": 1779000000000, "status": "expired" }
  ],
  "server_time_ms": 1780000000100
}
```

`status` reports the first event that ended the key.

| `status` | Meaning |
| --- | --- |
| `active` | The key is not revoked and `expiresAtMs` is in the future. It authenticates. |
| `revoked` | A rotation or `DELETE` revoked the key before it expired. |
| `expired` | The key reached `expiresAtMs` before any revocation. |

A rotation also sets `revokedAtMs` on a key that had already expired. That key
stays `expired`, like the last row above.

## Revoke a key

```http
DELETE /v1/apiKeys/{keyId}
```

`keyId` is the `id` of the key, from the list or create response.

```jsonc
{ "status": "ok", "data": { "revoked": true }, "server_time_ms": 1780000000100 }
```

`revoked` is `true` if `keyId` was the wallet's unrevoked key and is now
revoked. An unrevoked key that already expired counts too, and its `status`
stays `expired`. In every other case `revoked` is `false` and nothing changes:

* The key was already revoked, or a rotation replaced it.
* The key belongs to another wallet.
* No key has this id.

A page that still shows an old key therefore cannot revoke the key that
replaced it.

`DELETE /v1/apiKeys` without a key id was removed and returns `405`.

## Errors

| HTTP | Code | Meaning |
| --- | --- | --- |
| `401` | `unauthorized` | Missing bearer, or the token is not a valid Para JWT. |
| `403` | `not_whitelisted` | The wallet is not allowlisted. |
| `400` | `wallet_required` | The JWT holds several EVM wallets and `X-Tessera-Wallet` is missing. |
| `403` | `wallet_required` | The JWT holds no EVM wallet. |
| `403` | `wallet_not_connected` | `X-Tessera-Wallet` names a wallet the JWT does not hold. |
| `400` | `bad_request` | `X-Tessera-Wallet` is not an EVM address, `keyPrefix` does not match the pattern, or `expiresAtMs` is not in the future or is more than 365 days away. |
| `400` | `invalid_path` | `keyId` is not a UUID. |
| `422` | `invalid_request` | A body field has the wrong type, for example a fractional `expiresAtMs`. |
| `429` | `rate_limited` | The wallet exceeded its key creation budget, or the client IP exceeded the HTTP budget. |

A rejected create leaves the active key in place.

Revocation, rotation and expiry close the open exchange and market-data
WebSockets of the invalidated key. An operation admitted before that close may
still complete.
