InterposeInvest

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 →

The routes you will reach for first.

Every path below was checked against the running services. Paths are the public ones — the internal service path differs, and pasting an internal path produces a URL that 404s.

This is a working subset, not the whole surface

Portfolio Management and BaaS together expose around 800 operations. Listed here are the ones a wealth or investing integration reaches for first. Where you need something that is not on this page, it may well exist — ask us rather than assuming it does not.

Post-Trade and Retirement Plan Administration are deliberately out of scope for this section. The full four-pillar reference is at platform.interposehq.com/docs.

Two sibling pages carry the full generated reference, one per pillar — BaaS and Portfolio Management — every operation the services declare, in full. This page is the curated list: a much smaller set, each route confirmed by hand against the running services and annotated with the behaviour that is easy to get wrong. Start here, then go there when you need something that is not on it.

Brokerage-as-a-Service

Accounts, orders, positions, the transaction ledger and market data.

GET/api/v1/baas/accounts
GET/api/v1/baas/accounts/{account_id}
GET/api/v1/baas/accounts/{account_id}/positions
GET/api/v1/baas/accounts/{account_id}/tax-lots
GET/api/v1/baas/transactions
POST/api/v1/baas/orders
GET/api/v1/baas/orders
GET/api/v1/baas/orders/{order_id}
GET/api/v1/baas/market/quotes/{symbol}

Three paging shapes on this pillar alone

This is the single most common source of quiet data loss when integrating, so it is worth stating per endpoint rather than as a general rule:

EndpointParametersReturns
/accountspage, page_sizeThe {items, total, page, page_size} envelope
/transactionslimit, pageAn envelope whose page_size key echoes your limit. Passing page_size does nothing.
/orderslimit onlyA bare JSON array. No total, no paging, no offset.
/accounts/{id}/positionsA bare JSON array

Two traps in that table

/orders takes limit (default 50, maximum 500) and nothing else — there is no offset, so there is no way to reach the 501st order through this endpoint. Narrow with account_id or status rather than paging.

/transactions pages on limit plus page and echoes your limit back in a key named page_size — so a request using the house ?page=2&page_size=100 style silently returns 50 rows per page and the response looks like it honoured a parameter it ignored.

See pagination for the defensive accessor that handles all of these.

Portfolio Management

Portfolios, models, drift, rebalancing, performance, custodian overlay and billing.

GET/api/v1/pm/portfolios
GET/api/v1/pm/portfolios/{portfolio_id}
GET/api/v1/pm/portfolios/{portfolio_id}/drift
GET/api/v1/pm/models
POST/api/v1/pm/rebalancing
GET/api/v1/pm/rebalancing/{run_id}
POST/api/v1/pm/rebalancing/{run_id}/approve
GET/api/v1/pm/accounts/{account_id}/performance
GET/api/v1/pm/accounts/{account_id}/performance/timeseries
POST/api/v1/pm/custodians/connections
POST/api/v1/pm/custodians/connections/{connection_id}/sync
POST/api/v1/pm/billing/runs
GET/api/v1/pm/disclosures/effective

Running a rebalance

This is the flagship capability of the pillar and it is asynchronous, so the three rebalancing routes above are not enough to build against. POST /api/v1/pm/rebalancing takes a JSON body requiring at least one of portfolio_ids or account_ids portfolio_ids expands to every account currently assigned to that portfolio. The account, not the portfolio, is what actually gets rebalanced.

trigger — returns 202, not 200
curl -X POST https://interposehq.com/api/v1/pm/rebalancing \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
        "account_ids":     ["acct_01HV4Y..."],
        "drift_threshold": "0.0500",
        "rebalance_style": "TO_TARGET",
        "trade_scope":     "BREACHED_ONLY",
        "tax_strategy":    "NONE"
      }'

The response comes back immediately with status: "PENDING". Order generation happens in a background task so a large portfolio_ids list does not hold the request open for minutes, which means you must poll:

state machine
PENDING ──► ANALYZING ──► ORDERS_GENERATED ──approve──► (orders released)
                     └──► FAILED

Poll GET /api/v1/pm/rebalancing/{run_id} until it leaves ANALYZING. Nothing is sent to execution until you call POST /api/v1/pm/rebalancing/{run_id}/approve — a run that reaches ORDERS_GENERATED and is never approved simply sits there, which is the intended review step rather than a failure.

One exception to the async flow

A request carrying target_overrides — a single-security trade-to-target rather than a bulk drift rebalance — runs synchronously and returns the completed run. That is deliberate: backgrounding it would turn its validation errors into an opaque FAILED run instead of the 422 or 404 you can act on.

Performance is account-level

There is no bare /api/v1/pm/performance route — it returns 404. Performance is computed per account, and the portfolio-level path exists only as an aggregate view. Returns come back as decimal fractions, so "0.1630319325" is 16.30%.

Authentication and tenancy

POST/api/v1/auth/sandbox/signup
POST/api/v1/auth/sandbox/verify
GET/api/v1/auth/sandbox/me
POST/api/v1/auth/sandbox/reset
POST/api/v1/auth/login
GET/api/v1/auth/me
POST/api/v1/baas/api-keys/self
GET/api/v1/baas/api-keys/self
GET/api/v1/baas/api-keys/self/permission-groups
DELETE/api/v1/baas/api-keys/self/{key_id}
POST/api/v1/pm/api-keys/self
POST/api/v1/baas/oauth/token
POST/api/v1/pm/oauth/token

The /api-keys/self family is self-serve: the portal login a signup gives you can mint its own machine credential, and the key_secret comes back exactly once. The key is always scoped to the firm and rep codes of the login that created it — sending firm_id or allowed_rep_codes in the body is a 422 rather than a silent ignore, so a key can never be broader than its issuer. Ten active keys per firm; full_access stays operator-only. Exchange the pair for a bearer token at /oauth/token, or sign requests with it — both are on Authentication.

Event streams

Two real-time transports are reachable from your infrastructure. Server-Sent Events is ordinary HTTP and needs no handshake beyond your bearer token; WebSocket needs a ticket and gives you a per-account subscription in return. Both are scoped to your firm. Read Events & streaming for the CloudEvents envelope and the topic list before building on either.

Server-Sent Events

Three further streams are registered beyond the four below — funding/status, journals/status and system, the last of which has no emitter and so connects and stays empty. Naming an account_id you are not entitled to is a 403.

GET/api/v1/baas/events/trades
GET/api/v1/baas/events/positions
GET/api/v1/baas/events/activities
GET/api/v1/baas/events/account/status

WebSocket

Two steps, because a browser WebSocket constructor takes a URL and a subprotocol list and nothing else — there is no header to put a token in. Exchange your ordinary credential for a ticket over REST, then open the socket with it.

POST/api/v1/baas/stream/tickets
WS/api/v1/baas/stream
POST/api/v1/pm/stream/tickets
WS/api/v1/pm/pm/stream
mint, connect, subscribe
curl -X POST https://interposehq.com/api/v1/baas/stream/tickets \
  -H "Authorization: Bearer $TOKEN"

  {"ticket": "…", "token_type": "ws_ticket", "expires_in": 60}

wss://interposehq.com/api/v1/baas/stream?ticket=<ticket>
wss://interposehq.com/api/v1/pm/pm/stream?ticket=<ticket>

send  {"action": "subscribe", "account_id": "acct_01HV4Y…"}
recv  {"type": "subscribed",  "account_id": "acct_01HV4Y…"}

A ticket is single-use and lives 60 seconds. The reason it exists rather than ?token=<jwt> is that uvicorn logs handshake query strings and a portal JWT is valid for eight hours, so a logged token is a session handover; a logged ticket is already spent. An HMAC-signed request mints one just as well as a bearer token, which covers the machine case a subprotocol alone cannot. If you want one round trip instead of two, offer the subprotocols interpose.v1 and interpose.token.<ticket> instead of the query parameter.

Send the subscribe frame within ten seconds of connecting. Every subscribe is authorized against the same claims REST would have used, so you can only subscribe to accounts your credential can already read over REST.

The PM path contains pm twice, and that is correct

PM's route declares /v1/pm/stream on the service, and the proxy rewrites /api/v1/pm/(.*) to /v1/$1 — so both segments survive into /api/v1/pm/pm/stream. BaaS declares /v1/stream and therefore has no repeat. Deleting the second pm because it looks like a typo is the most likely way to fail this handshake.

A refused handshake has no close code

Closing a socket before accepting it produces no close frame — uvicorn turns it into a bare HTTP 403 and discards the code. So a bad or expired ticket looks like a failed HTTP upgrade, not a WebSocket error, and there is no 4401 to check for.

Close codes only carry meaning after a successful accept, which is where the subscribe-time denials live: 4403 for subscribing to a resource you may not see, and 4002 for a malformed or missing subscribe frame.

Deliberately not listed

Some routes exist and are not documented here, because documenting them would imply they are ready to build on:

  • Book migration. Moving an existing book is an assisted process — you send the position and lot export you already download from your custodian, and we run the transition with you. The import endpoints are not listed here because they are not ready to drive unattended: imported positions do not land, and imported sells do not deplete imported lots. What they do and do not do is on Data migration.