AXIS Docs

DevelopersAXIS API

WebSocket API

Stream contract state, account changes, trades, tickers, depth and candles from the AXIS API: subscribe, receive a snapshot, then live updates.

Endpoint#

Network URL
Testnet wss://demo-api.axis.markets/ws
Mainnet Not available yet

One connection carries many subscriptions. Messages are JSON text frames in both directions. Payloads use the shapes and conventions of the REST API: big integers as strings, token contract addresses for assets and canonical base/quote market keys for trades, contract and ticker. depth and candles keep the requested orientation.

Protocol#

Client messages#

Message Reply
{op: 'subscribe', channel, id?, ...params} The channel snapshot, echoing id
{op: 'unsubscribe', channel, id?, ...params} {type: 'unsubscribed', id, channel, topic}
{op: 'ping', id?} {type: 'pong', id}
  • id is optional and can be any JSON value. The server echoes it in the reply and in errors, so you can match them to requests.
  • Channel parameters sit at the top level of the message, next to op and channel.
  • unsubscribe takes the same parameters as the subscribe it cancels, because the server derives the topic from them. It answers unsubscribed even when nothing was subscribed, provided the parameters are valid.
  • Subscribing again to a topic you already hold sends a fresh snapshot without adding a subscription.
  • ping is an application-level heartbeat for clients that cannot send WebSocket ping frames, such as browsers.
{"op": "subscribe", "id": 5, "channel": "depth", "market": "XLM/CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", "limit": 3}
{"type": "pong", "id": "p1"}
{
  "type": "unsubscribed",
  "id": 9,
  "channel": "trades",
  "topic": "trades:CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
}

Server messages#

Every server message has a type. Messages of a subscription also carry its topic.

  1. The snapshot answers subscribe. It carries the request id and the topic, and holds the full current state of the channel.
  2. Updates follow as the state changes. They carry the topic but no id.

Route messages by topic, not by your request parameters. The server normalizes the parameters: asset codes become contract addresses, trades markets take the canonical order and defaults are filled in.

Topics#

Channel Topic
contract contract
ticker ticker
account account:<address>
trades trades:<base>/<quote>, or trades:* for every market
depth depth:<base>/<quote>/<depth>/<step>/<limit>, with auto as <step> when no step was given
candles candles:<base>/<quote>/<resolution in seconds>

For example, a depth subscription to XLM/<USDC address> with limit: 3 gets the topic depth:CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC/CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA/20/auto/3.

Errors#

Errors are {type: 'error', id?, error}. A failed subscribe creates no subscription, and the connection stays open.

Cause error
Malformed JSON Invalid JSON, without id
Binary frame Binary messages are not supported, without id
A JSON value that is not an object or an array Invalid message
Unknown or missing op, an array included Unknown op: <op>
Unknown channel Unknown channel: <channel>
Invalid account address Invalid account address
Invalid channel parameter The REST error text, for example Bad request. Invalid parameter: "market". Expected "BASE/QUOTE" format
Too many topics Too many subscriptions (max 20)
Too many accounts Too many account subscriptions (max 10)
Unexpected server error Internal error
{"type": "error", "id": 7, "error": "Unknown channel: nope"}

Channels#

contract#

Contract state and ledger progress. No parameters.

The snapshot is {type: 'contract', id, topic, ledger, data}:

Field Type Description
ledger number Last ledger processed. Absent until the service has processed one after a restart.
data object Same body as GET /contract

Updates:

Message When
{type: 'contract', kind, market?, data, topic} The state changed. kind is freeze, config or market. market is the canonical base/quote key, for kind: 'market' only. data is the full /contract body after the change.
{type: 'ledger', ledger, topic} A ledger was processed, with or without AXIS events. A burst of ledgers, for example while older ledgers are replayed after a restart, is coalesced into the latest one.

kind: 'market' arrives for every refresh event: a market opened with subsidize, or checked against the oracle again with requote or subsidize.

Snapshot, with one of the six markets shown:

{
  "type": "contract",
  "id": 1,
  "topic": "contract",
  "ledger": 5002168,
  "data": {
    "address": "CA6P26K4QNNIMTYP22ILTSCXQQDEKNWJIZJPC34YEZYOLBNT7YUPX7XS",
    "frozen": false,
    "config": {
      "safetyAdmin": "GBDCULE53LUPK4XHUCXBI35MAZFQHENMZ3JRKAJS2PPYBV646M6XKVHG",
      "oracle": "CDEIQLTE3Y3XQMYPSZFJC2OXIS67F4MURGP73NCE2C2NZZ22RNSPV7ZI",
      "listingMinDays": 90,
      "marketListingFee": "180000000000",
      "minTradeSize": "10000",
      "ledgerTime": 5
    },
    "markets": [
      {
        "base": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
        "quote": "CCUUDM434BMZMYWYDITHFXHDMIVTGGD6T2I5UKNX5BSLXLW7HVR4MCGZ",
        "created": "2026-10-05 21:12:47",
        "refreshed": "2026-10-05 21:12:47"
      }
    ]
  }
}

Ledger update:

{"type": "ledger", "ledger": 5002169, "topic": "contract"}

account#

One account's live orders and backing, plus the fills and swaps it takes part in.

Parameter Type Default Description
address string required Account address, G... or C...

The snapshot is {type: 'account', id, topic, address, ledger, orders, backing}, the body of GET /account/:address. A subscribed account is watched: backing covers every token of the open markets, whether or not the account has orders, and changes are pushed as they happen. The snapshot waits up to 5 seconds for these records to load.

Updates:

Message When
{type: 'order', address, action, order, fill?, topic} An order of the account changed. order is the full order after the change.
{type: 'backing', address, asset, backing, backed, topic} The account's balance, allowance or authorization in asset changed, or an order selling asset changed. backing is the Backing record, with headroom for a trustline asset, or null once it is no longer tracked. backed maps every live order ID selling asset to its backed amount.
{type: 'trade', address, market, trade, topic} The account was the taker of a fill. trade is a Trade record and market its canonical base/quote key.
{type: 'swap', address, swap, topic} The account made a swap. swap is a Swap record.
action Meaning
new Order created
fill Partially filled. The order stays live.
filled Filled completely and removed
update Amount, price or expiration changed, or an expired order revived
cancel Removed by its owner
expire Passed its expiration

fill comes with the fill and filled actions and describes the fill from the maker's side:

Field Type Description
sold string Amount of the order's selling token the maker delivered
bought string Amount of the order's buying token the maker received
taker string Taker address
trade string Trade ID
ts number Fill time, UNIX seconds
crossfill boolean true on the fill of a crossfill taker order, where taker is the caller who keeps the surplus. Absent otherwise.

Snapshot, with one of the account's orders and one of its backing records shown:

{
  "type": "account",
  "id": 2,
  "topic": "account:GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
  "address": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
  "ledger": 5002168,
  "orders": [
    {
      "id": "295788099352601974601093423915094661279",
      "status": "ACTIVE",
      "buying": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC",
      "selling": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
      "price": "3504154500000000000",
      "rprice": 3.5041545,
      "quote": "5497500",
      "amount": "5497500",
      "owner": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
      "backed": "5497500",
      "backing": {
        "balance": "69240923294",
        "allowance": "355869776",
        "liveUntil": 5520550,
        "authorized": true,
        "updated": "2026-10-03 13:33:27"
      },
      "created": "2026-10-03 09:46:52",
      "updated": "2026-10-03 09:46:52",
      "cursor": "1407218664943353069569"
    }
  ],
  "backing": {
    "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC": {
      "owner": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
      "asset": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
      "balance": "69240923294",
      "allowance": "355869776",
      "liveUntil": 5520550,
      "authorized": true,
      "budget": "355869776",
      "updated": "2026-10-03 13:33:27"
    }
  }
}

A partial fill of one of the account's orders:

{
  "type": "order",
  "address": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
  "action": "fill",
  "order": {
    "id": "9238900405409148379285264828899405993",
    "status": "ACTIVE",
    "buying": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC",
    "selling": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
    "price": "3400183201870916805",
    "rprice": 3.4001832018709166,
    "quote": "6442790",
    "amount": "2098101",
    "owner": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
    "backed": "2098101",
    "backing": {
      "balance": "69238809011",
      "allowance": "353355132",
      "liveUntil": 5520567,
      "authorized": true,
      "updated": "2026-10-03 13:33:54"
    },
    "created": "2026-10-03 13:33:17",
    "updated": "2026-10-03 13:34:17",
    "cursor": "1407983432454002180096"
  },
  "fill": {
    "sold": "4344689",
    "bought": "14772739",
    "taker": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
    "trade": "1407986810154796449795",
    "ts": 1791034457
  },
  "topic": "account:GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5"
}

A backing change, with two of the order IDs in backed shown:

{
  "type": "backing",
  "address": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
  "asset": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC",
  "backing": {
    "owner": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
    "asset": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC",
    "balance": "489443642225",
    "allowance": "558137166",
    "liveUntil": 5520557,
    "authorized": true,
    "budget": "558137166",
    "updated": "2026-10-03 13:33:54"
  },
  "backed": {
    "183931260950848202116713872522883870362": "24846547",
    "153390214124585565809379122683319801444": "10341628"
  },
  "topic": "account:GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5"
}

The taker side of a fill:

{
  "type": "trade",
  "address": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
  "market": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
  "trade": {
    "type": "trade",
    "id": "1407985402778570719235",
    "order": "309519727566973417685211056811511198536",
    "taker": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
    "maker": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
    "soldAsset": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
    "boughtAsset": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC",
    "sold": "1389711",
    "bought": "4661256",
    "price": 3.3541189499111685,
    "cursor": "1407985402778570719235",
    "timestamp": "2026-10-03 13:33:52",
    "left": "0"
  },
  "topic": "account:GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5"
}

Notes:

  • Updates of the account can arrive before its snapshot while the snapshot waits for the backing to load. The snapshot is built after them, so replace your state with it when it arrives.
  • The maker side of a fill arrives as an order message with fill, the taker side as a trade message. A self-trade produces both, as in the examples above.
  • The taker order fill of a crossfill (crossfill: true) is not pushed to the caller as a trade message. The owner of the taker order receives it as an order message whose fill carries crossfill: true.
  • Several backing changes of the same token within one processing step are coalesced into one message.
  • expire comes from the service's clock. The contract emits no event when an order expires.
  • The service watches a limited number of accounts at a time. Past that limit, a new subscription gets backing records only for the tokens of the account's orders.

trades#

Fills of one market or of all markets.

Parameter Type Default Description
market string every market BASE/QUOTE in either orientation and any asset format, or * for every market

The snapshot is {type: 'trades', id, topic, market, trades}. market is the canonical base/quote key or *, and trades holds the latest 50 records, newest first, as returned by GET /trades.

Updates are {type: 'trade', market, trade, topic}, one per fill as it is indexed, with market set to the fill's canonical key.

Snapshot of XLM/USDC, requested as XLM/<USDC address>, with one of 50 records shown:

{
  "type": "trades",
  "id": 3,
  "topic": "trades:CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
  "market": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
  "trades": [
    {
      "type": "trade",
      "id": "1407976395581195026434",
      "order": "133725813046786288052094016278953483903",
      "taker": "GBDCULE53LUPK4XHUCXBI35MAZFQHENMZ3JRKAJS2PPYBV646M6XKVHG",
      "maker": "GBDCULE53LUPK4XHUCXBI35MAZFQHENMZ3JRKAJS2PPYBV646M6XKVHG",
      "soldAsset": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
      "boughtAsset": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
      "sold": "835531",
      "bought": "3741722",
      "price": 4.478256342373892,
      "cursor": "1407976395581195026434",
      "timestamp": "2026-10-03 13:31:12",
      "left": "0"
    }
  ]
}

Update on a trades:* subscription:

{
  "type": "trade",
  "market": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
  "trade": {
    "type": "trade",
    "id": "1407991032280789286915",
    "order": "9238900405409148379285264828899405993",
    "taker": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
    "maker": "GACVXELMTE6JXAQCMR4OJOYBZFD6VUDLV4YZGUR2ONN77EGZVY4YCLY5",
    "soldAsset": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC",
    "boughtAsset": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
    "sold": "7133928",
    "bought": "2098101",
    "price": 0.29410179076660153,
    "cursor": "1407991032280789286915",
    "timestamp": "2026-10-03 13:35:32",
    "left": "0"
  },
  "topic": "trades:*"
}

Notes:

  • The snapshot can include swap records, while updates carry fills only. Swaps are pushed on the trader's account channel.
  • When you merge the snapshot with pages from GET /trades, deduplicate by id.
  • Updates include the fill of a crossfill taker order, flagged crossfill: true. Leave it out of volume sums, as for GET /trades.

ticker#

24-hour statistics of every market. No parameters.

The snapshot is {type: 'ticker', id, topic, data}, where data is the body of GET /ticker/24h. Updates have the same shape without id and always carry every market. An update follows fills and refresh events of the contract, at most once every 5 seconds.

Snapshot, with one of the six entries shown:

{
  "type": "ticker",
  "id": 4,
  "topic": "ticker",
  "data": {
    "ledger": 5002168,
    "timestamp": 1791034432,
    "ticker": [
      {
        "symbol": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
        "openPrice": "0.2968348",
        "highPrice": "0.3140691",
        "lowPrice": "0.2879428",
        "lastPrice": "0.2981408",
        "volume": "2702.6187351",
        "quoteVolume": "810.8429328",
        "change": 0.44,
        "avgPrice": "0.3000212",
        "trades": 2854
      }
    ]
  }
}

depth#

Aggregated orderbook of one market, in the orientation you request.

Parameter Type Default Description
market string required BASE/QUOTE, each side in any asset format
depth number 20 Band around the mid price in percent, greater than 0 and at most 100
step decimal automatic Price step of the levels
limit integer 100 Levels per side, 1 to 500

The parameters work as in GET /depth. The snapshot is {type: 'depth', id, topic, market, depth, data}, where market is <base>/<quote> as contract addresses in the requested orientation, depth the band and data the /depth body.

Updates have the same shape without id. Each one carries the whole book, not a delta. An update is sent at most once per second per topic, after an order or a backing change that can affect the market, and is skipped when its levels are identical to the previous update. The first update after the snapshot can repeat the snapshot's levels.

{
  "type": "depth",
  "id": 5,
  "topic": "depth:CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC/CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA/20/auto/3",
  "market": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC/CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
  "depth": 20,
  "data": {
    "ledger": 5002168,
    "timestamp": 1791034432,
    "step": "0.00001",
    "bestBid": "0.2233046",
    "bestAsk": "0.2242675",
    "bids": [
      ["0.2233", "0.0297168"],
      ["0.22259", "0.5495841"],
      ["0.21885", "0.11365"]
    ],
    "asks": [
      ["0.22427", "0.8481886"],
      ["0.22513", "0.5766172"],
      ["0.22594", "0.340319"]
    ]
  }
}

candles#

Candles of one market, in the orientation you request.

Parameter Type Default Description
market string required BASE/QUOTE, each side in any asset format
resolution seconds or alias required A resolution accepted by GET /candles, such as 300 or 5m
limit integer 200 Candles in the snapshot, 1 to 200

The snapshot is {type: 'candles', id, topic, market, resolution, candles}. resolution is in seconds and candles holds the latest limit buckets up to and including the current one, oldest first, in the tuple format of /candles. Buckets without fills are left out, so the snapshot can hold fewer rows than limit.

Updates are {type: 'candle', market, resolution, candle, topic}, one per bucket that fills changed, oldest first, at most once per second per topic. Replace the candle with the same timestamp, or append it when it is new.

Hourly XLM/USDC snapshot with limit: 2:

{
  "type": "candles",
  "id": 6,
  "topic": "candles:CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC/CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA/3600",
  "market": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC/CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
  "resolution": 3600,
  "candles": [
    [1791028800, "0.2201979", "0.2259326", "0.2159081", "0.2223594", "9.6849475", "2.1374418", 36],
    [1791032400, "0.2230679", "0.2250866", "0.2188517", "0.2233012", "5.925785", "1.3193004", 23]
  ]
}

Update of a 5-minute CETES/XLM subscription:

{
  "type": "candle",
  "market": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
  "resolution": 300,
  "candle": [1791034500, "0.2941018", "0.3033564", "0.2937915", "0.3033564", "2.269334", "0.6778706", 3],
  "topic": "candles:CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC/CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC/300"
}

Notes:

  • The snapshot is sorted oldest first, the opposite of the REST default.
  • limit only sizes the snapshot and is not part of the topic. Subscriptions that differ only in limit share a topic.
  • With a small limit, the snapshot can be empty early in a bucket, before its first fill.

Limits#

Limit Value When exceeded
Inbound message size 16 KiB The server closes the connection with code 1009
Topics per connection 20 subscribe fails with Too many subscriptions (max 20)
Account topics per connection 10 subscribe fails with Too many account subscriptions (max 10)
Server ping Every 30 seconds A connection that sent neither a pong frame nor any message since the previous ping is terminated
Outgoing buffer 4 MiB A client that does not read fast enough is disconnected

Browsers and most WebSocket libraries answer ping frames automatically, so a live client stays connected without extra work. Open more connections if you need more than 20 topics.

Reconnecting#

  • Subscriptions belong to the connection. After a reconnect, subscribe again to every channel you need.
  • The server does not replay updates missed while you were disconnected. The new snapshots replace whatever state you built before.
  • Reconnect with a growing delay. AxisStreamClient starts at 1 second and doubles the delay up to 30 seconds.
  • Detect half-open connections: send {op: 'ping'} periodically and reconnect when nothing arrives for twice that period. AxisStreamClient pings every 20 seconds.
  • While the connection is down, poll the REST API instead, for example GET /contract, GET /account/:address and GET /depth. The JS client's Axis class polls /contract and its accounts every 15 seconds by default until the stream is back.
  • A client that falls 4 MiB behind is dropped. Keep message handlers fast or subscribe to fewer topics.

Example#

A minimal client with the native WebSocket of a browser or of Node.js 22 or later:

const USDC = 'CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA'
const market = `XLM/${USDC}`
const ws = new WebSocket('wss://demo-api.axis.markets/ws')

ws.onopen = () => {
    ws.send(JSON.stringify({op: 'subscribe', id: 1, channel: 'depth', market, limit: 10}))
    ws.send(JSON.stringify({op: 'subscribe', id: 2, channel: 'trades', market}))
}

ws.onmessage = event => {
    const message = JSON.parse(event.data)
    switch (message.type) {
        case 'depth': //snapshot and updates both carry the whole book
            console.log('bid', message.data.bestBid, 'ask', message.data.bestAsk)
            break
        case 'trades': //snapshot: the latest fills, newest first
            console.log('recent fills', message.trades.length)
            break
        case 'trade': //update: one fill
            console.log('fill', message.trade.sold, 'of', message.trade.soldAsset)
            break
        case 'error':
            console.error('request', message.id, 'failed:', message.error)
            break
    }
}

ws.onclose = () => {
    //every subscription is gone: reconnect with a backoff, then subscribe again
}

In an application, use AxisStreamClient from the JS client instead. It reconnects with a backoff, resubscribes, routes messages by topic and pings the server. The Axis and AxisAccount classes build on it to keep markets and accounts current, see Axis, markets and accounts.