Sandbox environment. Interpose is not a registered broker-dealer and holds no FINRA approval. Every API documented here is available in a simulated evaluation environment only — no client assets are held, no real trades are executed, and no custody is provided. The calculation engines are the production code paths; fills, custody, settlement and KYC decisioning are simulated. What is real and what is simulated →
API conventions
Learn these once instead of per endpoint.
A small number of rules hold everywhere. Every item on this page is something that breaks a naive client, so it is worth reading before you write a model class rather than after.
Money and quantities are strings
Never parse a monetary value into a float. Not in your ORM, not in your serialiser, not “just for the chart”. Floating-point arithmetic silently corrupts money, and in a regulated context that is a reportable error rather than a rounding curiosity.
| Data | JSON | Precision | Example |
|---|---|---|---|
| Fiat monetary amount | string | 4 dp | "152.3400" |
| Equity / fund quantity | string | 8 dp | "10.00000000" |
| FX rate | string | 6 dp | "1.265432" |
| Performance return | string | 10 dp | "0.1630319325" |
| Percentage / basis points | string | decimal form | "0.0400" = 4% |
Performance returns are decimal fractions, not percentages
"0.1630319325" means 16.30%, not 0.16%. Multiply by 100 before display. Getting this backwards understates every return by a factor of 100 — it is a mistake we have made and fixed in our own client code.
The API does not normalise what you send
A quantity posted as "1.5" is stored as "1.5" and returned as "1.5" on every subsequent read — not "1.50000000". The precisions above describe values the platform computes; values you supply come back exactly as written. Trailing zeros are your responsibility. Compare as decimals, never as strings.
Currency is explicit — under two different field names
Currency is never implied, but the field name depends on the object:
| Object | Field | Meaning |
|---|---|---|
| Orders, transactions, quotes, fees | currency | The currency of this amount |
| Accounts, portfolios | base_currency | The currency the book is denominated and valued in |
Reading currency off an account returns nothing. ISO 4217 three-letter codes throughout; USD, GBP and EUR are supported.
Typed security identifiers — on the trading surface
On orders, positions, tax lots and settlements there is no bare symbol field. A security is an object:
This applies to the trading surface only
Market data, research, screener, watchlist and price-alert endpoints take and return a plain ticker string, because there is no lot or position to identify — the symbol is the whole reference. GET /api/v1/baas/market/quotes/AAPL is correct as written.
The rule to carry: anything that touches a position or a tax lot takes the typed object; anything that only looks up a price does not.
Identifiers
Every entity id is a prefixed ULID — a type prefix, an underscore, and 26 Crockford base-32 characters. ULIDs rather than UUIDs because they sort lexicographically by creation time and are URL-safe with no escaping.
| Prefix | Entity | Prefix | Entity |
|---|---|---|---|
| acct_ | Brokerage account | ptf_ | Portfolio |
| ord_ | Order | reb_ | Rebalancing run |
| pos_ | Position | lot_ | Tax lot |
| txn_ | Transaction | evt_ | Event |
| pauth_ | Portal user (a login) | sbx_ | Sandbox signup |
Treat ids as opaque strings
Not every *_id field is a prefixed ULID — firm_id among them. Some are firm-supplied or legacy codes. A client that validates every *_id against a ULID regex will reject live, valid responses. Do not parse them, do not assume a length, and do not derive one id from another.
Timestamps
Always UTC. Fractional precision and suffix are not uniform across services, so parse with a conformant ISO 8601 parser rather than pattern-matching:
| Service | Shape |
|---|---|
| BaaS, PM | 2026-05-11T14:23:00.000000000Z — 9 digits, Z |
| Agents, CRM | 2026-05-11T14:23:00.000000Z — 6 digits, Z |
| Auth | 2026-05-11T14:23:00.000000+00:00 — 6 digits, offset |
All three are valid ISO 8601 UTC and any conformant parser handles them. A regex expecting Z or exactly nine digits will not. For ordering, sort by ULID rather than by timestamp — it is monotonic by construction and immune to precision differences.
Pagination — four shapes
The envelope is the minority shape — check before you loop
Four shapes exist across the platform, and the envelope is not the common one. A loop written against body["items"] does not fail loudly on the other three — it raises on a bare array, or silently yields nothing where the collection sits under a different key. Detect the shape rather than assuming it.
| Shape | Where |
|---|---|
| {items, total, page, page_size} | Top-level collections — /accounts, /portfolios |
| A bare JSON array | The majority — most sub-resources, e.g. /accounts/{id}/positions |
| {items, total} | Assorted reports and registries, with no paging keys |
| {clients, total} | A named collection key — a handful of listings |
page is 1-indexed. Parameter names are not uniform either — most endpoints take page/page_size, some take limit/offset, and a few accept no paging parameters while still returning a page_size key. A defensive accessor costs one line:
Errors — detail has three shapes
Errors are FastAPI-standard, which means detail is a string, a list, or an object depending on why the request failed. Type-check it before indexing.
| Status | Meaning | Retry? |
|---|---|---|
| 400 | Malformed request | No — fix the request |
| 401 | Not authenticated | No — get a token |
| 403 | Authenticated, outside your scope | No |
| 404 | Not found, or not visible to you | No |
| 409 | Conflict with current state | No — re-read state first |
| 422 | Schema failure or a business-rule rejection | No — inspect detail |
| 429 | Application-level cap exceeded | Yes, with backoff |
| 503 | Rate limited at the edge, or a dependency is down | Yes, with backoff |
Rate limits
| Path | Sustained | Burst |
|---|---|---|
| /api/v1/{pillar}/* | 600 / minute | 100 |
| /api/v1/agents/* | 600 / minute | 40 |
| /api/v1/auth/* — including sandbox signup | 10 / minute | 5 |
| This site's signup form | 5 / minute | 3 |
Exceeding a limit returns 503, not 429 — and there is no Retry-After
The edge proxy uses its default status. A client that backs off only on 429 will hot-loop against a 503. Use exponential backoff with jitter, and treat 503 as potentially rate-limit-related.
The 429 you will see is application-level — for example a sandbox tenant exceeding its 100-account cap.
Idempotency
Idempotency support is per-endpoint, not platform-wide. There is no global Idempotency-Key header. Where it exists it is an explicit field in the request body. The safe general pattern: capture the ids the API returns, and have your retry logic check for existence before re-posting.
Versioning
Every path is under /v1. There is no /v2 and no deprecation in flight. Additive changes — new response fields, new optional request fields — ship inside v1 without notice, so your client must ignore unknown fields rather than failing to deserialise them. Breaking changes get a new version prefix.
Positions are derived, not stored
One architectural fact that changes how to read the API: positions and balances are folded from an immutable transaction ledger rather than held as mutable rows. The ledger is append-only — corrections are new entries, never edits, enforced at the database level rather than by convention. A position that looks wrong is a symptom; the ledger is the source of truth, and it is queryable.