BettorEdgePulse API
WebSocket

Streaming

One WebSocket carries live market data and your order events. Subscribe to what you need; every subscription is answered with a snapshot, then pushed changes. Frames use the same shapes as the REST endpoints, so one parser serves both.

Connect

Partnerwss://pulse.bettoredge.com/v1/your-slug/stream
Your accountwss://pulse.bettoredge.com/v1/bettoredge/stream
  • Partner: send your key as x-api-key on the upgrade request. It needs the read scope, and the path slug must be yours. Order events need no player token on the socket — see order events.
  • Your account: send your personal token as Authorization: Bearer. No key. The token's IP allowlist applies here too.
  • Any other path answers HTTP 404; a stream switched off for maintenance answers 503.
  • Can't set headers (a browser)? Connect bare and, within 5 seconds, send {"op":"auth","api_key":"…"} or {"op":"auth","token":"…"} as the first frame. Never ship a key or token to a browser you do not control.
  • Credentials are re-checked every 60 seconds. A revoked, expired or suspended credential closes the socket.

Example

import WebSocket from "ws";

const ws = new WebSocket("wss://pulse.bettoredge.com/v1/your-slug/stream", {
  headers: { "x-api-key": process.env.PULSE_API_KEY },
});

ws.on("open", () => {
  ws.send(JSON.stringify({ op: "subscribe", id: 1, channel: "depth", order_context_hash: "team:138847:1:home:team:42:0" }));
  ws.send(JSON.stringify({ op: "subscribe", id: 2, channel: "orders" }));
});

const seen = new Set(); // event_id — delivery is at-most-once, but dedupe across reconnects
ws.on("message", (raw) => {
  const frame = JSON.parse(raw.toString());
  switch (frame.type) {
    case "ack":   /* frame.snapshot is the current state */ break;
    case "depth": /* same shape as GET /markets/depth */   break;
    case "order":
      if (seen.has(frame.event.event_id)) return;
      seen.add(frame.event.event_id);
      console.log(frame.event.type, frame.event.data.order_id);
      break;
    case "error": console.warn(frame.code, frame.message); break;
  }
});

ws.on("close", (code) => {
  // 4001 / 4003: fix the credential first. Anything else: back off, reconnect, reconcile.
});

Subscribe

Send subscribe or unsubscribe with a channel. Each is acknowledged, and a subscribe's acknowledgement carries the current snapshot.

{"op":"subscribe","id":1,"channel":"depth","order_context_hash":"team:138847:1:home:team:42:0"}
{"op":"subscribe","id":2,"channel":"contest","contest_id":"138847","contest_type":"team"}
{"op":"subscribe","id":3,"channel":"league","league_id":"1"}
{"op":"subscribe","id":4,"channel":"orders"}
{"op":"unsubscribe","id":5,"channel":"depth","order_context_hash":"team:138847:1:home:team:42:0"}
depth
order_context_hash
One line's price ladder — the same shape as GET /markets/depth, sent when it changes (at most about twice a second per line).
contest
contest_id, contest_type
One contest: the board (same shape as a contest in /markets/available) and its game state.
league
league_id
Every contest in a league, as contest and contest_state frames.
orders
—
Order and position events. Partner: every connected player who granted portfolio:read. Personal token: your own account.

Frames you receive

welcome
First frame after a successful connect: environment (sandbox or production) and your per-connection limits.
ack
Answers each op, echoing its id. A subscribe ack carries snapshot — the current state, built exactly as the REST endpoint builds it — or snapshot_error.
contest
A contest's board changed. contest has the /markets/available shape.
contest_state
Status, clock and score changed, without the board.
depth
A subscribed ladder changed. depth has the /markets/depth shape.
order
An order event (below).
error
An op was refused, with code: VALIDATION_ERROR (malformed, or a hash that names no line), SUBSCRIPTION_LIMIT (a per-connection cap) or RATE_LIMITED (over 20 ops a second). The connection stays open.
{
  "type": "depth",
  "depth": {
    "order_context_hash": "team:138847:1:home:team:42:0",
    "levels": [
      {
        "price": 0.55,
        "available": 420
      },
      {
        "price": 0.56,
        "available": 180
      }
    ]
  }
}

{
  "type": "contest_state",
  "state": {
    "contest_id": "138847",
    "contest_type": "team",
    "status": "inprogress",
    "clock": "Q3 08:41",
    "scores": {
      "home": 17,
      "away": 13
    }
  }
}

Order events

Subscribe to orders once; every event names the player it is about. No player token goes on the socket: a partner receives events for players with a live grant that includes portfolio:read, and a personal token receives its own.

order.placed
A buy placed through POST /orders/place was accepted — order_type, max_slippage and expires_at included. Sent for API-placed buys only: an order placed in the BettorEdge app, or a sale, first appears as its fill, cancel or expiry. Event key order.placed:<order_id>.
order.filled
Part or all of an order matched. data.fill is the fill this event announces; filled_amount is cumulative.
order.cancelled
The order stopped working — cancelled through the API, in the app, or because its market closed. open_amount is what was still open when it stopped (and was returned) — check the event type or status, not open_amount, to know whether an order is working.
order.expired
The order reached its expire_time. open_amount is what was still open when it stopped, and was returned.
position.settled
The contest graded. One event per order: result, stake, what was returned, commission and profit.
  • A player who revokes your app stops producing events immediately.
  • Sandbox keys receive Edge Coin (FREE) events only.
  • event_id is stable for the same fact — the same fill seen twice has the same id. Dedupe on it, including across reconnects.
  • Every order event reports the order's real order_type (limit, or market when it may fill past its price). Money amounts are rounded to the cent.
{
  "type": "order",
  "event": {
    "event_id": "order.filled:5512093:450",
    "type": "order.filled",
    "created_at": "2026-09-21T20:41:07.000Z",
    "player_id": "20391",
    "data": {
      "order_id": "5512093",
      "order_context_hash": "team:138847:1:home:team:42:0",
      "market_type": "FOR_MONEY",
      "title": "Kansas City Chiefs",
      "action": "buy",
      "order_type": "limit",
      "price": 0.55,
      "amount": 10,
      "filled_amount": 4.5,
      "open_amount": 5.5,
      "fill": {
        "position_id": "889120",
        "amount": 4.5,
        "contracts": 8.181818,
        "price": 0.55
      }
    }
  }
}

Limits

Connections per key or personal token
3
Connections per partner
10
Contests per connection
100
Depth lines per connection
200
Leagues per connection
10
Client messages
20 / second
Client frame size
16 KB

The server pings every 30 seconds; any WebSocket client answers automatically. A connection that stops answering is closed.

Delivery, reconnect and reconcile

  • At most once. Nothing is replayed: whatever was in flight when a connection drops is gone.
  • Market frames are conflated — if a line changes faster than you read, you get the latest state, not every step.
  • Order events are never conflated or reordered per connection; a client too slow to take them is closed with 4008.
  • If the stream is switched off for maintenance, the upgrade answers HTTP 503. Retry with backoff.
  1. Reconnect with exponential backoff and jitter (for example 1s, 2s, 4s … capped at 30s).
  2. Re-send every subscribe. Each ack carries a fresh snapshot — replace your local state with it.
  3. Reconcile orders: GET /portfolio/orders and the order's activity for anything that may have changed while you were away.
  4. Keep deduping on event_id: an event can arrive after you already reconciled the change it describes.

Close codes

4001
UNAUTHENTICATED
No credential within 5 seconds, or the credential is invalid, expired or revoked. Fix the credential before reconnecting.
4003
FORBIDDEN
The credential is valid but not for this: another partner's slug, a key without read, an IP not on the allowlist, a player's OAuth token instead of your key, or an app session token. Do not retry unchanged.
4008
SLOW_CONSUMER
Order events could not be delivered fast enough (more than 1,000 queued, or over 1 MB buffered for 10 seconds). Reconnect, then reconcile.
4029
TOO_MANY_CONNECTIONS
A connection cap was reached. Close an existing connection, or back off and retry.
1008
POLICY_VIOLATION