# tronbase API

Buy TRON Energy and Bandwidth from your account balance, and hear about it over webhooks. One base URL: `https://tronbase.io/api/v1`. JSON, UTF-8.

Keys are made by the account holder in the dashboard, under **API Keys**, and start with `tbc_`.

This document is also public at [`https://tronbase.io/api-docs.md`](https://tronbase.io/api-docs.md) — no key, no login. Point an agent or a colleague at that URL before they have credentials.

The machine-readable contract is at [`https://tronbase.io/openapi.json`](https://tronbase.io/openapi.json) — OpenAPI 3.0, also public, and generated from the same source as this page so the two cannot disagree.

There is also an **MCP server** for AI agents, in `mcp/` in the repository: the same endpoints as tools, held to the same scopes and limits. It runs over stdio with a key from the environment, or over HTTP with the key on each request.

## Authentication

```
Authorization: Bearer tbc_xxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

- The full key is shown **once** when it's created. Store it in a secret manager.
- A revoked key, or a key whose owner is blocked, returns `401`.
- Rate limit, per key, answered with `429` and a `Retry-After`: **60 requests per minute**.

A key spends the balance of the account that made it. Treat it like a password: anyone holding it can place orders.

### Scopes

Each key is made with one of three scopes, and each contains the one before it:

| Scope | What it opens |
|---|---|
| `read` | Every `GET` below |
| `purchase` | Also `POST /orders` |
| `full` | Also the automations — standing orders that spend the balance unattended |

A key used beyond its scope gets `403 insufficient_scope`, with `details.needed` and `details.held`.

### Daily spending limit

A key can be given a limit on what it may spend between one midnight UTC and the next. An order that would go over it is refused with `403 key_limit_reached` **before** anything is charged, and `details.remaining` says what is left in TRX. Refunds put money back into the day's allowance, so an order the provider refused does not count against it.

Both the scope and the limit are chosen when the key is created, in the dashboard.

## Endpoints

| Method | Path | Purpose |
|---|---|---|
| GET | `/account` | Who owns the key, the balance in SUN, the deposit address, the webhooks |
| GET | `/prices` | Current prices, order sizes and the activation fee |
| GET | `/address/{address}` | What an address already holds on-chain, and whether activation is due |
| POST | `/orders` | Buy energy or bandwidth for an address |
| GET | `/orders` | Your recent orders. Query: `limit` (1–100, default 20) |
| GET | `/orders/{id}` | One order |
| GET | `/automations` | Your standing orders |
| POST | `/automations` | Create one. Needs a `full` key |
| GET | `/automations/{id}` | One standing order |
| PATCH | `/automations/{id}` | Pause, resume, or change what happens after a top-up. Needs a `full` key |
| DELETE | `/automations/{id}` | Stop it for good. `204`, no body. Needs a `full` key |
| GET | `/docs` | This document as Markdown. Send `If-None-Match` with the last `ETag` to get `304` when nothing changed |

## Buying energy

An order is charged to your balance the moment it is placed, then delegated on-chain. Amounts are always whole **SUN** (1 TRX = 1,000,000 SUN), so nothing is lost to rounding.

```bash
curl -X POST https://tronbase.io/api/v1/orders/ \
  -H "Authorization: Bearer $TRONBASE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"address": "TJ8bLmQ4vXFyRkC2nDp7WsEhG6aUz9YtKn", "resource": "energy", "volume": 65000}'
```

| Field | Type | Rules |
|---|---|---|
| `address` | string | **Required.** The address that *sends* the USDT. Energy is spent by the sender |
| `volume` | integer | **Required.** Units of the resource. 65,000 energy covers one USDT transfer to a recipient who already holds USDT; 131,000 covers one to an address that holds none |
| `resource` | string | `energy` (default) or `bandwidth` |
| `duration` | string | `15m` only, which is also the default. Fifteen minutes is all a transfer needs |

The answer is `201` with the order:

```json
{"data": {"id": 41, "status": "filled", "resource": "energy", "address": "TJ8b…YtKn", "volume": 65000,
 "duration": "15m", "priceSun": 1950000, "feeSun": 200000, "activationSun": 0, "totalSun": 2150000,
 "txId": "4b71e0c8…", "error": null, "createdAt": "…", "updatedAt": "…"}}
```

`status` is `pending` while the money is charged and the order placed, `sent` once the provider has it, `filled` when the delegation is on-chain, and `failed` if it was refused — in which case the money is already back on the balance. `error` carries the provider's reason on a `failed` order and is `null` on every other one. Poll `/orders/{id}` or, better, let a webhook tell you.

`GET /orders` answers with the same objects in an array, newest first, and no `pagination`: it is a recent-activity feed, not a full history.

An address that has never been used on TRON is activated first, and `activationSun` says what that cost. It is charged once per address, ever.

**Energy is rented, not bought.** It returns to its owner when the fifteen minutes are up, so place the order right before the transfer it is for — not in advance.

## Retrying an order safely

A request that never answers leaves you unable to tell a lost request from a lost reply, and retrying is what buys the energy twice. Send an `Idempotency-Key` — any string up to 255 characters, one per order you mean to place:

```bash
curl -X POST https://tronbase.io/api/v1/orders/ \
  -H "Authorization: Bearer $TRONBASE_KEY" \
  -H "Idempotency-Key: 0f3c1b7e-2f4a-4a3f-9a2d-5c6b7e8f9a01" \
  -H "Content-Type: application/json" \
  -d '{"address": "TJ8bLmQ4vXFyRkC2nDp7WsEhG6aUz9YtKn", "volume": 65000}'
```

- The retry returns the **same answer** as the first attempt, with the same order id and the same status code.
- While the first attempt is still running, the retry gets `409 idempotency_in_flight`. Wait, then ask again.
- The same key with a different body gets `422 idempotency_mismatch`.
- A request that never charged anything — a validation error, say — hands the key back, so you can correct the body and reuse it.
- Keys are remembered for 24 hours.

## Automations

A standing order buys energy without you asking each time. Both kinds spend the same balance as an ordinary order, and each firing shows up in `GET /orders` like any other.

| `kind` | What it does | Its own fields |
|---|---|---|
| `refill` | Watches the address and buys `volume` whenever its energy falls below `thresholdEnergy`. Checked every minute | `thresholdEnergy` |
| `renewal` | Buys `volume` once a day at `runAtMinute`, whatever the address holds | `runAtMinute` — minutes past midnight UTC, 0–1439 |

```bash
curl -X POST https://tronbase.io/api/v1/automations/ \
  -H "Authorization: Bearer $TRONBASE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kind": "refill", "address": "TJ8bLmQ4vXFyRkC2nDp7WsEhG6aUz9YtKn", "volume": 65000, "thresholdEnergy": 65000}'
```

```json
{"data": {"id": 7, "kind": "refill", "address": "TJ8b…YtKn", "volume": 65000, "thresholdEnergy": 65000,
 "runAtMinute": null, "status": "active", "pausedBy": null, "autoResume": true,
 "lastFiredAt": null, "silentSince": null, "error": null, "createdAt": "…", "updatedAt": "…"}}
```

`status` is `active` while it is running, `paused` when you paused it, `suspended` when the balance stopped covering a firing, and `canceled` once stopped for good. `pausedBy` says which of you did it — `user` or `system`.

**Out of balance.** A rule that cannot afford its next firing suspends itself rather than failing quietly, and says so over `automation.paused`. With `autoResume` on — the default — it starts again by itself the moment a deposit lands. Turn it off with `PATCH {"autoResume": false}` and it will wait for you instead.

**Running out of money.** `balance.low` is sent once per fall, not once per order: it is armed again only when the balance climbs back over the threshold. Set the threshold in the dashboard, under **Notifications** — there is no endpoint for it, because it is a preference rather than part of an integration.

**A rule that goes quiet.** A daily rule that has not fired for a day and a half is flagged with `silentSince`. Nothing is bought to make up for it: the energy was needed yesterday, and buying it now would pay for a transfer that has already happened. Read the flag as "look at this rule", not as "this is being handled".

Pause and resume:

```bash
curl -X PATCH https://tronbase.io/api/v1/automations/7/ \
  -H "Authorization: Bearer $TRONBASE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "paused"}'
```

Resuming is refused with `422 automation_refused` while the balance cannot cover one firing.

## Webhooks

Register endpoints in the dashboard, under **Webhooks**. Every endpoint receives every event:

| Event | When |
|---|---|
| `order.delivered` | The delegation is on-chain. The energy is on the address now |
| `order.failed` | The order was refused or lost; the balance has been put back |
| `automation.paused` | A standing order stopped. `data.reason` is `insufficient_balance` when we stopped it |
| `automation.resumed` | A standing order is running again |
| `deposit.credited` | Money landed on the balance. `data.deposit` carries the amount in SUN and the transaction hash |
| `balance.low` | The balance fell below the threshold set in the dashboard. `data.balance` carries both figures |

An order ends once, so each endpoint gets exactly one of the two order events per order, however the ending reached us. Each delivery is a `POST` with the subject in the body — `data.order` for an order event, `data.automation` for an automation one, `data.deposit` or `data.balance` for the two money ones:

```json
{"event": "order.delivered", "createdAt": "2026-09-24T12:00:00.000Z", "data": {"order": {"id": 41, "status": "filled", "…": "…"}}}
```

| Header | Meaning |
|---|---|
| `X-Tronbase-Event` | The event name |
| `X-Tronbase-Delivery` | Delivery id. The same id repeats on a retry — deduplicate on it |
| `X-Tronbase-Timestamp` | When this attempt was signed, ISO 8601 |
| `X-Tronbase-Signature` | `HMAC-SHA256(secret, timestamp + "." + body)`, hex |

Verify before trusting the body, in constant time, and reject a timestamp far from now:

```js
const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
```

Answer with any 2xx. Anything else — or a timeout after 10 seconds — is retried after 1, 5 and 30 minutes, then 2 and 6 hours, and given up after the sixth attempt. Endpoints must be public `https`; addresses inside a private network are refused.

## Responses

Success is always `{"data": …}`.

Errors always look like this, with **all** field errors at once:

```json
{"error": {"code": "validation_failed", "message": "Some fields are invalid.", "details": {"address": "Not a TRON address.", "volume": "Must be a whole number."}}}
```

| HTTP | code | When |
|---|---|---|
| 400 | `invalid_json` | Body is not valid JSON |
| 400 | `invalid_query` | Invalid query parameter |
| 401 | `unauthorized` | Missing, invalid or revoked key, or blocked owner |
| 402 | `insufficient_balance` | The balance does not cover the order; `details.needed` is the amount in TRX |
| 403 | `insufficient_scope` | The key's scope does not reach this endpoint; `details.needed` and `details.held` |
| 403 | `key_limit_reached` | The key has spent its allowance for the day; `details.remaining` is what is left, in TRX |
| 404 | `not_found` | No such order or automation, or it belongs to someone else |
| 409 | `order_in_doubt` | The order may or may not have gone through; it is being settled. **Do not retry it** |
| 409 | `idempotency_in_flight` | The first request with this `Idempotency-Key` is still being served |
| 422 | `idempotency_mismatch` | This `Idempotency-Key` was used for a different request body |
| 422 | `automation_refused` | The rule could not be created or changed; the message says why |
| 415 | `unsupported_media_type` | Missing `Content-Type: application/json` |
| 422 | `order_refused` | The provider refused the order; the money is back on the balance |
| 422 | `validation_failed` | Field errors in `details` |
| 429 | `rate_limited` | Too many requests |
| 503 | `provider_unavailable` | Orders are not being taken right now. Retry later |
| 503 | `chain_unavailable` | The address could not be read from the network |

## Examples

Read the latest version of these docs, with a key or without one:

```bash
curl https://tronbase.io/api/v1/docs/ -H "Authorization: Bearer $TRONBASE_KEY"
curl https://tronbase.io/api-docs.md
```

Check a key and its balance:

```bash
curl https://tronbase.io/api/v1/account/ -H "Authorization: Bearer $TRONBASE_KEY"
```

See what an address already has, then order only what it lacks:

```bash
curl https://tronbase.io/api/v1/address/TJ8bLmQ4vXFyRkC2nDp7WsEhG6aUz9YtKn/ -H "Authorization: Bearer $TRONBASE_KEY"
```

Read the current prices:

```bash
curl https://tronbase.io/api/v1/prices/ -H "Authorization: Bearer $TRONBASE_KEY"
```

List your recent orders:

```bash
curl "https://tronbase.io/api/v1/orders/?limit=20" -H "Authorization: Bearer $TRONBASE_KEY"
```

List your standing orders:

```bash
curl https://tronbase.io/api/v1/automations/ -H "Authorization: Bearer $TRONBASE_KEY"
```
