Skip to content

Credentials & tokens

Every integration call is made with a credential — { apiKey, apiSecret } — that you exchange for a short-lived bearer token. The credential also carries the scopes and tier that decide what you can do.

Two ways:

  • Self-serve signup — POST /api/public/tenants/signup with your email, slug and displayName (all three are required), then POST /api/public/tenants/verify with the emailed token. Verify provisions your tenant, an admin invite, and (where self-serve issuance is enabled) an operator credential carrying firm:operate + prop:manage.
  • Admin console — an existing tenant issues a credential from the API Clients screen and picks its scopes.

The secret is shown once. Store it like a password.

Once you hold an operator credential with tenant:config, you mint the rest yourself — no console:

Terminal window
curl -s https://api.troncharts.xyz/api/v1/api-clients \
-H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{"name":"Reporting bot","tier":"readonly","propManage":true}'
# → 201 { "apiClient": { "id": "…", "apiKey": "pk_…", … },
# "apiSecret": "sk_…" } ← shown once

You never send a tenant. It comes off your own credential, so the new one can only land in your tenant.

Two limits are deliberate:

  • It mints operators only. A trader credential for a paper account is issued by the workspace operator in the admin console; for a live account it is minted where the account’s wallet can sign for it, which is the browser — see Trader vs operator. This costs you less than it sounds: an operator credential with a fullTrading tier already drives the whole paper journey, because a paper account never reaches a venue. Only real venues need the trader credential.
  • You cannot grant what you do not hold. Asking for a scope your own credential lacks is 403 scope_escalation, and a higher tier than your own is 403 tier_escalation. Nothing is minted either way: a credential can only mint an equal or narrower one.
  • It never takes over a trader’s account. Omit accountId and a fresh service account is created with the credential. An accountId that has a human owner is 403 account_has_owner — that account trades with its owner’s signature.

GET /api/v1/api-clients lists your tenant’s credentials (never the secret), and POST /api/v1/api-clients/{id}/revoke retires one. Revoking the credential you are calling with is refused — 409 cannot_revoke_self — so you cannot lock yourself out mid-request.

Terminal window
curl -s https://api.troncharts.xyz/api/auth/api-token \
-H 'content-type: application/json' \
-d '{"apiKey":"…","apiSecret":"…"}'
{
"token": "eyJ…", "access_token": "eyJ…",
"token_type": "Bearer", "expires_in": 86400,
"expiresAt": 1787688000000,
"tier": "fullTrading",
"accountId": "…",
"refresh_token": "rt_…",
"scope": "mcp:fullTrading",
"enabledTools": null
}

The JWT lasts 24 hours. token and access_token are the same string, as are expiresAt (epoch ms) and expires_in (seconds) — the endpoint answers in both the legacy shape and the standard OAuth 2.0 one so an MCP connector can parse it natively.

Two fields read like more than they are. scope is not your credential’s scopes: it is always mcp:<tier>, and says nothing about firm:operate, prop:manage or your audience. enabledTools is the MCP tool allowlist — null means “everything this tier permits”.

The endpoint accepts three request shapes, all returning the body above:

Shape Send
Legacy JSON content-type: application/json, { apiKey, apiSecret }
OAuth form grant_type=client_credentials&client_id=<apiKey>&client_secret=<apiSecret>
OAuth form + HTTP Basic grant_type=client_credentials, credentials in Authorization: Basic base64(apiKey:apiSecret)

Send Authorization: Bearer <token> on every /api/v1/* call. That is the whole requirement — your credential already names its tenant, so there is no tenant header to send and calling from your own backend works. See Tenancy.

The scheme is case-insensitive (Bearer, bearer), the token is not.

Revoke a single token before it expires with POST /api/auth/api-token/revoke, passing { token }.

The SDK client is constructed with a token, so mint it first and then build the client:

const sdk = new TronCharts({
baseUrl: 'https://api.troncharts.xyz',
token,
tenantSlug: 'your-slug',
})

Every mint also returns a 30-day rotating refresh token (rt_…). Use it to get the next 24h access token without putting your apiSecret on the runtime path:

Terminal window
curl -s https://api.troncharts.xyz/api/auth/api-token \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=rt_…'

The response is the same body as a mint — including a new refresh_token. That is the part to get right: refresh tokens are single-use. Each exchange invalidates the one you presented and hands you its successor, so store the new one every time. Keep using the old one and you do not get an error the first time — you get one on the second, and it is not a small one.

Refresh state lives in Redis rather than in your credential row, so treat the refresh token as a convenience and never as your only way back in. Keep the credential: grant_type=client_credentials is always the fallback, and it is the only path after a family is burned.

Revoking the credential also stops refreshing — the refresh grant re-resolves the credential on every exchange, and a revoked one answers invalid_grant.

The SDK does not expose the refresh grant yet. auth.apiToken({ apiKey, apiSecret }) mints; to refresh, call the endpoint directly and construct a new client with the token.

Two authentication paths reach the same API:

Path Who uses it How
Cookie session Browser front ends SIWE or social sign-in yields a tron_session cookie; same-origin requests authenticate automatically.
Bearer JWT Integrators, algos, servers Minted from a credential as above.

Use bearer for anything server-side. A cookie session is scoped to a browser and a single origin; it is not a machine credential.

The WebSocket channels do not accept the bearer token in the Authenticate frame. Call POST /api/auth/bootstrap — authenticating either with { apiKey, apiSecret } in the body or with a bearer token and no body — and it returns a short-lived, single-use handshake token per channel. That handshake token is what the socket wants. See Connecting.

Unlike the bearer mint, /api/auth/bootstrap is tenant-gated, so send X-Tenant-Slug on it too.

Rotate by issuing a second credential, deploying it, then revoking the first.

Revoking a credential takes effect immediately for minting new tokens, for order placement, modification and cancellation by tokens already outstanding, and for the firm:operate / prop:manage / tenant:config scope gates — though those three are cached briefly, so allow a few minutes.