Skip to content

Authentication

Every call to the Broker Platform API carries an access token, and every access token comes from POST /api/v1/oauth/token.

There are two kinds, for two different jobs.

Token Grant Lifetime Used by
Server token client_credentials 10 minutes Your backend
Delegated token — opaque, prefixed dt_ RFC 8693 token exchange 15 minutes One trader's session

Your credential

An administrator in your firm creates the credential in the Backoffice, under Security → Broker API Credentials. Creating it returns a client_id and a client_secret.

The secret is shown once

The client_secret is displayed exactly once, at creation, and cannot be read back afterwards. Put it in your secret manager at that moment. If it is lost, rotate the secret rather than creating a second credential for the same integration.

Each credential carries, and your administrator can change at any time:

  • Scopes — what it may ever ask for. A token can never exceed them.
  • Brand — optional. A brand-bound credential reaches only that brand's accounts; leave it unset for the whole firm.
  • Allowed addresses — an optional list of addresses and ranges your backend may call from. Empty means any address. It applies when minting a token and on every call made with a server token; it is not applied to a trader's delegated session, which by design comes from wherever the trader is.
  • Allowed origins — intended for browser use, but inert today. Nothing reads this list, and no browser can call the REST endpoints regardless of what you put in it. Setting it neither enables nor blocks anything. See the note under delegated tokens.
  • Protected fields — which field-level-security-protected fields this credential is allowed to receive unmasked. Empty, the default, means all of them come back masked.
  • Plan tier — the rate limits it runs under. See Rate limits.

The credential is not the ceiling

Scopes are intersected with what your firm currently permits. Narrowing the firm's own permissions immediately narrows every credential under it — the API can never grant more than the Backoffice does.

Minting a server token

Send the credential with HTTP Basic authentication:

curl -sS -X POST https://platform-api.example.com/api/v1/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials' \
  -d 'scope=accounts:read transactions:read'
{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 600,
  "scope": "accounts:read transactions:read"
}

Then send it on every call:

Authorization: Bearer eyJhbGciOi...

scope is optional and must be a subset of what the credential holds; ask for less than everything if the process needs less. Asking for more than the credential holds is invalid_scope. If you cannot use HTTP Basic, send client_id and client_secret as form fields instead — the header wins when both are present.

Caching and refresh

The token lives 600 seconds, and expiry is enforced with no grace period.

  • Cache it. The token endpoint allows 10 requests per minute per client_id, so minting per request will fail.
  • Refresh early — roughly 30 seconds before expiry — so a request already in flight cannot cross the boundary.
  • Mint once across concurrent workers. A cold start that fans out ten parallel requests must not become ten token requests.

The SDKs do all three. If you are hand-rolling, budget for them.

Minting a delegated trader token

A delegated token is scoped to a single customer account. Your backend exchanges its server token for one and hands that to the trader's session, so your credential never reaches a browser.

The exchange authenticates with your server token, not with the client secret:

curl -sS -X POST https://platform-api.example.com/api/v1/oauth/token \
  -H "Authorization: Bearer $SERVER_TOKEN" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
  -d 'subject_token=1000123' \
  -d "subject_token_type=$SUBJECT_TOKEN_TYPE" \
  -d 'read_only=false' \
  -d 'scope=stream:quotes stream:account'
{
  "access_token": "dt_9f2c...",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "stream:quotes stream:account"
}
  • subject_token is the customer account id, as a decimal string. A customer account is the only subject type served today.
  • subject_token_type is a URN identifying that subject type. It is being finalized ahead of general availability and is issued to you with your credential — see the note below.
  • Requires trader:delegate on the server credential.

One value is still settling

The subject_token_type URN is the last piece of the token-exchange contract that is not final. It is deliberately not printed here so that no integration is written against a value that changes before launch. Your integration pack carries the current one, and the changelog will record it the day it is frozen.

Read-only is the default

read_only defaults to true. To mint a token that could ever trade or move funds, your backend must explicitly send read_only=false, which it should do only after it has authenticated the trader itself. A token minted for a read-only login never carries trading or funds capability, whatever you ask for.

What a delegated token can do today

Delegated tokens authorize the WebSocket channels — live prices and that account's live figures and events — and, when minted with read_only=false, trading and funds calls on their own account.

They cannot call the bulk-read endpoints, which require the server-credential scopes (accounts:read, positions:read, transactions:read). Run those reads from your backend.

A delegated token is limited to the account it was minted for, and — when the exchange named a specific trading account — to that trading account. Anything else answers 404, not 403, so the token cannot be used to discover which ids exist.

Browser calls are not available

Calling the REST endpoints from a browser does not work, and the Allowed origins setting on your credential does not change that: nothing reads it, so no web origin is ever permitted and every cross-origin request is blocked by the browser itself.

The WebSocket is unaffected — browsers do not apply that check to it — so a browser session can stream with a delegated token today. Route every REST call, read or write, through your own backend.

Failures are deliberately uninformative

A subject that does not exist, belongs to another firm, or belongs to a different brand all answer the same invalid_grant. That is intentional: a distinguishable error would let anyone map your customer ids by trial. Do not build logic on the difference — there isn't one.

Delegated tokens live 900 seconds and do not slide. A busy trader's token still expires on schedule, so re-exchange rather than trying to extend.

Scopes

The scope vocabulary is frozen: these fourteen strings are the wire contract, and none of them will change meaning inside v1. Request only what a given process needs.

Scope Grants Available
accounts:read Read accounts and trading accounts, including live status Yes
positions:read Read positions Yes
transactions:read Read monetary transactions Yes
stream:quotes Subscribe to the price stream Yes
stream:account Subscribe to live account figures and the event feed Yes
webhooks:manage Create, inspect, rotate and disable webhook subscriptions Yes
trader:delegate Exchange a server token for a delegated trader token Yes
accounts:write Create and update accounts, singly and in bulk Yes
trades:write Open and close positions Yes
transactions:write Record transactions, and approve, reject or cancel them Yes
trader:trade Same as trades:write — see the note below Yes
trader:funds Same as transactions:write — see the note below Yes
transactions:amend Nothing on its own. It extends the two funds scopes so they may also cancel a transaction that has already settled Only as an extension
trader:read Reserved for reading on behalf of one trader Not yet served

trader:read is real and grantable, but no endpoint accepts it today. Granting it changes nothing; it will start working when the matching endpoints ship, and that will be a changelog entry.

The trader: prefix is not a boundary

trader:trade and trader:funds limit a delegated token to its own account. Held by a server credential, they do not limit anything: that credential can trade and move money on every account it can reach, exactly as trades:write and transactions:write do. The account limit comes from the delegated token, not from the scope name. Grant trader:trade and trader:funds to a server credential only if you mean to grant full trading and funds capability.

Holding a scope is necessary but never sufficient. Every call is also authorized against the specific record, so a firm-wide credential still cannot reach another firm's account, and a brand-bound one cannot reach another brand's.

Write scopes are checked once more, per call, against what your firm currently permits. Narrowing your firm's own permissions makes a granted write scope inert immediately, and the API answers exactly as it would if the scope had never been granted.

Rotation

Rotate on a schedule, and immediately after any suspected exposure. Your administrator has three separate controls, and they do different things:

Control Effect
Rotate secret Issues a new client_secret. The old secret stops working immediately, so no further tokens can be minted with it. Server tokens already minted keep working until they expire.
Revoke all tokens Leaves the secret alone and invalidates every outstanding server token at once.
Revoke credential Retires the credential. It keeps its history for audit and can never authenticate again.

Rotating the secret does not recall tokens already issued

Rotating closes the door on minting; it does not reach back for the tokens already through it. Those remain valid for up to their 10-minute lifetime. When you are responding to a leak, do both: rotate the secret and revoke all tokens.

The unhurried path is to create a second credential, deploy it, confirm traffic has moved (each credential records when it was last used), then revoke the first.

Errors from the token endpoint

The token endpoint answers in OAuth 2.0's own error shape, not problem+json.

Status Body Meaning
401 {"error":"invalid_client"} Unknown, wrong, revoked, or called from a disallowed address. One uniform answer for all four.
400 {"error":"invalid_request"} The form is malformed, or a parameter was sent twice.
400 {"error":"unsupported_grant_type"} That grant is not available here.
400 {"error":"invalid_scope"} You asked for more than the credential holds.
400 {"error":"invalid_grant"} The token-exchange subject was rejected.
429 {"error":"rate_limited"} More than 10 token requests in a minute. Honor Retry-After.

Keeping tokens safe

  • Never put a token in a URL, a log line, or browser storage you do not control. The WebSocket takes its token as a subprotocol rather than a query parameter for exactly this reason.
  • A leaked server token is usable for at most 10 minutes. Rotate the secret to close the window immediately.
  • A leaked delegated token affects one account for at most 15 minutes. In early access there is no way to recall one before it expires, so treat the 15-minute lifetime as the exposure window and keep delegated tokens out of anything that persists them.
  • If a client_secret leaks, treat every token minted from it as compromised: rotate the secret, then review the credential's recorded last-used time.