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.
1. Get a credential
Section titled “1. Get a credential”Two ways:
- Self-serve signup —
POST /api/public/tenants/signupwith youremail,sluganddisplayName(all three are required), thenPOST /api/public/tenants/verifywith the emailed token. Verify provisions your tenant, an admin invite, and (where self-serve issuance is enabled) an operator credential carryingfirm: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.
Every credential after the first
Section titled “Every credential after the first”Once you hold an operator credential with tenant:config, you mint the
rest yourself — no console:
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 onceYou 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
fullTradingtier 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 highertierthan your own is403 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
accountIdand a fresh service account is created with the credential. AnaccountIdthat has a human owner is403 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.
2. Exchange it for a bearer token
Section titled “2. Exchange it for a bearer token”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',})3. Refresh instead of re-minting
Section titled “3. Refresh instead of re-minting”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:
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.
Cookie sessions vs bearer sessions
Section titled “Cookie sessions vs bearer sessions”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.
WebSocket handshake tokens
Section titled “WebSocket handshake tokens”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.
Rotation
Section titled “Rotation”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.