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

# Customer keys

> Mint an API key per end customer (and per customer organization) so spend splits per customer without per-request tagging. Create, list, and revoke them over the API.

A **customer key** is an AI Reserve API key that belongs to your organization but is bound to
one of **your** customers. Every request made with it is billed to you and reported under that
customer — the gateway stamps the customer id on the usage record itself, so the caller never
has to send `x-aireserve-customer` or `user`. Hand one to each customer (or to each integration
you run on a customer's behalf) and *Spend by Customer* splits on its own.

A customer key can also carry the customer's **organization / sub-account**. That makes it a
**sub key**: usage lands under the customer *and* that organization, so one customer who runs
several workspaces, regions, or business units gets a second level of split. Organization ids
mean whatever they mean in your product; AI Reserve treats them as opaque strings.

| | Customer key | Sub key |
| - | - | - |
| Bound to | one customer id | one customer id **and** one organization id |
| Reported as | `cust_8f3a2b` | `cust_8f3a2b › emea` |
| Created with | `POST /v1/customers/{customerId}/keys` | the same, with `org_id` in the body |

> [!NOTE]
>
> Customer keys are the recommended way to attribute spend when the requests are made by the
> customer's own systems. Keep the [per-request tags](/capabilities/customer-attribution) for
> traffic your own backend sends on behalf of many customers through one key. A customer key's
> binding always wins over a per-request tag; the optional `x-aireserve-feature` tag is still
> honored on both.

## Authentication

Call these endpoints with one of your organization's **own, organization-level** API keys —
the kind an enterprise admin creates on the portal's **Keys** page with scope *Organization*.
Other keys are refused with `403`:

| Key used | Result |
| - | - |
| Organization-level key | Allowed |
| Customer key or sub key | `403 customer_key_not_allowed` — a customer's key can never manage customers |
| Team key or personal key | `403 organization_key_required` |

The customer id in the path is URL-encoded, exactly as you want it reported — 1 to 128
printable characters, no control characters. Use the opaque id your own system uses for the
customer (`cust_8f3a2b`, a UUID, a tenant slug), never an email or a name: it appears in
Analytics, exports, and these responses.

Enterprise admins can create the same keys by hand on the Keys page: **Create key → One of
your customers**.

## Create a key

`POST /v1/customers/{customerId}/keys`

| Body field | Type | Notes |
| - | - | - |
| `name` | string, optional | 1–255 characters. Defaults to the customer id, or `<customer> › <org>` for a sub key. Names are unique among your organization's active keys. |
| `org_id` | string, optional | 1–128 printable characters. Makes the key a sub key for this organization of the customer. |

Returns `201` with the key object **plus `key`, the plaintext secret** — it is shown in this
response only. Store it before you respond to whatever asked for it; AI Reserve keeps only a
hash and cannot show it again.

```bash curl theme={"dark"}
# A key for the customer
curl -X POST "https://api.aireserve.com/v1/customers/cust_8f3a2b/keys" \
  -H "Authorization: Bearer $AIRESERVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Acme production"}'

# A sub key for one of the customer's organizations
curl -X POST "https://api.aireserve.com/v1/customers/cust_8f3a2b/keys" \
  -H "Authorization: Bearer $AIRESERVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"org_id": "emea"}'
```

```python Python theme={"dark"}
import os, requests
from urllib.parse import quote

BASE = "https://api.aireserve.com/v1/customers"
headers = {"Authorization": f"Bearer {os.environ['AIRESERVE_API_KEY']}"}

customer_id = "cust_8f3a2b"
created = requests.post(
    f"{BASE}/{quote(customer_id, safe='')}/keys",
    json={"org_id": "emea"},
    headers=headers,
)
created.raise_for_status()
secret = created.json()["key"]  # shown once — store it now
```

```typescript TypeScript theme={"dark"}
const headers = {
  Authorization: `Bearer ${process.env.AIRESERVE_API_KEY}`,
  "Content-Type": "application/json",
};

const customerId = "cust_8f3a2b";
const res = await fetch(
  `https://api.aireserve.com/v1/customers/${encodeURIComponent(customerId)}/keys`,
  { method: "POST", headers, body: JSON.stringify({ org_id: "emea" }) },
);
if (!res.ok) throw new Error(`create failed: ${res.status}`);
const { key: secret } = await res.json(); // shown once — store it now
```

```json theme={"dark"}
{
  "id": "0c0b8b5e-3e6b-4f3f-9a2e-6d8c2f1a4b70",
  "name": "cust_8f3a2b › emea",
  "customer_id": "cust_8f3a2b",
  "org_id": "emea",
  "key_prefix": "aireserve_api_9f1",
  "key_suffix": "c2d7e1",
  "status": "active",
  "created_at": "2026-10-06T09:14:02.000Z",
  "last_used_at": null,
  "revoked_at": null,
  "key": "aireserve_api_9f1…c2d7e1"
}
```

The customer then uses `key` exactly like any other AI Reserve key — same base URL, same
endpoints, same rate limits as your organization. No header or body field is needed; if the
customer's code does send `x-aireserve-customer` or `user`, the key's customer still wins.

## List a customer's keys

`GET /v1/customers/{customerId}/keys`

Returns every key bound to the customer, newest first, revoked keys included. Secrets are
never returned — only the prefix and suffix you need to tell keys apart.

```bash theme={"dark"}
curl "https://api.aireserve.com/v1/customers/cust_8f3a2b/keys" \
  -H "Authorization: Bearer $AIRESERVE_API_KEY"
```

```json theme={"dark"}
{
  "customer_id": "cust_8f3a2b",
  "keys": [
    {
      "id": "0c0b8b5e-3e6b-4f3f-9a2e-6d8c2f1a4b70",
      "name": "cust_8f3a2b › emea",
      "customer_id": "cust_8f3a2b",
      "org_id": "emea",
      "key_prefix": "aireserve_api_9f1",
      "key_suffix": "c2d7e1",
      "status": "active",
      "created_at": "2026-10-06T09:14:02.000Z",
      "last_used_at": "2026-10-06T11:40:19.000Z",
      "revoked_at": null
    },
    {
      "id": "7a4d1c22-5b0e-4a9f-8e31-2f6b9d0c5e18",
      "name": "Acme production",
      "customer_id": "cust_8f3a2b",
      "org_id": null,
      "key_prefix": "aireserve_api_4bd",
      "key_suffix": "a81f03",
      "status": "revoked",
      "created_at": "2026-09-30T16:02:45.000Z",
      "last_used_at": "2026-10-05T22:11:08.000Z",
      "revoked_at": "2026-10-06T08:00:00.000Z"
    }
  ]
}
```

| `status` | Meaning |
| - | - |
| `active` | Serves requests |
| `paused` | Stopped by a spending guardrail or an admin; can be resumed from the portal's Keys page |
| `revoked` | Permanent. Requests with the key are refused with `401` |

## Revoke a key

`DELETE /v1/customers/{customerId}/keys/{keyId}`

Revocation is immediate and permanent: the next request made with the key is refused. The
call is idempotent — revoking an already-revoked key returns the same `200`. A `keyId` that
does not belong to this customer (or to your organization) returns `404`, so a key can never
be revoked through the wrong customer's path.

```bash theme={"dark"}
curl -X DELETE "https://api.aireserve.com/v1/customers/cust_8f3a2b/keys/7a4d1c22-5b0e-4a9f-8e31-2f6b9d0c5e18" \
  -H "Authorization: Bearer $AIRESERVE_API_KEY"
```

```json theme={"dark"}
{
  "id": "7a4d1c22-5b0e-4a9f-8e31-2f6b9d0c5e18",
  "customer_id": "cust_8f3a2b",
  "org_id": null,
  "status": "revoked",
  "revoked_at": "2026-10-06T08:00:00.000Z"
}
```

To rotate, create the new key first, move the customer over, then revoke the old one.

## Errors

All three endpoints share your organization's API rate limit and return the standard
[error shape](/api-reference/errors).

| Status | `code` | When |
| - | - | - |
| `400` | `invalid_customer_id` | The path segment is empty, longer than 128 characters, or contains control characters |
| `400` | `invalid_body`, `invalid_json`, `invalid_org_id` | The body is not a JSON object, has unknown fields, or `name` / `org_id` fail validation |
| `400` | `invalid_key_id` | `keyId` is not a UUID |
| `401` | — | Missing or unknown API key |
| `403` | `customer_key_not_allowed` | The caller is a customer key or sub key |
| `403` | `organization_key_required` | The caller is a team or personal key |
| `404` | `key_not_found` | No such key for this customer in your organization |
| `409` | `name_taken` | An active key of yours already has that `name` |
| `429` | `rate_limit_exceeded` | Your organization's API rate limit; honor `Retry-After` |

Full request and response schemas are under *Customer keys* in the
[API reference](/api-reference).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.