API Gateway (REST/WebSocket)¶
Learning objectives
After reading this page you will understand:
- What
pm-api-gwydoes in the EduMatcher process model - How to configure API keys in the central
engine_config.yaml - How to call the REST API and inspect Swagger documentation
- How private and public WebSocket streams work
- Where to find reusable Python and C REST examples
What this process is¶
pm-api-gwy exposes EduMatcher order entry, order management, reference
data, history, and market data over REST/JSON and WebSocket. It is intended for
third-party software: browser UIs, dashboards, simple bots, and teaching
examples.
It is not a second matching engine. The process translates HTTP and WebSocket
requests into the same engine ZMQ/JSON messages used by the interactive
pm-alf-console process.
flowchart LR
UI[Trading UI] -->|REST /api/v1| API[pm-api-gwy]
BOT[Bot] -->|REST /api/v1| API
UI -->|WS /api/v1/events| API
DASH[Dashboard] -->|WS /api/v1/market-data| API
API -->|ZMQ PUSH :5555| ENG[pm-engine]
ENG -->|ZMQ PUB :5556| API
STATS[pm-stats\nstats.db] -->|read-only history| API
Configuration¶
API gateway configuration lives in the central engine_config.yaml, matching
the existing CALF and RALF gateway pattern.
Use the top-level key api_gateways (underscore). The dashed form
api-gateways is not valid.
api_gateways:
desk:
enabled: true
host: 127.0.0.1
port: 8080
swagger_enabled: true
log_level: info
stats_db: data/stats.db
credentials:
- api_key: key-trader-demo
gateway_id: TRADER01
description: Demo trading client
- api_key: key-dashboard-demo
gateway_id: null
description: Read-only dashboard client
rate_limit:
writes_per_second: 10
burst: 20
timeouts:
engine_auth_sec: 3.0
engine_reply_sec: 3.0
wait_ack_sec: 3.0
| Field | Meaning |
|---|---|
api_gateways.<NAME> |
Named API gateway process configuration selected with --instance NAME when needed |
host / port |
HTTP server bind address and port |
swagger_enabled |
Enables /docs and /openapi.json when true |
credentials[].api_key |
Bearer token clients use for REST and WebSocket auth |
credentials[].gateway_id |
Engine gateway identity; null means read-only market-data access; non-null values must be unique across api_gateways entries |
rate_limit |
Per-key write limiting for POST/PATCH/DELETE endpoints |
timeouts |
Engine auth, request/reply, and synchronous ACK wait timeouts |
The engine's gateways.alf allowlist remains authoritative. If a credential
maps to TRADER01 but TRADER01 is not allowed by the engine config, the
engine rejects the API gateway handshake.
Use multiple named entries when you want logical process separation, such as one
gateway for a human trading desk and another for automated clients. Each
non-null gateway_id is owned by one API gateway process so process-local
session and event state remain unambiguous. Read-only gateway_id: null
credentials can appear in more than one entry.
Start the process¶
Installed mode:
Developer mode:
poetry run pm-engine --verbose
poetry run pm-stats
poetry run pm-api-gwy --config engine_config.yaml --instance desk
Useful options:
| Option | Default | Description |
|---|---|---|
--host ADDR |
config value | Override HTTP bind address |
--port PORT |
config value | Override HTTP listen port |
--instance NAME |
auto-selected only when one entry exists | Select a named api_gateways entry |
--config PATH |
EDUMATCHER_CONFIG resolution |
Central engine config path |
--engine-host HOST |
config value | Override engine host for ZMQ ports 5555/5556 |
--stats-db PATH |
config value | SQLite database for /history/* |
--log-level LEVEL |
config value | debug, info, warning, or error |
Uvicorn writes access and application logs to stdout/stderr. Redirect them with your shell or service manager:
poetry run pm-api-gwy --config engine_config.yaml --instance desk --log-level debug \
> api-gateway.log 2>&1
Swagger interface¶
When swagger_enabled: true, open:
Swagger shows all REST endpoints, request schemas, response schemas, and enum values. Use the Authorize button with:
Authentication principles¶
REST clients send an HTTP bearer token:
WebSocket clients send the API key as their first JSON message:
Read-only credentials (gateway_id: null) can use /api/v1/market-data but
cannot submit, cancel, or inspect private orders.
REST endpoints¶
Base path: /api/v1.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/orders |
trading | Submit one order |
DELETE |
/orders/{order_id} |
trading | Cancel one order |
PATCH |
/orders/{order_id} |
trading | Amend price and/or quantity |
POST |
/orders/{order_id}/replace |
trading | Cancel then submit replacement |
GET |
/orders |
trading | List live orders for the gateway |
GET |
/orders/{order_id} |
trading | Read cached order state |
POST |
/oco |
trading | Submit OCO pair |
DELETE |
/oco/{oco_id} |
trading | Cancel OCO pair |
POST |
/combos |
trading | Submit combo order |
DELETE |
/combos/{combo_id} |
trading | Cancel combo |
POST |
/quotes |
trading | Submit two-sided quote |
DELETE |
/quotes/{symbol} |
trading | Cancel quote for symbol |
POST |
/mass-cancel |
trading | Cancel all or symbol-scoped exposure |
POST |
/kill-switch |
trading | Alias of /mass-cancel |
GET |
/symbols |
trading | Instrument metadata |
GET |
/session |
trading | Current engine session state |
GET |
/quotes/bootstrap |
trading | Active quote bootstrap state |
GET |
/quotes/legs |
trading | Quote leg state |
GET |
/positions |
trading | Net positions by symbol |
GET |
/status |
trading | Gateway cache summary |
GET |
/history/orders |
trading | Historical order lifecycle events |
GET |
/history/orders/{order_id} |
trading | Full lifecycle for one order |
GET |
/history/fills |
trading | Historical fills |
GET |
/history/trades |
any valid key | Public trade log |
GET |
/history/daily |
any valid key | Daily OHLCV rows |
GET |
/healthz |
none | Liveness probe (not in Swagger) |
Submit order¶
{
"symbol": "AAPL",
"side": "BUY",
"order_type": "LIMIT",
"quantity": 100,
"tif": "DAY",
"price": 150.50,
"smp_action": "NONE",
"client_order_id": "ui-42"
}
| Field | Required | Notes |
|---|---|---|
symbol |
yes | Instrument symbol |
side |
yes | BUY or SELL |
order_type |
yes | MARKET, LIMIT, STOP, STOP_LIMIT, FOK, ICEBERG, IOC, TRAILING_STOP |
quantity |
yes | Positive integer |
tif |
no | DAY, GTC, ATO, ATC; default DAY |
price |
conditional | Required for LIMIT, FOK, IOC, ICEBERG, STOP_LIMIT |
stop_price |
conditional | Required for STOP, STOP_LIMIT |
visible_qty |
conditional | Required for ICEBERG, less than quantity |
trail_offset |
conditional | Required for TRAILING_STOP |
smp_action |
no | Self-match prevention action |
Default write calls return immediately with 202 Accepted. Add ?wait=ack to
wait for the matching engine ACK until the configured timeout. The wait filters
by order_id so concurrent requests on the same gateway receive their own ack.
Submitting an order with a client_order_id that already exists in the session
cache returns 409 Conflict.
Cancel, amend, and replace¶
| Operation | Payload |
|---|---|
DELETE /orders/{order_id} |
no body |
PATCH /orders/{order_id} |
{ "price": 151.00 }, { "quantity": 200 }, or both |
POST /orders/{order_id}/replace |
same shape as POST /orders |
OCO, combos, quotes, and mass cancel¶
| Endpoint | Minimal payload |
|---|---|
POST /oco |
{ "oco_id":"tp-sl-1", "symbol":"AAPL", "quantity":100, "leg1":{"side":"SELL","order_type":"LIMIT","price":152.0}, "leg2":{"side":"SELL","order_type":"STOP","stop_price":147.0} } |
POST /combos |
{ "combo_id":"spread-1", "legs":[{"symbol":"AAPL","side":"BUY","quantity":100,"price":150.0},{"symbol":"MSFT","side":"SELL","quantity":100,"price":410.0}] } |
POST /quotes |
{ "symbol":"AAPL", "bid_price":150.0, "bid_qty":500, "ask_price":150.1, "ask_qty":500 } |
POST /mass-cancel |
{ "symbol":"AAPL" } or {} for all symbols |
WebSocket endpoints¶
| Path | Purpose | First message |
|---|---|---|
/api/v1/events |
Private order/quote/risk lifecycle events for one gateway | { "api_key": "key-trader-demo" } |
/api/v1/market-data |
Public book, trade, depth, session, and circuit-breaker events | { "api_key": "key-dashboard-demo" } |
Private event envelope:
{
"type": "order.fill",
"ts": "2026-06-24T10:15:03.221Z",
"gateway_id": "TRADER01",
"data": {
"order_id": "ORD-...",
"fill_qty": 50,
"fill_price": 150.50,
"remaining_qty": 50,
"status": "PARTIAL"
}
}
Market-data subscription control:
Session and circuit-breaker events are always delivered after authentication.
Python REST example¶
from api_gateway_client import ApiGatewayClient
client = ApiGatewayClient("http://127.0.0.1:8080", "key-trader-demo")
print(client.get_json("/api/v1/status"))
print(client.get_json("/api/v1/symbols"))
Runnable examples live under docs/examples/REST/python/.
C REST example¶
The C example uses a small POSIX socket helper for simple HTTP GET calls:
ApiGatewayClient client = api_gateway_client("127.0.0.1", 8080, "key-trader-demo");
char *body = api_gateway_get(&client, "/api/v1/status");
puts(body);
free(body);
Runnable examples live under docs/examples/REST/c/.
Implementation notes and design deviations¶
The original API gateway design described a separate api_gateway_config.yaml.
EduMatcher now keeps API gateway settings in the central engine_config.yaml
under api_gateways: so the API gateway follows the same configuration pattern
as the other gateway processes and supports multiple named API gateway process
configs.
The runtime rejects duplicate non-null gateway_id assignments across named
API gateway entries. This is deliberate: sharing one engine gateway identity
between two API gateway processes would split private session/event state across
process memory. Use separate ALF gateway IDs for separately managed write paths,
or use gateway_id: null for read-only dashboard credentials.
Swagger exposure is configurable with swagger_enabled. Plain bearer keys in
YAML are used for teaching and local labs; production deployments should put the
gateway behind TLS and manage secrets with the surrounding platform.
?wait=ack waits for the engine event matching both the topic and the specific
order_id. Concurrent requests sharing one gateway_id each resolve
independently.
engine_auth_sec, engine_reply_sec, and wait_ack_sec are all applied from
configuration.
The implementation keeps engine payloads close to the existing EduMatcher event model. Outbound WebSocket events are wrapped in a consistent envelope, but they do not attempt broad tick-to-display price rewriting beyond the payloads already published by the engine.
Cancel-replace is implemented as cancel, wait for the cancel event, then submit
the replacement. The replacement body uses the same shape as POST /orders,
including symbol.
Startup creates the engine client and listener, but does not fail the process
only because stats.db is absent. History endpoints depend on pm-stats having
created and populated the configured database.
Operational checklist¶
- Confirm
api_gateways.<NAME>.credentialsmaps to gateways allowed undergateways.alf - Confirm each non-null
gateway_idappears in only one API gateway entry - Start
pm-engine,pm-stats, thenpm-api-gwy --instance NAME - Open
/docsif Swagger is enabled - Test
GET /api/v1/healthz(no auth required) — returns{"ok": true}when the engine listener is running - Test
GET /api/v1/statuswith a bearer token - Connect
/api/v1/eventsbefore submitting orders if you want async outcomes - Use
/history/*only whenpm-statsis running and writingstats.db
Minimal MARKET order CLI¶
The script below lives at docs/examples/REST/python/submit_market_order.py
and reuses the same ApiGatewayClient library used by demo_info.py.
Run it from the examples directory:
cd docs/examples/REST/python
python3 submit_market_order.py --side BUY --symbol AAPL --qty 100
python3 submit_market_order.py --side SELL --symbol MSFT --qty 50 --wait-ack
Override gateway URL and key with environment variables:
EDUMATCHER_API_URL=http://127.0.0.1:8080 \
EDUMATCHER_API_KEY=key-trader-demo \
python3 submit_market_order.py --side BUY --symbol AAPL --qty 100
| Option | Required | Default | Description |
|---|---|---|---|
--side |
yes | — | BUY or SELL |
--symbol |
yes | — | Instrument symbol |
--qty |
yes | — | Order quantity |
--wait-ack |
no | off | Block until the matching engine ACKs the order |
--url |
no | $EDUMATCHER_API_URL or http://127.0.0.1:8080 |
Gateway base URL |
--key |
no | $EDUMATCHER_API_KEY or key-trader-demo |
Bearer API key |
Example output without --wait-ack:
Example output with --wait-ack:
order_id : ORD-3a7f1e2c
status : ACKED
accepted : True
engine ack:
{
"order_id": "ORD-3a7f1e2c",
"accepted": true,
"reason": null
}
MARKET orders must not include price or stop_price. The gateway validates
this and returns 400 VALIDATION if either field is present.
Troubleshooting¶
Check whether the port is in use¶
Before starting pm-api-gwy, or when a client cannot connect, verify that
something is listening on the configured port (default 8080).
macOS:
# lsof — shows the process name and PID holding the port
sudo lsof -iTCP:8080 -sTCP:LISTEN
# BSD netstat (ships with macOS)
netstat -an | grep LISTEN | grep 8080
Linux:
# ss — preferred on modern Linux
ss -tlnp 'sport = :8080'
# lsof
sudo lsof -iTCP:8080 -sTCP:LISTEN
# netstat (older distributions)
netstat -tlnp | grep 8080
Replace 8080 with the port value from your api_gateways.<name> config block.
If no output appears, the gateway is not running.
Test HTTP connectivity from the command line¶
The health endpoint requires no authentication and is the fastest connectivity check:
Test an authenticated endpoint:
Test WebSocket connectivity:
# websocat (brew install websocat / apt install websocat)
websocat ws://127.0.0.1:8080/api/v1/market-data
# curl — look for HTTP 101 Switching Protocols in the response headers
curl -v --no-buffer \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlc2FtcGxla2V5" \
http://127.0.0.1:8080/api/v1/market-data
Common problems¶
| Symptom | Likely cause | Fix |
|---|---|---|
Connection refused |
Gateway not started or wrong port | Confirm pm-api-gwy is running; check port in api_gateways config |
{"ok": false} from /healthz |
Engine ZMQ connection not established | Start pm-engine first |
401 Unauthorized |
Missing or wrong Authorization header |
Use Authorization: Bearer <key> with a key listed in credentials |
403 Forbidden |
Credential has no gateway_id; endpoint requires one |
Use a credential with a non-null gateway_id for order-entry endpoints |
404 on all endpoints |
Wrong base path or wrong --instance flag |
Check pm-api-gwy --instance NAME matches the config block name |
| Swagger UI not loading | swagger_enabled: false |
Set swagger_enabled: true in the config block and restart |
| History endpoints return empty results | pm-stats not running or wrong stats_db path |
Start pm-stats; verify the stats_db path in config points to the correct file |
| WebSocket disconnects immediately | Engine not running or client rate limit hit | Start engine; check gateway logs for disconnect reason |