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 →

Scoped by rep code, not by a shared account.

Access is computed from the claims in your token on every request — the API does not trust a resource id you pass it. On the REST surface, two firms cannot see each other's data, and that isolation runs through one code path rather than a special case per endpoint.

That includes the streams. Server-Sent Events connections and WebSocket subscriptions are authorized with the same predicate as the REST route for the same account, so a stream carries your book and nobody else's. The handshake mechanics are under Events & streaming.

Interpose accepts three credentials. A portal login is a human signing in for an 8-hour token — what a sandbox signup issues, and it works on every pillar. A machine credential is a key pair you issue yourself from that login and your server exchanges for a one-hour token. Request signing is the option where the secret never crosses the wire. Coverage of the latter two is uneven by pillar, and the table below says which is which.

You are…UseWhy
Evaluating in the sandboxPortal loginIt is what signup issues, and it works on every pillar
A backend calling any pillarOAuth client credentialsStandard and short-lived, and you issue the key yourself
Barred by policy from transmitting a secretHMAC request signingPortfolio Management only

Portal login

POST/api/v1/auth/login
request
curl -X POST https://interposehq.com/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email": "you@yourfirm.com", "password": "..."}'
response
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type":   "bearer",
  "user": {
    "user_id":  "pauth_01J8Z...",
    "email":    "you@yourfirm.com",
    "role":     "advisor",
    "firm_id":  "firm_01J8Z...",
    "rep_code": "SBX7R01",
    "is_active": true
  }
}
PropertyValue
AlgorithmHS256
Lifetime8 hours
Rate limit10 requests/minute per IP across all /api/v1/auth/*
Bad credentials401 — the same body for an unknown email and a wrong password
use it as an ordinary bearer token
curl -H "Authorization: Bearer $TOKEN" \
  https://interposehq.com/api/v1/pm/portfolios

What the token carries

The payload includes role, firm_id, rep_code and allowed_rep_codes. Authorization is computed from those claims on every request — the API does not trust a resource id you pass it.

RoleSees
advisorOnly accounts, portfolios and CRM records under its own rep codes
branch_supervisorIts branch's rep codes
ops_adminPlatform-wide
clientOnly its own accounts
api_clientA machine credential — an API key exchanged for a token, or an HMAC-signed request. It resolves to the firm and rep codes on its own key, so it sees exactly what the login that issued it sees

A sandbox tenant is an ordinary advisor. That is worth understanding if you are evaluating the security posture rather than the features: sandbox traffic is not a separate authorization branch that could quietly fail open, it rides the same rep-code scoping path every advisor login on the platform exercises daily.

API keys — issue your own

A portal login can mint machine credentials for its own firm, so a signup is all you need to get a server talking to the API. Both BaaS and Portfolio Management issue them, on the same four paths.

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}
request
curl -X POST https://interposehq.com/api/v1/baas/api-keys/self \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name": "settlement-worker", "permission_group": "read_only"}'
response — 201
{
  "key_id":            "key_01J8Z...",
  "name":              "settlement-worker",
  "permission_group":  "read_only",
  "allowed_rep_codes": ["SBX7R01"],
  "status":            "active",
  "key_secret":        "ik_live_9f3c..."
}

key_secret comes back exactly once

No endpoint will show you the secret again. If you lose it, revoke the key and issue another.

Portfolio Management issues keys at /api/v1/pm/api-keys/self on the same four paths. Wherever you mint it, one key authenticates on every pillar — brokerage, portfolio management, post-trade, retirement, trust, CRM, agents and network all resolve it from the same store. Issue one, not one per pillar. What it may do on each pillar is decided by its scopes, below.

A key can never be wider than the login that made it

firm_id and allowed_rep_codes are copied from your token. Sending either in the body is a 422 rather than a silent ignore — a caller who believes a scope field was honoured would assume the key is narrower than it is. Only an advisor or branch_supervisor login may issue, so a leaked key cannot be used to manufacture more.

PillarPermission groups you may grant yourself
BaaSread_only · trading · account_management
Portfolio Managementread_only · portfolio_management · advisory

Omitting permission_group gives you read_only. Anything outside the list above returns a 422 naming the valid values; full_access is the operator tier and is not self-grantable. A firm may hold ten active keys at once and the eleventh returns 429, so rotate by issuing the new key before revoking the old one — revocation takes effect immediately. A key belonging to another firm answers 404 on delete, the same as one that does not exist, so this is not a way to probe which key ids are real.

Machine credentials — OAuth 2.0 client credentials

The standard server-to-server flow, RFC 6749 §4.4. Available on BaaS and Portfolio Management.

POST/api/v1/baas/oauth/token
POST/api/v1/pm/oauth/token

The token endpoint is form-encoded, not JSON

This follows RFC 6749, and it is the single most common way to get a 422 here — a JSON body is rejected with Field required on all three fields. In httpx or requests, that means data=, not json=.

request
curl -X POST https://interposehq.com/api/v1/baas/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials' \
  -d 'client_id=key_01J8Z...' \
  -d 'client_secret=ik_live_...'
response
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type":   "Bearer",
  "expires_in":   3600
}

The client_id is the key id (key_…); the client_secret is the secret shown once at issuance (ik_live_…). Swapping them produces a 401 that reads like a credential problem.

HMAC request signing — Portfolio Management only

The high-ceremony option: the secret never crosses the wire. Brokerage-as-a-Service does not accept signed requests on its REST routes — use a bearer token there. Two surfaces are the exception and do take a signed request: the stream-ticket endpoint below, and the Server-Sent Event streams under /v1/events/.

PropertyValue
Canonical stringMETHOD \n PATH \n TIMESTAMP_MS \n SHA256_HEX(body)
Path signedThe service-side path — /v1/..., not the public /api/v1/pm/...
HeadersX-VM-Key-Id · X-VM-Timestamp · X-VM-Signature
Timestamp unitUnix milliseconds
Replay window30 seconds

Two details that will cost you an afternoon

The timestamp is in milliseconds. A signer written against time.time() in seconds fails every request.

You sign the service-side path. The proxy rewrites /api/v1/pm/portfolios to /v1/portfolios before the service sees it, so signing the public path produces a signature over a string the verifier never reconstructs.

Stream tickets

A WebSocket handshake carries no Authorization header, so the socket takes a single-use ticket you mint over REST with a credential that does — a bearer token or an HMAC-signed request.

POST/api/v1/baas/stream/tickets
POST/api/v1/pm/stream/tickets
response
{"ticket": "eyJhbGciOiJIUzI1NiIs...", "token_type": "ws_ticket", "expires_in": 60}

The ticket lives 60 seconds, is spent on first use, and carries your entitlement claims frozen at mint time — so the socket authorizes subscriptions against exactly the identity REST would have used. A portal JWT passed in the socket URL is refused, which is the whole point: a token good for eight hours must not become a stream credential because somebody pasted it into a query string. Connecting and subscribing are covered under Events & streaming.

Current limits

what is not built yet

Portal logins, self-serve API keys and request signing all work today. These are the edges worth knowing before you build on them.

  • Scope names carry their pillar, and older keys keep the short form

    A scope reads baas.orders:write or pm.portfolios:read — the prefix is load-bearing, because one credential now reaches every service and a bare reporting:read would not say which one it meant. Keys issued before this carry the short names and keep working; they are read as belonging to the pillar that minted them. Enforcement is deny-by-default, so a path with no rule is refused rather than allowed, and a few are closed to keys entirely whatever they hold — key issuance itself, the compliance surfaces, the bulk migration endpoints.

  • Signing coverage is uneven, even though the key is not

    The key itself is recognised on every pillar, but which routes accept a signed request rather than a bearer token still varies: only Portfolio Management accepts HMAC on its REST routes, and on BaaS the signature is taken by the stream-ticket endpoint alone. The token exchange works everywhere, so use it unless policy forbids the secret crossing the wire. Only the client_credentials grant is implemented — there is no authorization-code flow and no PKCE support.

Verifying inbound deliveries

Webhooks invert the direction — there, Interpose authenticates to you and you verify our signature. The scheme and its traps are documented under Events & streaming, alongside the delivery contracts that matter when you design around it: sandbox events dispatch immediately in-process; production events deliver through a durable outbox with retries persisted across restarts.