Direct Linking Integration with the ProphetX Trading API

Overview

This guide walks a Direct Link aggregator through linking a user's ProphetX account on the client, then placing orders directly from the client using that user's own credentials. The aggregator's backend runs a separate integration, under the aggregator's own ProphetX account, purely to ingest market fixture data and real-time order book updates — it never holds or uses an individual user's key/secret.

High-level steps:

  1. Link the user's ProphetX account from the client (login + 2FA).
  2. Generate the user's ProphetX trading API key/secret and store it on the user's device only.
  3. Ingest event/market fixture data server-side, using the aggregator's own ProphetX account.
  4. Connect the aggregator's backend to Talaria, ProphetX's in-house WebSocket platform, for real-time order book data, using the aggregator's own account.
  5. Run a geolocation check before every order.
  6. Submit the order directly from the client using the user's own key/secret; the client also listens on its own Talaria WebSocket channel for that order's status.
  7. Certify with ProphetX before going live.

Integration flow

A Direct Link integration is really two separate connections to ProphetX that must never merge into one:

┌────────────────────────────┐        ┌────────────────────────────┐
│  Your app, on the user's    │        │  Your backend                │
│  own device (the client)    │        │  (your own MM account)        │
├────────────────────────────┤        ├────────────────────────────┤
│ • User's ProphetX login      │        │ • Your aggregator ID          │
│   (Step 1, entered once)     │        │ • Your own access_key/        │
│ • User's trading             │        │   secret_key (Steps 3–4)      │
│   access_key/secret          │        │                                │
│   (Step 2 — on-device only)  │        │ Never sees: any user's         │
│ • Places the order (Step 6)  │        │ password, access_key/secret,   │
│ • Own private WebSocket       │        │ or individual order flow       │
│   channel for order status   │        │                                │
└──────────────┬─────────────┘        └──────────────┬─────────────┘
               │                                       │
               ▼                                       ▼
      ProphetX auth + trading API              ProphetX market-maker API
   (per-user credentials, never shared)      (fixtures + shared order book)
StepRuns onCredential in playReaches your backend?
1. Link account (login + 2FA)ClientUser's email/password, entered onceOnly as a stateless proxy, never stored
2. Generate trading key/secretClientUser's session token from Step 1No — key/secret stored on-device only
3. Ingest fixturesYour backendYour own MM access_key/secretN/A — this is your backend
4. Order book via TalariaYour backendYour own MM access_key/secretN/A — this is your backend
5. Geolocation checkClientNoneNo
6. Submit orderClientUser's access_key/secret from Step 2No — order goes client → ProphetX directly
7. Certify———

Worked example: one user placing one order

Sasha opens your app, links their ProphetX account, and places a $10 order on a Lakers moneyline. Stitching together the calls from Steps 1–6:

  1. Login — client calls auth/login with Sasha's email/password. ProphetX responds account_required_2fa; client calls send-verification-code, Sasha enters the code they receive by text, client calls auth/login again with that code and gets back an accessToken.
  2. Generate key — client calls partner/auth/keys with Authorization: Bearer {accessToken} and receives { "access_key": "sasha_ak_...", "secret_key": "sasha_sk_..." }. These are written to the device's secure keychain and never leave it.
  3. (Already running in the background) — your backend is separately subscribed to Talaria on the Lakers game's event_id (from Step 4), using your own MM credentials, so live order-book prices are already flowing to your client's UI.
  4. Geolocation check — client calls check-ip with Sasha's current IP; response comes back { "success": true, "state": "NJ", ... }, so the client proceeds.
  5. Submit order — client calls submit_order directly, authenticated with Sasha's own access_key/secret_key:
{
  "external_id": "8f14e45f-...",
  "aggregator_id": "your-aggregator-id",
  "strike_id": "40a32ae7b39684d430cf494be26cbe6b",
  "price": -130,
  "quantity": 10.0
}
  1. The response's order object shows matching_status: "unmatched"; a few seconds later, the client's private WebSocket channel (private-service=6-device_type=5-user={sasha's account_id}, from Sasha's own registration) delivers an orders event with matching_status: "fully_matched". The client updates its UI — your backend was never involved in this order at all.

What NOT to build

Some integrations have mistakenly built this instead — one connection where the backend does everything on the user's behalf:

┌─────────────────────────────────────────┐
│  Your backend                             │
│                                            │
│  ✗ Stores the user's email/password        │
│  ✗ Calls auth/login itself, repeatedly,    │
│    per user, using that stored password    │
│  ✗ Holds the user's access_key/secret      │
│  ✗ Submits orders on the user's behalf     │
└─────────────────────────────────────────┘

Every line above breaks something: it violates the security requirement below, it bypasses the 2FA flow that auth/login expects a live user to complete, it abuses an endpoint that isn't built for repeated server-side calls, and it means a compromise of your backend is a compromise of every linked user's ProphetX account. If this describes your current integration, stop here, read the section below, and contact ProphetX before doing anything further — this will not pass certification (Step 7).


Security requirement: never touch a user's ProphetX login credentials

This gets its own section, separate from the checklist below, because it's the single most common way Direct Link integrations go wrong:

  • Your backend must never receive, log, or store an end user's ProphetX email/password. That credential pair is entered exactly once, in the modal in Step 1, on the user's own device — submitted directly to ProphetX, or proxied through your backend only as a stateless, non-persisting relay (see the CORS note in Step 1). Persisting it anywhere, even "temporarily" or for debugging, is not compliant.
  • Your backend must never call auth/login, send-verification-code, or auth/extend-session on a user's behalf using stored credentials. These endpoints are for a single, real-time, user-initiated login — not for your backend to re-authenticate as a user later (to sync orders, refresh a session, or poll their account). If you need visibility into a user's activity after the fact, the answer is the client reporting back to you (see the note under Step 6), not your backend logging in as the user.
  • Step 2 hands you a key/secret that belongs on the user's device, not yours. Steps 3–4 are the only place your backend should hold any ProphetX credentials, and those are your own market-maker credentials — never an individual user's.

This isn't a style preference — it's a hard certification requirement (Step 7). An integration found storing user passwords, or replaying them server-side, will not be certified, and existing production access can be suspended until it's remediated.

Coming soon: ProphetX is building self-service API key generation ("Universal API Access") that will let users generate and manage their own trading key/secret directly from their ProphetX account settings — no login modal required in your app at all. Once that's available in sandbox, it will become the preferred way to complete Steps 1–2 for both new and existing Direct Link integrations. If your current integration handles user passwords in any way, plan to migrate to this once it ships — contact ProphetX in the meantime.


Before you start

Companion pages. This guide is the end-to-end flow. The details that come up once you're actually building are split out so this page stays readable: Environments, Hosts, and Tokens (every base URL, which calls run client-side vs backend, CORS, token lifetimes), Order Lifecycle (fills, partials, polling vs WebSocket, cancel_order outcomes), IDs and Field Names, and Sandbox Notes and FAQ.

  • The Trading API is not yet open for public self-serve signup. The aggregator needs its own ProphetX API/market-maker account (separate from any end user's account) to run Steps 3–4. Contact ProphetX's market-making team to get set up in sandbox and, later, production. You'll also pick an aggregator ID (a short, stable slug like juicereel) to send on every order in Step 6 — it's self-declared, not assigned, but tell ProphetX the value you've chosen so order attribution lines up on our side.
  • Two different credential types are in play here and should not be confused: the aggregator's own access key/secret (server-side, used only for fixture ingestion and market-wide order book data in Steps 3–4) and each individual user's access key/secret (client-side only, generated in Step 2, used only by that user's own client in Step 6).
  • Sandbox market-maker endpoints in Steps 3–4 are relative to https://api.sandbox.prophetx.dev/partner. Keep the /partner path as-is.

Step 1: Link the user's ProphetX account

When the user clicks "Link ProphetX" in the aggregator's app, open a modal asking them to log in with their ProphetX email and password.

CORS on these endpoints is allowlisted per-origin, not open. Calling auth/login (and send-verification-code) directly from the browser only works once ProphetX has added your app's origin to the allowlist — send ProphetX your origin(s) to request that. Until then, or if you'd rather not depend on an allowlist, proxy these calls through your own backend instead; the request/response shapes below are identical either way.

No published rate limit on auth/login or send-verification-code specifically — these aren't part of the MM API's rate-limit tiers (a separate, per-endpoint system documented for the Trading API proper). Call auth/login once per session and hold the resulting token rather than re-calling it per-request regardless, and avoid hammering send-verification-code (each call texts the user a new code).

Every call in this step requires the header X-Currency: cash (or point). It's enforced on all /api/v1 auth endpoints. If it's missing or set to anything else, you'll get back an HTTP 400 with an HTML error body instead of JSON — that's the tell that this header is the problem, not something wrong with your request body.

POST https://sandbox.prophetx.dev/api/v1/auth/login
X-Currency: cash
Content-Type: application/json
{
  "email": "...",
  "password": "...",
  "device_id": "<client-generated identifier, e.g. a UUID>"
}
  • device_id is required. It's an opaque string ProphetX doesn't issue or validate the format of — generate one per device/install on the client and send that same value consistently.
  1. If the response indicates two-factor authentication is required, you'll get:
{ "code": "account_required_2fa", "message": "This account required 2FA for log in" }

Call the verification-code endpoint to text the user a code — it's sent to the phone number already on the account, not one you supply here:

POST https://sandbox.prophetx.dev/api/v1/send-verification-code
X-Currency: cash
Content-Type: application/json
{ "email": "..." }
  1. Prompt the user for that code, then call auth/login again with the same body as before plus the code in a code field, to complete the login:
{
  "email": "...",
  "password": "...",
  "device_id": "<same value as the first call>",
  "code": "<the code the user entered>"
}
  1. A successful login returns accessToken and refreshToken — each a claims object (jti, exp, plus account-status fields), not a plain token string — alongside a top-level exp. Open item: whether Step 2's Authorization: Bearer ... header should carry a JWT that ProphetX's API gateway signs from these claims, or something else, isn't confirmed yet — see Open items before wiring up Step 2 against this.

Renewing before accessToken expires: there's no separate refresh-only endpoint — call the same X-Currency-headered endpoint below, still authenticated with your (not-yet-expired) Authorization: Bearer header, passing the refreshToken from login:

POST https://sandbox.prophetx.dev/api/v1/auth/extend-session
X-Currency: cash
Content-Type: application/json
Authorization: Bearer <still-valid access token>
{ "refreshToken": "<refreshToken from login>" }

This returns a fresh access_token / refresh_token / exp, same shape as login's response. Because it requires a currently-valid Authorization header, call it before the access token expires, not after — this renews an active session rather than exchanging a bare refresh token for a new one. (An earlier answer in this thread pointed to partner-docs.prophetx.co/swagger/mm/index.html#/auth/post_auth_refresh — that's a different, unrelated refresh mechanism belonging to the market-maker partner API's own access_key/secret_key session system, not this participant login flow. Disregard it for this step.)

These are the same public endpoints ProphetX's own sandbox web app (sandbox.prophetx.dev) calls, not endpoints published in ProphetX's partner API docs. Once going to production, replace https://sandbox.prophetx.dev with https://www.prophetx.co as the base URL for all APIs.


Step 2: Generate the user's trading API key/secret

Open a second modal confirming the user wants to generate a ProphetX API key for the aggregator to trade with. On confirmation, call:

POST https://sandbox.prophetx.dev/partner/auth/keys
Authorization: Bearer {accessToken from Step 1}

No request body is needed. The method matters: POST generates a new key pair; GET on the same path only lists the keys the account already has, so a GET on a fresh account returns { "data": [] } — that's not an error, it just means nothing has been generated yet.

Response (201 Created)

{
  "data": {
    "id": "...",
    "access_key": "...",
    "secret_key": "..."
  }
}
  • The pair is wrapped in a data object. secret_key is only returned at creation — capture it then; it can't be fetched again later.
  • Store the returned access_key / secret_key on the user's device only (e.g. secure local storage or platform keychain). Do not send it to, or persist it on, the aggregator's backend.
  • This is a deliberate trade-off: keeping the trading secret off the aggregator's servers limits the aggregator's exposure if its backend is ever compromised, at the cost of the aggregator's backend not being able to place orders or query order status for the user directly (see Step 6).
  • Any KYC-approved, non-suspended account can generate a key — there's no longer a market-maker account-type requirement. The only other reason this fails is hitting the account's key-pair cap.

Exchange the key pair for a trading token

The access_key / secret_key are not sent on trading requests directly. They're used exactly once per session, to log in to the Trading API and get a Bearer token:

POST https://api.sandbox.prophetx.dev/partner/auth/login
Content-Type: application/json
{ "access_key": "...", "secret_key": "..." }

Response

{
  "data": {
    "access_token": "...",
    "access_expire_time": 1234567890,
    "refresh_token": "...",
    "refresh_expire_time": 1234567890
  }
}

Every Trading API call from here on — submit_order, get_balance, cancel_order, the WebSocket register — uses Authorization: Bearer {access_token}. There's no alternative access-key auth scheme on those endpoints. The access_token expires after 10 minutes; renew it with POST /partner/auth/login's sibling POST /partner/auth/refresh (body { "refresh_token": "..." }) before it lapses rather than waiting for a 401. Do this exchange on the client, since that's where the key/secret lives.

This is a different login from Step 1: Step 1 is the user's email/password login to ProphetX (participant API), used only to authorize key generation. This one is the Trading API login, keyed off the generated pair.


Step 3: Ingest event/market fixture data (aggregator backend)

Using the aggregator's own ProphetX account credentials (from "Before you start", not any user's key), pull tournaments, events, and markets so you have the identifiers you'll need later to place an order.

GET /mm/get_tournaments
GET /mm/get_sport_events?tournament_id={id}
GET /v4/mm/get_multiple_markets?event_ids={id1,id2,...}
GET /v4/mm/get_price_ladder

Record the following for each market selection you plan to support:

FieldExampleNeeded for
tournament_id109Fetching events, WebSocket topic naming
event_id10073803Fetching markets
market typemoneyline / total / spreadFiltering which markets you expose
sub_typeplayer_total_hits (optional field)Needed to subscribe to prop-specific channels in Step 4 — fetch it via the v2/v4 get_multiple_markets endpoint version and read it defensively, since it's optional
strike_id40a32ae7b39684d430cf494be26cbe6bRequired on order submission in Step 6
valid price ladderfrom get_price_ladderOrders must use a price on this ladder

Reference: docs.prophetx.co/docs/integration (Step 3: Seed tournaments, events, markets, and prices).


Step 4: Real-time order book via Talaria WebSocket (aggregator backend)

Real-time order book data is delivered over Talaria, ProphetX's in-house WebSocket platform. Talaria has a stable wire protocol — consistent event names, double-JSON-encoded frames, and a consistent channel/auth signing scheme — and no cluster/region concept: one Talaria deployment is your entire connection target, and routing is by hostname.

The event strings on the wire — pusher:connection_established, pusher:subscribe, pusher:signin, pusher:signin_success, pusher_internal:subscription_succeeded, pusher:error — are simply what Talaria's wire protocol calls them. That's the protocol's own naming, not a reference to any other vendor.

ProphetX has moved off a single shared market-data channel to dedicated channels per event and, for props, per market sub-type. Both migration deadlines have already passed as of this writing (player props: July 29, 2026; main markets, which now also include sup_moneyline and moneyline_3_way: August 12, 2026) — the old shared channel no longer carries market_selections at all, so the aggregator's backend needs to be on the new model now, not as a future migration. This channel model is a property of the subscription API itself — it's the same event/event_subtype subscription types regardless of transport details.

  1. Get connection config.
GET /websocket/connection-config

This response includes {app_id, key} plus a host (and likely port) field — Talaria has no cluster to select, so there's nothing region-based to configure. The exact field name for that host is not finalized in any source available as of this writing. Treat it like an undocumented endpoint: confirm the real shape with ProphetX, then substitute it for the TALARIA_WS_HOST / TALARIA_WS_PORT placeholders below.

  1. Open the WebSocket connection to {scheme}://{host}[:{port}]/app/{key}?protocol=7&..., using the host/port from step 1:
connect(host=TALARIA_WS_HOST, port=TALARIA_WS_PORT, path=f"/app/{APP_KEY}", query="protocol=7", secure=False)

That's the entire connection setup — no region/cluster parameter exists to configure. Once connected, capture socket_id off the pusher:connection_established event: its data field is a JSON string containing JSON — decode it twice.

  1. Discover what to subscribe to — event_id (GET /mm/get_sport_events?tournament_id={id}) and, for props, sub_type per market (GET /v4/mm/get_multiple_markets?event_ids={ids} — use the v4 endpoint version; sub_type is optional, so read defensively).

  2. Register your subscription — POST /v4/mm/websocket (this replaces the old POST /v4/mm/pusher call; path and body shape unchanged) with a declarative, complete subscription set on every call:

{
  "socket_id": "...",
  "service": "pusher",
  "subscriptions": [
    { "type": "event", "ids": ["18756"] },
    { "type": "event_subtype", "ids": ["18756:player_to_record_a_double_double"] }
  ]
}

The "service": "pusher" field stays literally "pusher" — it names the wire protocol being spoken, not a vendor. Subscribing with type "event" always gets every current main market for that event — you don't need to track the main/prop split yourself.

  1. Complete the handshake: send pusher:signin using the authenticated block from the register response, wait for pusher:signin_success, then send pusher:subscribe per channel using that channel's own auth + channel_name from authorized_channel (each channel's auth only works for that channel), and wait for pusher_internal:subscription_succeeded on each. The signing scheme is a {key}:{hex-HMAC} string, computed over {socket_id}:{channel_name} for channel auth and {socket_id}::user::USER_DATA for signin.

  2. Bind and consume — bind market_selections on each authorized channel. Payload shape is unchanged — only the routing changed (per the channel-model note above). Classify incoming messages by the response's scope field (event_id, and sub_type where present), not by channel name or event name.

Re-fetch connection-config at least every 30 minutes and reconnect if it has changed; treat a dropped socket as a signal to reconnect, get a fresh socket_id, and re-register (reconnecting also resets your channel usage). Always read channel_limit from the register response rather than hardcoding it — it's account-configured, counts the cumulative union of channels you've registered on that socket, and going over it rejects the entire request with HTTP 400 exceed_subscription_count. Subscribe selectively (e.g. not every event's full prop set) rather than assuming headroom.

What's confirmed vs. not: the wire protocol, event names, double-JSON encoding, the subscribe/signin auth scheme, and all channel/event naming are confirmed. What's not confirmed is the production hostname/port/TLS scheme you'll actually connect to, and the precise field name connection-config returns for it. Get both from ProphetX before touching your connection code.

This design avoids the earlier concern about needing one live connection per linked user: the aggregator's backend keeps a single connection for shared market data, and each user's own order-status updates are handled by their own client in Step 6 instead. One relevant detail: this same registration flow also auto-authorizes a private per-account channel (private-service=6-device_type=5-user={account_id}, carrying an orders event) — for the aggregator's backend that only reflects the aggregator's own account's activity, not any individual user's, which is consistent with Step 6's design. The residual question is whether the aggregator's backend needs any visibility into individual users' orders (e.g. for its own history/analytics) — see Open items.

Reference: docs.prophetx.co/docs/integrate-with-websockets (confirm the connection host/port with ProphetX before relying on it); docs.prophetx.co/docs/websocket-events.


Step 5: Geolocation check (before every order)

Before letting a user submit an order, call ProphetX's IP-based geolocation check and only proceed if it returns success.

POST https://sandbox.prophetx.dev/ip-geolocation/api/v1/check-ip
Content-Type: application/json
{ "ip_address": "<user's current IP address>" }
  • Must be a POST — this endpoint doesn't accept GET, which returns an HTTP 404 (not a validation error) if you try it.
  • The field is ip_address, not ip, and it's required and validated as a real IP address.
  • No X-Currency or other auth header is required on this endpoint.
  • An optional user_uuid field can be included alongside ip_address.

Response

{
  "success": true,
  "country": "US",
  "state": "NJ",
  "city": "Newark",
  "reason": ""
}
  • success reflects whether the location is authorized, not just whether the request itself succeeded — a call from a blocked/sanctioned location returns a normal HTTP 200 with success: false and a reason string explaining why, not an error status.
  • Proceed to Step 6 only if success is true. If it's not, block the order and give the user a way to retry (e.g. refresh location, try again).
  • country / state / city are omitted when not applicable.

Step 6: Submit the order from the client

Once the geolocation check in Step 5 passes, the client — not the aggregator's backend — submits the order directly to ProphetX, authenticated with the Bearer access_token obtained from the user's own key/secret in Step 2.

POST v4/submit_order

There's no separate market-order endpoint — submit_order handles both a resting limit order and a simulated market order, and the order_strategy field is what tells them apart. price is always required; there's no priceless order type. (order_strategy also accepts ALO — Add-Liquidity-Only / Post-Only — see Order Strategy in the main integration guide; the two values are mutually exclusive.)

Regular limit order — rests on the order book until matched or canceled; omit order_strategy entirely:

{
  "external_id": "<client-generated uuid>",
  "aggregator_id": "<your aggregator id>",
  "strike_id": "<from Step 3>",
  "price": -130,
  "quantity": 1.0
}

Simulated market order — set order_strategy: "fillOrKill" and price the order to cross the current best available price (read live off the Step 4 order-book feed right before submitting). If liquidity is there, it fills immediately. If it isn't fully matched right away, it briefly rests (status: open) and is automatically canceled after about 2 seconds unmatched — there's no synchronous/immediate fill-or-kill today, so don't assume the response itself always carries the final state (see the note below):

{
  "external_id": "<client-generated uuid>",
  "aggregator_id": "<your aggregator id>",
  "strike_id": "<from Step 3>",
  "price": -122,
  "quantity": 1.0,
  "order_strategy": "fillOrKill"
}

See partner-docs.sandbox.prophetx.dev/swagger/mm/index.html for the full request schema.

  • aggregator_id is optional but recommended: pass your aggregator ID in this dedicated field for order-attribution tracking. It's self-declared (no registry; oddsjam, juicereel, etc.) — just keep it consistent on every order, since that exact string is what attribution keys on. It follows the same format rule as external_id: max 100 characters, letters, digits, -, and _ only.
  • external_id must still be unique per order — reusing one on a retry gets it rejected, which is what prevents duplicate submission. Suggested convention: {aggregator_id}_{client-generated uuid}.
  • price is a limit, not a target: it's the worst price you'll accept. The engine fills against resting orders at their prices, best first, and never worse than the book — so a more aggressive price than what's available just fills at what's available. It must be a value from get_price_ladder (Step 3); anything else is rejected with 400 invalid_price.
  • quantity is the stake in USD (minimum $1).
  • If you submit through the batch endpoint (place_multiple_orders), you can set aggregator_id once at the batch level as a default and override it per item if needed.
  • Direct Link aggregators can place either order type through this one endpoint — in practice most only place limit orders, using order_strategy: "fillOrKill" (above) to simulate a market order when a resting order isn't wanted.

Everything from here to the end of this step — polling vs WebSocket, partial fills, cancel_order outcomes, REST-vs-WebSocket field differences, reading history — is covered in more depth in Direct Link: Order Lifecycle. Read that before going live.

The submit_order response is pre-match. The order is created and the response returns before matching runs, so the order object you get back will show matching_status: unmatched and filled_quantity: 0 essentially every time — regardless of how much liquidity was available. That's expected, not a failed fill. To learn what actually happened, wait ~2–3 seconds and then GET /v4/mm/get_order/{id} (passing the order_id from the response as id), or listen on the WebSocket orders channel below. Read these fields on the order:

FieldMeaning
matching_statusunmatched, partially_matched, or fully_matched
filled_quantityHow much of quantity has matched
open_quantityHow much is still resting on the book
statusOrder lifecycle state (open, closed, canceled, void, etc. — separate from matching_status)

For a fillOrKill order, poll until it reaches a terminal state rather than sleeping a fixed interval: matching runs within well under a second, and the kill sweep cancels any unfilled remainder about 2 seconds after the order's last activity — so a remainder that keeps picking up small partial fills can stay open longer than 2 seconds. Keep polling (every ~0.5–1s) until open_quantity is 0. There are only two ways that happens: matching_status: fully_matched (the order is done filling — this includes a partial fill whose remainder was killed, see below), or status: canceled with filled_quantity: 0 (nothing filled). Most orders settle within ~2–3 seconds; put a sane cap (e.g. 10s) on the loop.

What a partial fill looks like afterward. fillOrKill is "fill what's available, kill the rest," not all-or-nothing (there's no all-or-none option today). When the unfilled remainder is killed, the order is not marked canceled — it's rewritten as a smaller, fully matched order: quantity shrinks to the amount that filled, open_quantity becomes 0, matching_status becomes fully_matched, and status stays open (it's now a live position until settlement). To tell a partial from a full fill, compare quantity to what you submitted. A plain resting order (no order_strategy) can of course still match later, so keep the WebSocket subscription for those.

To show the user their own order status in real time, the client runs the same connect-and-register flow as Step 4 (Sections 2–5 of the WebSocket doc), authenticated with that user's own key/secret. It doesn't need to request any event/event_subtype subscriptions for this — the register response automatically includes a private per-account channel (private-service=6-device_type=5-user={user's account_id}) carrying an orders event; bind that on the client to get the user's own order/trade updates.

  • Because the user's key/secret never reaches the aggregator's backend, the aggregator's backend has no direct visibility into individual order submissions, fills, or cancellations unless the client separately reports them back to the aggregator — flag this as an open item if the aggregator needs server-side order history or analytics.
  • Cancellation follows the same client-side pattern: POST .../cancel_order with the external_id and the order_id returned at submission.

Step 7: Certify and go live

Once Steps 1–6 are working end-to-end in sandbox, go through ProphetX's production transition / certification process before pointing your integration at production hosts. ProphetX will confirm certification is complete before you switch over — do not flip any base URL until you have that confirmation.


Open items

TopicStatus
Trading API general availabilityNot yet public — requires ProphetX to approve the aggregator's own API/market-maker account for Steps 3–4.
accessToken → Step 2 Bearer tokenauth/login returns accessToken as a claims object (jti, exp, account-status fields), not a token string, but Step 2 sends it directly as Authorization: Bearer {accessToken}. Confirm with the team that owns the API gateway whether the client needs a separately-issued JWT (e.g. signed from these claims) instead of the raw claims object.
Talaria connection-config response shapeNot finalized in any source available as of this writing — the register/connection-config endpoint is owned by a separate façade service, and the field replacing (or joining) cluster isn't settled yet. Confirm the real shape with ProphetX before changing connection code.
Talaria production hostname/port/TLSDeployment/ingress-specific; not published anywhere available here. Get this directly from ProphetX.
Client-side key storage riskThe user's trading secret now lives on-device rather than server-side. Run a security review of local storage/keychain handling on each client platform the aggregator supports.
Backend visibility into user ordersThe aggregator's backend won't see individual submissions/fills unless the client reports them back. Decide if that reporting path is needed for the aggregator's own order history or analytics.
Certification timelineTo be scheduled with ProphetX once sandbox testing is complete.
Self-service API key generation ("Universal API Access")In development, targeting sandbox availability soon. Will let users generate and manage their own trading key/secret directly from their ProphetX account settings, removing the need for partner apps to run their own login modal. Integrations currently handling user passwords in any way (see Security requirement above) should plan to migrate to this once it ships — contact ProphetX.

References

  • Trading API integration guide (fixture ingestion & WebSocket, aggregator's own account): docs.prophetx.co/docs/integration
  • WebSocket integration: docs.prophetx.co/docs/integrate-with-websockets
  • Order submission / market-maker API (swagger): partner-docs.sandbox.prophetx.dev/swagger/mm/index.html
  • Production transition / going live: docs.prophetx.co/docs/prophetx-service-api-switch-to-production

Did this page help you?