# OAuth — API reference

Source: https://tenergy.me/docs/api/oauth
Last updated: 2026-10-09

The person's side of AI-agent connections (OAuth 2.1). An agent starts at
`GET /oauth/authorize` on this host (metadata: `/.well-known/oauth-authorization-server`);
the dashboard's consent page reads the request, the person chooses permissions, spend
limits and expiry, and connected agents are listed, limited or revoked here.

| Method | Path | Summary |
|---|---|---|
| `GET` | `/v1/oauth/requests/{requestId}` | An agent's authorization request, for the consent page |
| `POST` | `/v1/oauth/requests/{requestId}/decision` | Approve or deny an agent's authorization request |
| `GET` | `/v1/oauth/grants` | Connected agents |
| `PATCH` | `/v1/oauth/grants/{grantId}` | Change a connected agent's limits or expiry |
| `DELETE` | `/v1/oauth/grants/{grantId}` | Disconnect an agent |

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.

## An agent's authorization request, for the consent page

`GET /v1/oauth/requests/{requestId}` · `getOAuthRequest`

**Auth:** Dashboard session cookie + `X-CSRF-Token`.

What an AI agent asked for through `GET /oauth/authorize`: the client (name, host, where
its metadata came from), the requested permissions, the resource the token is for, and the
defaults the page pre-fills. `api_keys.manage` and `webhooks.manage` are opt-in
(`opt_in: true`, unchecked). Live for ten minutes and until decided. **Dashboard session only.**

### Parameters

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

### Responses

| Status | Meaning |
|---|---|
| `200` | OK |
| `401` | Missing, malformed or rejected credentials. |
| `404` | `3029 oauth_request_not_found`: unknown, decided, or expired. |

#### Response fields (`OAuthRequest`)

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes |  |
| `expires_at` | string (date-time) | yes |  |
| `client` | object | yes |  |
| `client.client_id` | string | yes | A registered id, or the URL of the client's metadata document. |
| `client.name` | string | yes |  |
| `client.host` | string | yes |  |
| `client.source` | enum: `dcr`, `cimd` | yes |  |
| `client.client_uri` | string \| null | yes |  |
| `client.logo_uri` | string \| null | yes |  |
| `redirect_uri` | string | yes |  |
| `resource` | string | yes | The API or MCP URL the token will be for. |
| `scopes` | array of object | yes |  |
| `scopes[].scope` | enum: `prices.read`, `balance.read`, `balance.topup_address`, `orders.read`, `orders.create`, `orders.reclaim`, `subscriptions.read`, `api_keys.manage`, `webhooks.manage` | yes | A permission an agent may be granted. The first seven are the API key scopes of the same name; `api_keys.manage` (list, create, revoke API keys) and `webhooks.manage` (read and change webhook endpoints) are opt-in. Withdrawals, members and sign-in methods are never grantable. |
| `scopes[].opt_in` | boolean | yes |  |
| `scopes[].checked` | boolean | yes | Pre-checked on the consent page. |
| `defaults` | object | yes |  |
| `defaults.spend_limit_day_sun` | integer | yes |  |
| `defaults.spend_limit_month_sun` | integer | yes |  |
| `defaults.expires_in_days` | integer | yes |  |
| `step_up` | enum: `off`, `email`, `telegram`, `wallet` | yes | The person's 2-step channel. |

## Approve or deny an agent's authorization request

`POST /v1/oauth/requests/{requestId}/decision` · `decideOAuthRequest`

**Auth:** Dashboard session cookie + `X-CSRF-Token`.

`approve: true` creates the grant with the chosen `scopes` (a subset of the requested ones)
and a one-time code, and answers the client URL to send the browser to
(`code`, `state`, `iss`). `approve: false` answers the client URL with
`error=access_denied`. Granting `orders.create` needs both spend limits (omitted = 100 TRX
a day and 1,000 TRX a month; `null` is refused). `expires_in_days` omitted = 30, `null` =
no expiry. With 2-step on, a grant holding `orders.create`, `api_keys.manage` or
`webhooks.manage` answers `1020 step_up_required` first. Ordering and managing keys or
webhooks need the `editor` role. **Dashboard session only.**

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `requestId` | path | string | yes |  |
| `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 (`OAuthDecisionRequest`), required.

| Field | Type | Required | Description |
|---|---|---|---|
| `approve` | boolean | yes |  |
| `scopes` | array of enum: `prices.read`, `balance.read`, `balance.topup_address`, `orders.read`, `orders.create`, `orders.reclaim`, `subscriptions.read`, `api_keys.manage`, `webhooks.manage` |  | Required when approving; a subset of the requested scopes. |
| `spend_limit_day_sun` | integer \| null |  |  |
| `spend_limit_month_sun` | integer \| null |  |  |
| `expires_in_days` | integer \| null |  |  |

### Responses

| Status | Meaning |
|---|---|
| `200` | Decided; send the browser to `redirect_to`. |
| `400` | Malformed request — bad JSON, unknown field, wrong type. |
| `401` | Missing, malformed or rejected credentials. |
| `403` | `1020 step_up_required`, `1021 step_up_invalid`, `1005 insufficient_scope` (role), `1014 csrf_token_invalid`. |
| `404` | `3029 oauth_request_not_found`. |

#### Response fields

| Field | Type | Required | Description |
|---|---|---|---|
| `redirect_to` | string (uri) | yes |  |

## Connected agents

`GET /v1/oauth/grants` · `listOAuthGrants`

**Auth:** Dashboard session cookie + `X-CSRF-Token`.

The account's live agent grants (not revoked, not expired), newest first. **Dashboard session only.**

### Responses

| Status | Meaning |
|---|---|
| `200` | OK |
| `401` | Missing, malformed or rejected credentials. |

#### Response fields

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | array of object (OAuthGrant) | yes |  |
| `data[].id` | string | yes |  |
| `data[].client_id` | string | yes |  |
| `data[].client_name` | string | yes |  |
| `data[].client_host` | string | yes |  |
| `data[].resource` | string | yes |  |
| `data[].scopes` | array of enum: `prices.read`, `balance.read`, `balance.topup_address`, `orders.read`, `orders.create`, `orders.reclaim`, `subscriptions.read`, `api_keys.manage`, `webhooks.manage` | yes |  |
| `data[].spend_limit_day_sun` | integer \| null | yes |  |
| `data[].spend_limit_month_sun` | integer \| null | yes |  |
| `data[].expires_at` | string (date-time) \| null | yes |  |
| `data[].created_at` | string (date-time) | yes |  |
| `data[].last_used_at` | string (date-time) \| null | yes |  |
| `data[].last_ip` | string \| null | yes |  |
| `data[].user_id` | string | yes | The member who connected the agent. |

## Change a connected agent's limits or expiry

`PATCH /v1/oauth/grants/{grantId}` · `updateOAuthGrant`

**Auth:** Dashboard session cookie + `X-CSRF-Token`.

Lowering a limit or shortening the expiry applies at once; raising a limit, extending the
expiry or removing it needs a step-up when 2-step is on. Spend limits exist only on a grant
holding `orders.create` and cannot be removed. The person who connected the agent or the
account owner. **Dashboard session only.**

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `grantId` | path | string | yes |  |
| `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 (`OAuthGrantPatch`), required.

| Field | Type | Required | Description |
|---|---|---|---|
| `spend_limit_day_sun` | integer |  |  |
| `spend_limit_month_sun` | integer |  |  |
| `expires_in_days` | integer \| null |  | From now; `null` = no expiry. |

### Responses

| Status | Meaning |
|---|---|
| `200` | Saved. |
| `400` | Malformed request — bad JSON, unknown field, wrong type. |
| `401` | Missing, malformed or rejected credentials. |
| `403` | `1020 step_up_required`, `1021 step_up_invalid`, `1005 insufficient_scope`, `1014 csrf_token_invalid`. |
| `404` | `3030 oauth_grant_not_found`. |
| `422` | `2002 empty_patch`. |

#### Response fields (`OAuthGrant`)

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes |  |
| `client_id` | string | yes |  |
| `client_name` | string | yes |  |
| `client_host` | string | yes |  |
| `resource` | string | yes |  |
| `scopes` | array of enum: `prices.read`, `balance.read`, `balance.topup_address`, `orders.read`, `orders.create`, `orders.reclaim`, `subscriptions.read`, `api_keys.manage`, `webhooks.manage` | yes |  |
| `spend_limit_day_sun` | integer \| null | yes |  |
| `spend_limit_month_sun` | integer \| null | yes |  |
| `expires_at` | string (date-time) \| null | yes |  |
| `created_at` | string (date-time) | yes |  |
| `last_used_at` | string (date-time) \| null | yes |  |
| `last_ip` | string \| null | yes |  |
| `user_id` | string | yes | The member who connected the agent. |

## Disconnect an agent

`DELETE /v1/oauth/grants/{grantId}` · `revokeOAuthGrant`

**Auth:** Dashboard session cookie + `X-CSRF-Token`.

Revokes the grant and every token it issued, at once. **Dashboard session only.**

### Parameters

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

### Responses

| Status | Meaning |
|---|---|
| `204` | Revoked. |
| `401` | Missing, malformed or rejected credentials. |
| `403` | `1005 insufficient_scope` (not yours and not the owner), `1014 csrf_token_invalid`. |
| `404` | `3030 oauth_grant_not_found`. |
