Authentication
The Partner API uses the OAuth 2.0 client credentials grant (RFC 6749 §4.4). You exchange a client ID and secret for a short-lived bearer token, then send that token on every other call.
Getting a token
Credentials go either in the request body or in an HTTP Basic header - both are accepted, and the body may be JSON or form-encoded.
Code
Code
Code
Send it as a bearer token on every subsequent request:
Code
Caching the token
Tokens are valid for one hour by default (expires_in is authoritative - read
it rather than hard-coding the number). Cache the token in your backend and
re-fetch it shortly before it expires; do not call the token endpoint on every
request.
The token response is served Cache-Control: no-store. A cached bearer token in
a shared proxy is a leaked one.
Scopes
A credential is issued with a set of scopes, and each endpoint requires one:
| Scope | Grants |
|---|---|
pools:read | Reading staking pools and their TVL/APY history |
stocks:read | Reading the stock catalogue, prices, quotes and charts |
bridge:read | Reading bridge routes, quotes and transfers. Building and submitting a bridge needs it and transactions:write |
users:read | Reading the users you created |
users:write | Creating, updating and closing users |
portfolio:read | Reading a user's balances, value history and open positions |
transactions:read | Reading a user's indexed transfers |
transactions:write | Building and submitting staking transactions, stock trades and bridges |
rates:read | Reserved for endpoints not yet exposed |
all:read | Every scope above ending in :read, including ones added later |
all:write | Every scope above ending in :write, including ones added later |
Wildcard scopes
all:read and all:write are for integrations that use the whole API and do
not want a credential reissued each time we expose an endpoint. Ask for them
instead of a list, and they cover every current and future permission on their
side.
They are two separate grants: all:read never carries a write permission, so an
integration that only reads stays that way. A credential holding both can reach
every partner endpoint.
Everywhere the API reports what a token can do, it reports the individual
permissions rather than the wildcard - the scope of the token response, and
the scopes that GET /s2s/me echoes back:
Code
That expansion happens when the token is issued, so a token grants what the wildcard covered at that moment. A permission added on our side reaches you with the next token you fetch - no change to your credential, and nothing to redeploy.
Asking for less
Ask for a narrower token by passing a space-delimited scope parameter - useful
when a component of your system only needs to read:
Code
Omitting scope grants everything the credential holds. Asking for a scope the
credential does not have is an invalid_scope error rather than a silent
downgrade.
scope takes the wildcards as well: "scope": "all:read" on a credential
granted both wildcards is how a reporting job gets a token that cannot move
funds, without listing the read permissions one by one. Asking for a wildcard
your credential does not fully hold is invalid_scope - a wildcard is never a
way around what was granted.
Your effective scopes are always the intersection of what the credential holds and what your organisation holds - if a scope is withdrawn from the organisation, tokens stop carrying it everywhere at once. That applies to a wildcard too: it only ever resolves to permissions your organisation still has.
IP allowlist
Credentials can be pinned to a set of IP addresses or CIDR ranges. If yours is, a request from anywhere else is rejected even with a valid token. Tell us the egress addresses of your backend when you ask for a credential, and again if they change.
Revocation and rotation
Revocation is immediate, not "when your token expires" - a revoked credential stops working within seconds, and so does every token already issued to it. The same holds if your organisation's account is suspended.
To rotate a secret without downtime, ask for a rotation with a grace period: the new credential is issued and the old one keeps working for the overlap you request, so you can roll out the new secret and only then let the old one lapse.
The secret is shown once
A client secret is returned exactly once, at the moment the credential is created or rotated. Only a hash is stored, so it cannot be recovered or shown again - if it is lost, the credential has to be rotated.
Errors
| Status | Meaning |
|---|---|
400 invalid_request | Missing or malformed parameters |
400 unsupported_grant_type | grant_type is not client_credentials |
400 invalid_scope | You asked for a scope the credential does not hold |
401 invalid_client | Unknown client ID, wrong secret, or the credential/organisation is not active |
401 on any other endpoint | Missing, malformed or expired access token |
403 on any other endpoint | Valid token, but it lacks the scope that endpoint requires |
The split between 401 and 403 is deliberate: 401 means fix your token,
403 means ask us for that permission. A 401 carries a
WWW-Authenticate: Bearer challenge.
