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 →
Events & streaming
React to what happens, instead of polling for it.
Interpose is event-sourced: every fill, position change and rebalance is published as an immutable event, and positions are derived by folding those events. Your systems can subscribe to that stream rather than asking for changes on a timer — which is the difference between noticing a fill at 09:00 tomorrow and noticing it at 14:23:07 today.
Two transports are reachable from your own infrastructure. Server-Sent Events is ordinary HTTP and needs nothing but a bearer token. WebSocket costs one extra call — you mint a single-use ticket first — and gives you a socket a browser can open too. For higher volume, Kafka is available by arrangement. The ledger behind all three is durable and append-only; the stream is a live view of it, not a replay log.
Transport availability
| Transport | Status | Note |
|---|---|---|
| Server-Sent Events | Live | Ordinary HTTP — no upgrade, no ticket. Scoped to your book |
| WebSocket | Live | BaaS and Portfolio Management, via a single-use ticket. Every subscription is authorized |
| Kafka | Self-hosted broker; connection details arranged during onboarding. No self-serve path | |
| Webhooks | Registration only | You get a subscription and a signing secret. Interpose dispatches nothing to it today |
| SQS fan-out | Not available | The AWS dependency is in no service image |
Server-Sent Events
Seven streams are registered under BaaS, all reachable at the normal public base URL. They are ordinary HTTP — no upgrade, no ticket, no special client — which makes them the shortest path to a working integration, and the transport that survives a corporate proxy refusing to upgrade a connection. The four below are the ones to build on:
Three more are registered and will connect. /events/funding/status and /events/journals/status have emitters behind them and carry real traffic; /events/system has no emitter at all, so it connects and stays empty.
The response is text/event-stream; the connection stays open and each event arrives as a data: line carrying the CloudEvents envelope. Use -N with curl, or disable buffering in your client, or you will see nothing until the buffer flushes.
The stream is scoped to your book
Every event is filtered against the same authorization predicate that guards the REST route for the account it belongs to, so a connection carries your firm's events and no one else's. The account_id query parameter narrows it further and now matches — it reads data.account_id off the envelope, which is where the platform actually puts it. Naming an account you are not entitled to returns 403 rather than an empty stream.
Assume at-least-once delivery
Dedupe on the CloudEvents id — a reconnect can replay. An event whose account cannot be resolved is withheld rather than broadcast, which is the second reason /events/system stays empty for you; see Current limits.
WebSocket
The WebSocket streams are reachable at the public base URL. A handshake carries no Authorization header — the browser WebSocket constructor takes a URL and a subprotocol list and nothing else — so you exchange your ordinary credential for a single-use ticket over REST first, then connect with that.
| Pillar | Mint a ticket | Connect |
|---|---|---|
| BaaS | POST /api/v1/baas/stream/tickets | wss://interposehq.com/api/v1/baas/stream |
| Portfolio Management | POST /api/v1/pm/stream/tickets | wss://interposehq.com/api/v1/pm/pm/stream |
The doubled pm is real — copy it exactly
Portfolio Management declares its socket at /v1/pm/stream on the service, and the proxy rewrites /api/v1/pm/(.*) to /v1/$1, so the public URL carries the segment twice. Its ticket endpoint is declared at /v1/stream/tickets and therefore carries it once. Two paths on one pillar, one doubled and one not; both are correct and neither is a typo.
A socket frame is not a CloudEvent
Kafka and Server-Sent Events carry the CloudEvents envelope below. The socket sends its own shape — a type and the fields that go with it, no specversion, id or time. Expect position.updated, account.buying_power.updated, margin.call.issued and the PDT counters on your subscribed account, and note that price_update and fx_rate_update ticks go to every open socket rather than to a subscription — they are market data, not account data. There is no id to dedupe on here, so use SSE or Kafka where you need one.
| Ticket property | Value |
|---|---|
| Lifetime | 60 seconds — mint it immediately before you connect, not at start-up |
| Reuse | Single use. A reconnect needs a fresh ticket |
| Credential to mint it | A bearer token or an HMAC-signed request — whichever you already use for REST |
| Entitlements | Copied from your claims at mint time, so the socket authorizes exactly as REST would |
| A portal JWT in the URL | Refused. The socket accepts tickets and nothing else |
The reason for the extra call rather than ?token=<your portal JWT>: the application server logs the handshake path including its query string, and a portal token is valid for eight hours — so a logged URL would be a session handover. A spent 60-second ticket in the same log line is worth nothing.
One round trip, and from a browser
A browser cannot set a header on a handshake but can offer subprotocols, so the ticket may travel there instead. That form keeps it out of the access log altogether.
Subscribing, and what a refusal looks like
Send {"action": "subscribe", "account_id": "acct_..."} within ten seconds of connecting and the server answers {"type": "subscribed", "account_id": "acct_..."}. Every subscribe frame is authorized against the same predicate as the REST route for that account, so a socket can only ever carry a book your credential could already read. One account per connection — open a second socket for a second account.
| Outcome | What you see |
|---|---|
| Ticket missing, expired, already spent, or an ordinary JWT | HTTP 403 on the upgrade — no close frame, no close code |
| Subscribed to an account you may not see | Close code 4403 |
| Malformed subscribe frame, or none within ten seconds | Close code 4002 |
A rejected handshake has no close code, and there is no 4401
Closing a socket before it is accepted cannot produce a close frame — the server turns a pre-accept close into a bare 403 and discards the code — so a client waiting on a close event to learn its credential was bad waits forever. Handle the failed upgrade. Close codes exist only after a successful accept, which is where 4403 and 4002 live.
The CloudEvents envelope
Server-Sent Events and Kafka both carry CloudEvents 1.0 — the same envelope, byte for byte, whichever you consume. The two senders that use their own shape are the WebSocket frame above and the webhook delivery below, and each says so where it is documented.
| Field | Notes |
|---|---|
| type | com.interpose.{pillar}.{resource}.{verb} |
| source | The emitting pillar's reverse-DNS identifier — com.interpose.baas. Not a URI path; routing on a /services/ prefix matches nothing. |
| id | Your dedupe key. |
| issandbox | A boolean extension attribute on every envelope. One word, all lowercase, as the CloudEvents spec requires of extension names. |
There is no com.interpose.crypto.* namespace — crypto events are emitted under their owning pillar like everything else. New attributes are added inside the envelope without a version bump, so ignore unknown fields. Order holds within a partition; it does not hold across topics.
Event types you can rely on
Consuming Kafka directly
For higher-volume integrations, direct topic consumption avoids delivery overhead entirely. The broker is self-hosted; connection details and credentials are arranged during onboarding, and there is no self-serve path. Use your own consumer group id, start from latest unless you have a reason to replay, and expect the CloudEvents envelope above as the JSON payload.
| Topic | Fires when |
|---|---|
| vm.baas.orders.submitted | An order is accepted and routed |
| vm.baas.orders.filled | An order fills, whole or partial |
| vm.baas.orders.cancelled | An order is cancelled |
| vm.baas.positions.updated | A position changes |
| vm.pm.rebalancing.initiated | A rebalancing run starts |
Two namespaces, both correct
Topic names use the vm. prefix for historical reasons — it predates the Interpose name and is retained because renaming a topic is a migration, not a rename. The CloudEvents type field uses com.interpose.*. These are different namespaces, and both are right.
Webhooks
Webhook registration works today and event dispatch does not — the detail is in Current limits. The signature scheme below is documented because it is what you will verify against once dispatch ships, and because the one signed sender that is live — the professional-network subscription feed — already uses it.
| Header | Value |
|---|---|
| X-Interpose-Signature | Hex HMAC-SHA256 over the canonical string |
| X-Interpose-Timestamp | Send time, unix epoch milliseconds |
The millisecond unit is the detail that bites
A freshness check written against time.time() in seconds rejects every genuine delivery as roughly 57 years stale. The canonical string uses the raw header value — divide only in the age check, never before signing.
Three further rules, each of which has burned somebody: verify the raw body before parsing, because re-serialising JSON changes the bytes; reject deliveries older than five minutes, because a valid signature on a replayed message is still a replay; and use constant-time comparison, because == on a signature is a timing oracle.
The delivery body is not a CloudEvent
Kafka and SSE carry the CloudEvents envelope. The webhook sender posts its own shape:
Dedupe on delivery_id. There is no id, specversion or type field to key on — those belong to the CloudEvents envelope, not to this one. Return 2xx quickly: acknowledge, enqueue, and do the work asynchronously.
Current limits
what is not built yetServer-Sent Events, WebSocket and Kafka carry real traffic today. These are the parts of the event surface that do not yet, stated so you can design around them rather than discover them.
WebSocket covers BaaS and Portfolio Management
Those are the two streams we publish public paths for, and a connection carries one account. Server-Sent Events is the broader surface today — seven streams, no per-socket subscription. If your integration needs a WebSocket on another pillar, ask us before you design around it.
Webhooks: registration works, dispatch does not
You can register an endpoint and receive a one-time signing secret, but BaaS does not dispatch order, settlement or rebalance events to it today. You may find material describing a retry schedule of “3 attempts: 5s, 30s, 300s” — that describes code that was never written.
An event with no account on it is withheld, not broadcast
Scoping resolves each event to an account and answers with the same predicate as REST. An event carrying no
account_idcannot be resolved, so it reaches only roles that see every book./events/systemtherefore connects and stays empty for a tenant — on top of having no emitter behind it. Failing closed is deliberate: the alternative is leaking exactly the events we cannot reason about.The event log is in-memory, not a replay log
The stream is served from a per-process log capped at 10,000 events. It does not survive a restart, and the
since_id/until_idcursors work within that window only. Reconcile against the REST API rather than treating the stream as your system of record.SQS fan-out is not enabled
The code paths exist in BaaS and Portfolio Management, but the AWS dependency is in no service image and each would need per-service queue configuration. Talk to us before planning around it.