API reference¶
Base URL for bots: http://api.prayog.localhost (the web app uses /api on its own host). All bodies are JSON.
Prices are integer paise; times are sim time in epoch microseconds. Authenticated calls need
Authorization: Bearer <token>; X-Prayog-Account: <label> picks the account (default main).
Public (no token)¶
| Method and path | Returns |
|---|---|
GET /api/v1/instruments |
[{symbol, tickSize, maxOrderQuantity, referencePrice, bandPercent, bandLow, bandHigh}] |
GET /api/v1/session |
{state, simTime, clockMultiplier, open, close, offset} |
GET /api/v1/market/tickers |
[{symbol, referencePrice, last, open, high, low, volume, trades, bestBid, bestAsk}] |
GET /api/v1/market/{symbol}/book?depth=20 |
The same snapshot message the WebSocket sends |
GET /api/v1/market/{symbol}/trades?limit=200 |
Recent trade messages, oldest first (up to 5,000) |
GET /actuator/health/readiness |
{"status":"UP"} when the exchange is ready |
GET /actuator/prometheus |
Metrics: prayog_orders_total{kind,outcome}, prayog_order_latency_seconds, prayog_ring_remaining, ... |
Trading (role trader or bot)¶
| Method and path | Body | Returns |
|---|---|---|
GET /api/v1/me |
{subject, accountLabel, accountId, clientId, roles} |
|
POST /api/v1/orders |
{symbol, side: BUY\|SELL, type: LIMIT\|MARKET, price (LIMIT only), quantity, clientOrderId?} |
201 + OrderResult |
DELETE /api/v1/orders/{orderId} |
OrderResult (cancelled or rejected) |
|
PATCH /api/v1/orders/{orderId} |
{price, quantity (new TOTAL), clientOrderId?} |
OrderResult (modified, cancelled or rejected) |
GET /api/v1/orders |
Open orders: [{orderId, clientOrderId, symbol, side, price, quantity, leavesQuantity, filledQuantity}] |
|
DELETE /api/v1/orders |
{requested, cancelled}: cancels all your open orders |
OrderResult: {status, orderId, clientOrderId, symbol, reason, filledQuantity, leavesQuantity, fills:
[{tradeId, price, quantity}], inputSeq, simTime}.
status |
Meaning |
|---|---|
resting |
On the book with leavesQuantity open (maybe partly filled already) |
filled |
Completely filled now |
cancelled |
Nothing left open; reason says why (NO_LIQUIDITY, SELF_TRADE_PREVENTION, CLIENT_REQUEST, MODIFIED_TO_ZERO) |
modified |
The modify was applied (it may also have traded: see fills) |
rejected |
Refused; reason: UNKNOWN_SYMBOL, INVALID_PRICE, INVALID_QUANTITY, PRICE_NOT_ON_TICK, PRICE_OUTSIDE_BAND, SESSION_NOT_OPEN, UNKNOWN_ORDER, DUPLICATE_CLIENT_ORDER_ID, ACCOUNT_DISABLED |
Safe retries: if a POST /orders times out you cannot know whether it arrived. Send it again with the same
clientOrderId: if the first one got through, the retry is rejected with DUPLICATE_CLIENT_ORDER_ID and no second
order exists (check GET /orders). IDs are unique per account per trading day, for accepted orders only.
Errors¶
{ "error": "...", "message": "..." } with: 400 bad_request (missing or invalid field), 401 (no or bad token:
WWW-Authenticate says why), 403 (wrong role), 404 not_found (unknown symbol, or no open order with that id for
this account), 429 rate_limited, 503 busy (exchange queue full; retry).
Rate limits (token bucket per account, ADR 0020): people 20 orders/s (burst
40), bots 50/s (burst 100), the exchange's own simulated traders 500/s. Over the limit: 429; wait and retry.
Market-data WebSocket (public)¶
ws://api.prayog.localhost/api/v1/ws/market?symbols=INFY,TCS&depth=20 (all symbols if symbols is omitted)
type |
Fields |
|---|---|
snapshot |
symbol, seq, session, bids: [{price, quantity, orders}], asks: [...], trades: [trade...], ticker |
book |
symbol, seq, changes: [{side, price, quantity, orders}] (quantity 0 removes the level) |
trade |
symbol, seq, tradeId, price, quantity, aggressor: BUY\|SELL, simTime |
session |
state: OPEN\|HALTED\|CLOSED, simTime |
heartbeat |
simTime (every 5 s) |
seq is per symbol and increases by exactly 1 per book or trade message after the snapshot.
Private WebSocket (role trader or bot)¶
ws://api.prayog.localhost/api/v1/ws/private?account=<label> with the bearer token in the Authorization header or
as &access_token=....
type |
Fields |
|---|---|
hello |
accountId, accountLabel (first message) |
order |
status: accepted\|rejected\|cancelled\|modified, eventSeq, simTime, orderId, clientOrderId?, symbol, side?, orderType?, price?, quantity?, leavesQuantity?, reason? |
fill |
eventSeq, simTime, tradeId, orderId, symbol, side (yours), price, quantity, aggressor (true if your order took liquidity) |
heartbeat |
simTime |
Updates sent while you were disconnected are not replayed: reconcile with GET /api/v1/orders after reconnecting.
Operations (role ops)¶
| Method and path | Body | Effect |
|---|---|---|
GET /api/v1/ops/status |
Recovery info, last input seq, ring capacity and free slots, publish errors, sim time, session, Kafka publisher (kafka: enabled, connected, publishedSeq, lag, errors) |
|
POST /api/v1/ops/session |
{state: OPEN\|HALTED\|CLOSED} |
Global kill switch; HALTED allows cancels only; CLOSED expires all orders |
POST /api/v1/ops/accounts/{accountId} |
{enabled: false} |
Per-account kill switch: cancels its orders, refuses new ones |
PUT /api/v1/ops/clock |
{multiplier: 1..10000} |
Sim speed |
POST /api/v1/ops/clock/next-open |
Jump to the next day's open | |
POST /api/v1/ops/snapshot |
Take an engine snapshot now (ADR 0016); returns its inputSeq |
Account history and P&L (post-trade, role trader or bot)¶
Served by the post-trade service on the same hosts. Amounts are paise. Use X-Prayog-Account like on the exchange.
| Method and path | Returns |
|---|---|
GET /api/v1/account/pnl |
{accountId, label, realisedPnl, unrealisedPnl, charges, netPnl, trades, rank, positions: [{symbol, quantity, averagePrice, cost, mark, realisedPnl, unrealisedPnl, charges, netPnl, bought, sold, trades}]} |
GET /api/v1/account/fills?limit=100 |
Newest first: {tradeId, orderId, symbol, side, price, quantity, charges, realisedPnl, simTime, eventId} |
GET /api/v1/account/orders?limit=100 |
Newest first: {orderId, clientOrderId, symbol, side, orderType, price, quantity, filledQuantity, leavesQuantity, status (open\|partially_filled\|filled\|cancelled), reason, createdAt, updatedAt} |
GET /api/v1/account/rejections?limit=100 |
Refused new orders: {eventId, clientOrderId, symbol, reason, simTime} |
GET /api/v1/leaderboard?limit=20 |
Public. {accounts, entries: [{rank, accountId, name, netPnl}]}; name is username/label once that account has looked itself up |
GET /api/v1/post-trade/status |
Role ops. Ledger totals (trades, accounts, P&L before charges and net quantity per symbol, both 0 in a healthy ledger) and consumer progress per partition |
P&L uses the average-cost method; charges are brokerage 0.03% capped at ₹20 per fill plus 0.0035% fees (ADR 0015). The ledger follows the event stream, so it lags the exchange by milliseconds normally, longer after an outage.
Event stream (Kafka)¶
Every exchange event, in the shape of contracts/schemas/events/exchange-event.schema.json, on topic
prayog.exchange.events.v1 (3 partitions). From containers use kafka:9092, from the host localhost:9094.
- Key: the symbol, so one symbol's events are in order on one partition. Session-wide events
(
SessionStateChanged) use the empty key. - Event id:
seq, also in theeventIdheader. Delivery is at least once: skip any id you have already applied. - Order: by
seqwithin a partition; across partitions, sort byseqif you need it.