API keys — API reference
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 |
Base URL https://api.tenergy.me/v1, or https://api-nile.tenergy.me/v1 on Nile (Environments). Every request below is signed as in 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[].compat_dialect | string | null | The compatible-API dialect of this key, null for a native key. | |
data[].daily_limit_sun | integer (int64) | null | The most orders placed with this key may cost per UTC day, in SUN; null = no limit. | |
data[].monthly_limit_sun | integer (int64) | null | The same per UTC calendar month; null = no limit. | |
data[].created_by_agent | object | null | The AI agent (connected-agent grant) that created this key; null for a key a person or another key created. Its spend limits stay within that agent’s unless the account owner changes them in the dashboard. | |
data[].created_by_agent.grant_id | string | null | yes | The grant; null once that grant was deleted. |
data[].created_by_agent.client_name | string | yes | The agent’s name when it created the key. |
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.
Keys created by an AI agent (OAuth access token with api_keys.manage): a key that can
spend (orders.create, subscriptions.write) gets the connected agent’s own daily and monthly
limits when daily_limit_trx / monthly_limit_trx are omitted. A limit above the agent’s, or
null while the agent has one, is refused with 403 3032 agent_key_limit_exceeded
(details.grant_daily_limit_sun, details.grant_monthly_limit_sun). Only the account owner
sets other limits, in the dashboard. Such a key shows created_by_agent.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Step-Up | header | string | A 2-step approval: details.step_up_id of a 1020 step_up_required answer, approved with POST /v1/step-up/{id}/approve. Sent on the repeat of the identical request; used once. Only a dashboard session whose person switched 2-step on is ever asked for it. |
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. | |
compat_dialect | string | Makes this a key for a compatible API: it answers another service’s API under /compat/<dialect>/… with that service’s paths, fields, errors and auth scheme, so an integration written for it moves here by changing the base URL and the key. Plain energy rental only (price, order, status, balance, activation); a 1-hour term. Such a key may hold only orders.create, orders.read, balance.read, balance.topup_address, prices.read, and orders.reclaim for the services with an early return: iTRX, trxx.io, CatFee, Netts, SoHu, Tron Energy Market; a Netts key may also hold webhooks.read / webhooks.write and then sees and changes only the endpoints it registered. Values: the dialect ids listed in the compatibility docs. | |
daily_limit_trx | number | null | The most orders placed with this key may cost per UTC day, in TRX. Past it an order is refused with 3026 api_key_spend_limit_reached until 00:00 UTC. Absent or null = no daily limit. Compatible-API calls count too. For a spending key created by an AI agent: absent = the agent’s own daily limit, and null or more than the agent’s limit is refused with 3032 agent_key_limit_exceeded. | |
monthly_limit_trx | number | null | The same per UTC calendar month. Absent or null = no monthly limit (agent keys as above). |
Example request body, from the contract (illustrative values):
{
"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 | ||
compat_dialect | string | null | The compatible-API dialect of this key, null for a native key. | |
daily_limit_sun | integer (int64) | null | The most orders placed with this key may cost per UTC day, in SUN; null = no limit. | |
monthly_limit_sun | integer (int64) | null | The same per UTC calendar month; null = no limit. | |
created_by_agent | object | null | The AI agent (connected-agent grant) that created this key; null for a key a person or another key created. Its spend limits stay within that agent’s unless the account owner changes them in the dashboard. | |
created_by_agent.grant_id | string | null | yes | The grant; null once that grant was deleted. |
created_by_agent.client_name | string | yes | The agent’s name when it created the key. |
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.
An AI agent may only lower a key’s spend limits or set them within its own limits; it cannot
remove one while it has a limit itself, nor add a spending scope to a key whose limits exceed
its own. Otherwise 403 3032 agent_key_limit_exceeded.
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 | ||
daily_limit_trx | number | null | A new daily spend limit in TRX; null removes it. | |
monthly_limit_trx | number | null | A new monthly spend limit in TRX; null removes it. |
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 | ||
compat_dialect | string | null | The compatible-API dialect of this key, null for a native key. | |
daily_limit_sun | integer (int64) | null | The most orders placed with this key may cost per UTC day, in SUN; null = no limit. | |
monthly_limit_sun | integer (int64) | null | The same per UTC calendar month; null = no limit. | |
created_by_agent | object | null | The AI agent (connected-agent grant) that created this key; null for a key a person or another key created. Its spend limits stay within that agent’s unless the account owner changes them in the dashboard. | |
created_by_agent.grant_id | string | null | yes | The grant; null once that grant was deleted. |
created_by_agent.client_name | string | yes | The agent’s name when it created the key. |
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. |