# API keys — API reference

Source: https://tenergy.me/docs/api/api-keys
Last updated: 2026-09-27

Managing the credentials of this account.

| Method | Path | Summary |
|---|---|---|
| `GET` | `/v1/api-keys` | List the keys of this account |
| `POST` | `/v1/api-keys` | Create an API key |
| `PATCH` | `/v1/api-keys/{keyId}` | Edit a key |
| `DELETE` | `/v1/api-keys/{keyId}` | Revoke a key |

Generated from [`openapi.yaml`](https://tenergy.me/openapi.yaml) at build time. Base URL `https://api.tenergy.me/v1`, or `https://api-nile.tenergy.me/v1` on Nile ([Environments](https://tenergy.me/docs/environments)). Every request below is signed as in [Authentication](https://tenergy.me/docs/authentication) unless its **Auth** line says otherwise.

## List the keys of this account

`GET /v1/api-keys` · `listApiKeys`

**Auth:** API key (HMAC).

Secrets are never returned by this endpoint — a secret exists in a response exactly once,
at creation. The `key` shown here is the public identifier sent in `X-API-KEY`.

### Responses

| Status | Meaning |
|---|---|
| `200` | OK |
| `401` | Missing, malformed or rejected credentials. |
| `500` | Something broke on our side. |

#### Response fields

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | array of object (ApiKey) | yes |  |
| `data[].id` | string | yes |  |
| `data[].key` | string | yes | The public identifier sent in `X-API-KEY`. Not secret. |
| `data[].label` | string | yes |  |
| `data[].scopes` | array of enum (13 values, ApiKeyScope) | yes |  |
| `data[].ip_allowlist` | array of string |  |  |
| `data[].is_active` | boolean | yes |  |
| `data[].last_used_at` | string (date-time) \| null |  | When this key last signed a request. Written at most once a minute per key, so it answers "is this key still in use", not "to the second, when". |
| `data[].last_used_ip` | string \| null |  | The source address of that last request. `null` until the key is used. |
| `data[].expires_at` | string (date-time) \| null |  |  |
| `data[].created_at` | string (date-time) | yes |  |
| `limit` | integer | yes | How many **active** keys this account may hold at once. A product limit, not a permission: it caps how many credentials exist, never what any of them may do. Past it, `POST /api-keys` answers `3017 api_key_limit_reached`; revoking a key frees a slot immediately. |
| `used` | integer | yes | Active keys right now — revoked and expired ones do not count. |

## Create an API key

`POST /v1/api-keys` · `createApiKey`

**Auth:** API key (HMAC) or bootstrap token.

Creates a key and returns its **secret once**. The secret is not stored in a recoverable
form; if it is lost, delete the key and create another.

The first key of an account is created with the **bootstrap token** from the signup
flow; every later one with an existing key that itself holds `keys.create`.

**Also on an unfunded account** (since 2026-09-26), so an integration can read
its deposit address with the new key and top up without a human in the loop.

`scopes` is **required and has no default**. The API deliberately does not invent a
starter set of permissions: a key's reach is a decision for the account owner, taken
with the list in front of them. A request without `scopes` is rejected with
`2001 validation_failed`, and no client library, agent flow or dashboard form may
supply one on the owner's behalf.

###### One permission vocabulary

Permissions are named `area.action` and there is **one list of names** for the whole
platform. The names below are the ones an **API key** may carry in v1. The remaining
names in that vocabulary — `balance.withdraw`, `billing.write`, `invoices.read`,
`referral.read`, `referral.withdraw`, `team.read`, `team.invite`, `team.remove`,
`team.grant`, `settings.write`, `audit.read` — exist for use elsewhere in the platform
(team management in the dashboard) but are **not key-eligible in v1**.

| Scope | What it opens |
|---|---|
| `prices.read` | `POST /estimate/transfer` (`GET /prices`, `GET /estimate` and `GET /resources/{address}` are public and need no scope) |
| `balance.read` | `GET /account`, `GET /balance` — balances and lifetime counters |
| `balance.topup_address` | `GET /deposit-addresses` — the account's deposit address and its retired ones |
| `orders.read` | `GET /orders` (also as CSV), `GET /orders/{id}`, `GET /batches…`, `GET /quotes/{id}`, `GET /account/stats` (sums over the same orders) |
| `orders.create` | `POST /orders`, `POST /quotes`, `POST /batches`, `POST /batches/{id}/cancel` — **spends money** |
| `orders.reclaim` | `POST /orders/{id}/reclaim` — ends a rental early, no refund |
| `subscriptions.read` | `GET /subscriptions…` |
| `subscriptions.write` | create, patch, cancel subscriptions — **commits to recurring spend** |
| `webhooks.read` | `GET /webhooks…`, including `GET /webhooks/{id}/deliveries` |
| `webhooks.write` | create, patch, delete, rotate, test webhook endpoints |
| `keys.read` | `GET /api-keys` |
| `keys.create` | `POST /api-keys`, `PATCH /api-keys/{id}` — **a key with this can widen its own account's reach** |
| `keys.revoke` | `DELETE /api-keys/{id}` — can stop a production integration instantly |

`GET /ledger`, `GET /account/addresses` and `GET /session/history` are dashboard-session
operations and no scope opens them to a key.

A key can only be created with scopes that the creating key itself holds, so
`keys.create` cannot be used to escalate beyond what the caller already has. A key
created with the bootstrap token may carry any key-eligible scope, because the signing
address is the account's owner.

### Request body

JSON (`ApiKeyRequest`), required.

| Field | Type | Required | Description |
|---|---|---|---|
| `label` | string |  | Optional. Omitted, the key is named `Key N` at the next free index, so several keys can be created without inventing names for them. A label is a name, not a permission — which is why it may have a default and `scopes` never will. |
| `scopes` | array of enum (13 values, ApiKeyScope) | yes | Required, no default, no server-side suggestion. The names are the key-eligible subset of the one platform permission vocabulary; see the endpoint description for what each opens. |
| `ip_allowlist` | array of string |  | Source addresses allowed to use this key, IPv4/IPv6 addresses or CIDR blocks. An empty or absent list means any IP — acceptable for a read-only key, a bad idea for one that can spend. |
| `expires_at` | string (date-time) \| null |  | Automatic revocation time. `null` for a key that does not expire. |

Example request body, from the contract (illustrative values):

```json
{
  "label": "grafana exporter",
  "scopes": ["balance.read","orders.read"],
  "ip_allowlist": ["203.0.113.10"]
}
```

### Responses

| Status | Meaning |
|---|---|
| `201` | Created. `secret` appears only here. |
| `400` | Malformed request — bad JSON, unknown field, wrong type. |
| `401` | Missing, malformed or rejected credentials. |
| `403` | Authenticated, but this key is not allowed to do it. |
| `409` | `3017 api_key_limit_reached` — the account already holds the maximum number of active keys. `details.limit` and `details.used`; revoke one to free a slot. |
| `422` | Syntactically valid but semantically impossible. |
| `500` | Something broke on our side. |

#### Response fields

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes |  |
| `key` | string | yes | The public identifier sent in `X-API-KEY`. Not secret. |
| `label` | string | yes |  |
| `scopes` | array of enum (13 values, ApiKeyScope) | yes |  |
| `ip_allowlist` | array of string |  |  |
| `is_active` | boolean | yes |  |
| `last_used_at` | string (date-time) \| null |  | When this key last signed a request. Written at most once a minute per key, so it answers "is this key still in use", not "to the second, when". |
| `last_used_ip` | string \| null |  | The source address of that last request. `null` until the key is used. |
| `expires_at` | string (date-time) \| null |  |  |
| `created_at` | string (date-time) | yes |  |
| `secret` | string | yes | HMAC secret, 256 bits, **shown once**. `sk_live_…` on the production host and `sk_test_…` on the Nile host, matching the key id. It is not stored in a recoverable form: if it is lost, revoke the key and create another. |

## Edit a key

`PATCH /v1/api-keys/{keyId}` · `updateApiKey`

**Auth:** API key (HMAC).

Change `label`, `ip_allowlist`, `is_active`, or `scopes`. Narrowing `scopes` takes effect
immediately. Widening them is subject to the same rule as creation: the calling key must
already hold every scope being granted.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `keyId` | path | string | yes |  |

### Request body

JSON (`ApiKeyPatch`), required.

| Field | Type | Required | Description |
|---|---|---|---|
| `label` | string |  |  |
| `scopes` | array of enum (13 values, ApiKeyScope) |  |  |
| `ip_allowlist` | array of string |  |  |
| `is_active` | boolean |  |  |
| `expires_at` | string (date-time) \| null |  |  |

### Responses

| Status | Meaning |
|---|---|
| `200` | Updated |
| `400` | Malformed request — bad JSON, unknown field, wrong type. |
| `401` | Missing, malformed or rejected credentials. |
| `403` | Authenticated, but this key is not allowed to do it. |
| `404` | No such object, or it belongs to another account. The two are not distinguished. |
| `422` | Syntactically valid but semantically impossible. |
| `500` | Something broke on our side. |

#### Response fields (`ApiKey`)

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes |  |
| `key` | string | yes | The public identifier sent in `X-API-KEY`. Not secret. |
| `label` | string | yes |  |
| `scopes` | array of enum (13 values, ApiKeyScope) | yes |  |
| `ip_allowlist` | array of string |  |  |
| `is_active` | boolean | yes |  |
| `last_used_at` | string (date-time) \| null |  | When this key last signed a request. Written at most once a minute per key, so it answers "is this key still in use", not "to the second, when". |
| `last_used_ip` | string \| null |  | The source address of that last request. `null` until the key is used. |
| `expires_at` | string (date-time) \| null |  |  |
| `created_at` | string (date-time) | yes |  |

## Revoke a key

`DELETE /v1/api-keys/{keyId}` · `deleteApiKey`

**Auth:** API key (HMAC).

Immediate and irreversible. In-flight requests signed with this key start failing at once;
orders it already created are unaffected and keep running.

A key cannot delete itself — that would leave an account with no way back in if it were the
last one. Revoking your own key is done from the dashboard.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `keyId` | path | string | yes |  |

### Responses

| Status | Meaning |
|---|---|
| `204` | Revoked. No body. |
| `401` | Missing, malformed or rejected credentials. |
| `403` | Authenticated, but this key is not allowed to do it. |
| `404` | No such object, or it belongs to another account. The two are not distinguished. |
| `500` | Something broke on our side. |
