Errors
Two shapes
The token endpoint answers in the OAuth shape, so stock OAuth client libraries can parse it:
Code
Every other endpoint uses the API's error envelope:
Code
code is a stable, upper-snake-case identifier for the failure. Branch on it,
not on message, whose wording can change. Every code is listed in
Error codes. message is always a string. When request
validation fails, code is VALIDATION_FAILED, message joins every failure
with ; , and errors lists them one per entry:
Code
Do not write one parser for both. If your HTTP client is configured to expect
{ data } everywhere, the token call is the one that will surprise it.
Insufficient balance
Every construct call (stake, unstake, buy, sell, bridge) checks the wallet's
on-chain balances before it builds anything, with the platform fee counted. A
wallet that cannot afford the batch gets a 400 with the code
INSUFFICIENT_BALANCE and the numbers behind it, so you can render it without
parsing message:
Code
required, available and shortfall are in the token's base units. token
is the token that is short - the one being spent, or the fee token (USDC) when
only the fee is missing. To build for the largest amount that will pass, send
maxAmount: true to the construct call instead of computing it yourself: the
fee tier steps down as the amount grows, and the max accounts for that. With
maxAmount, this error means the wallet cannot afford even the smallest
amount.
Status codes
| Status | Meaning | What to do |
|---|---|---|
400 | Malformed request (VALIDATION_FAILED), unknown pool, unlinked wallet (WALLET_NOT_LINKED), a spent/expired intent (INTENT_NOT_FOUND), or a wallet that cannot afford the batch (INSUFFICIENT_BALANCE) | Fix the request; for intents, rebuild rather than retry; for a balance, construct with maxAmount: true |
401 | Missing, malformed, expired or revoked access token | Fetch a new token. If a fresh token also fails, the credential or the organisation has been stopped |
403 | Valid token, insufficient scope | Ask us to add the scope - retrying will not help |
404 | No such resource, or it belongs to another partner | Not proof the ID is wrong; see below |
409 | Email or wallet address already belongs to another user | The person likely already has a Restake account |
401 versus 403
The split is meaningful and worth branching on:
401means fix your token. It carries aWWW-Authenticate: Bearerchallenge. It also nameserror="invalid_token"only when a token was actually presented and rejected - a request with noAuthorizationheader at all is challenged without that code, because an absent token is not an invalid one.403means the token is fine but is not allowed to do this. No amount of re-authenticating changes it; the credential needs the scope added.
404 on a user
A user ID that belongs to another partner is indistinguishable from one that
does not exist. That is deliberate: a 403 here would let anyone with a partner
token probe whether a given account is on the platform.
So 404 on a userId means one of three things - it never existed, it was
created by a different partner, or it belongs to an app signup - and the API
will not tell you which.
Revocation is immediate
A revoked credential does not keep working until its tokens expire. Both the credential and every token issued to it stop within seconds, and the same happens if your organisation's account is suspended.
Practically, that means a sudden wall of 401s across every endpoint at once is
not a token-refresh bug - check whether the credential is still live before
rewriting your auth code.
Retries
| Situation | Retry? |
|---|---|
401 after a token refresh | No - the credential is stopped |
403 | No |
404, 409, 400 | No - the request has to change |
5xx or a timeout | Yes, with exponential backoff |
Take particular care retrying POST …/transaction/batch/send on a timeout: the
operation may have been submitted. Re-submitting the same intentId is safely
rejected as spent, so the risk is misreading that rejection as a failed stake -
confirm on-chain with the userOpHash before rebuilding.
