Conventions
Amounts, chains, paging and errors. The handful of rules that hold across every endpoint.
Amounts are integers, always
Every monetary value in this API, request or response, is a whole number of the chain's smallest unit, carried as a JSON string.
| Chain | Unit | 1 native token |
|---|---|---|
solana | lamport | 1000000000 |
ethereum, base, bsc, robinhood | wei | 1000000000000000000 |
They are strings because they do not fit in a JavaScript number, and integers because the alternative is worse.
Decimals are rejected, not scaled. Sending "0.2" where the API wants
lamports gets you a 400, not two hundred million lamports. No parser in this
API will quietly turn a decimal into base units, because that is exactly where
an off-by-10⁹ funding mistake comes from. 0.2 SOL is "200000000".
For anything user-facing you will want USD. GET /prices returns the current
USD price of each chain's native token, and you do the conversion on your side.
Authorization
bearerAuth JWT from /auth/login (users) or an org API key (mk_...) for integrations.
In: header
Response Body
application/json
curl -X GET "https://example.com/prices"{ "base": 0, "bsc": 0, "ethereum": 0, "robinhood": 0, "solana": 0}Chains
solana, ethereum, base, bsc, robinhood. Solana runs on SVM semantics,
the other four on EVM. Where the behaviour differs, which is mostly around
funding, the page in question says so.
A chain identifier is always lowercase. An unknown one comes back as 400 bad_request with the offending value quoted.
Timestamps
RFC 3339, UTC, as strings: "2026-02-14T09:31:07Z". The one exception is
gap_with on the catalog, which takes Unix epoch seconds because it is a
comma-separated list and readability lost that argument.
Pagination
Two schemes, and which one you get depends on how the underlying list behaves.
Cursor paging on /catalog and /wallets, where rows move under you. Ask
for a page, get back items and a next_cursor, then pass that cursor to get
the page strictly after it. A null cursor means you have reached the end. The
cursor is opaque, so do not build one yourself.
GET /catalog?chain=solana&count=50
GET /catalog?chain=solana&count=50&cursor=<next_cursor from the previous page>Offset paging on /quotes, /purchases, /quotes/abandoned and
/funding/plans, which are your own records and hold still. limit and
offset, both clamped server-side.
Errors
Every failure uses the same envelope:
{
"error": {
"code": "conflict",
"message": "insufficient inventory: requested 50, available 31"
}
}| Status | code | Typically means |
|---|---|---|
| 400 | bad_request | a malformed value: a decimal amount, a bad UUID, an unknown chain |
| 401 | unauthorized | missing, expired, or wrong-scope token |
| 403 | forbidden | authenticated, but not allowed. Org admin required, or org suspended |
| 404 | not_found | no such record for your organisation |
| 409 | conflict | the state moved. Quote already settled, inventory gone, balance too low |
| 422 | unprocessable | a payment problem |
| 429 | rate_limited | over your organisation's requests-per-minute |
| 500 | internal | our fault, and the message is deliberately generic |
404 deserves a note. Scoping happens before existence, so a quote belonging to
another organisation is reported as not found rather than forbidden. Telling you
it exists would already be telling you something.
The 2FA variant
Some sensitive actions answer 401 with an extra X-2FA-Required response
header and a richer body:
{
"error": {
"code": "two_fa_required",
"message": "two-factor code required",
"challenge_id": "…",
"method": "email",
"label": "…",
"sent_to": "o…r@example.com"
}
}It shares a status code with "your session is gone", which is why the header is there. Check it before you tear down the session. On the organisation surface this only shows up for accounts that have enrolled a second factor.
Idempotency and retries
There is no Idempotency-Key header. Where repeating a call would be dangerous,
the endpoint is idempotent by construction instead:
GET /quote/{id}/keysreturns the same keys however many times you ask.POST /funding/plans/{plan_id}/startreports the current state rather than starting a second time.- Settlement is driven by the on-chain balance, so paying and then calling
POST /quote/{id}/settletwice settles once.
Where it would not be dangerous but also not free, as with POST /quote and
POST /funding/plans, a retry creates a second record. Reconcile with GET /quotes or GET /funding/plans rather than blindly re-sending.
Last updated on