API Gateway REST/WebSocket Training¶
Objective¶
Train on configuring and using pm-api-gwy for REST order entry, reference-data
and history queries, admin operations, Swagger/OpenAPI exploration, WebSocket
event handling, and multi-process logical separation.
You will practice:
- generating
api_gatewaysconfig with bearer credentials - starting a named API gateway process
- using REST endpoints with trading and read-only keys
- querying reference data and order/trade history over REST
- exercising admin REST endpoints with a resolved
ADMINrole - receiving private and market-data events over WebSocket
- splitting API gateway processes by ALF
gateway_id
Background reading
Prerequisites¶
- Chapters 01-24 completed.
- Engine and stats commands available from the current environment.
- REST examples available in
docs/examples/REST. On a container orpipxinstall that directory is inside the package: setEX=$(python -c "from edumatcher.config_deploy import examples_root; print(examples_root().parent)")and readdocs/examplesas$EXin Exercises 6, 7 and 12. Exercise 7's C example is optional: it needsmakeand a C compiler, neither of which is installed in the container image. Exercise 6's Python client provides the equivalent API workflow. - The
curlcommands below assume the lab config from Exercise 1 and are run from the directory holdingengine_config.yaml. - A terminal for
pm-engine, one forpm-stats, one forpm-api-gwy, and one or more client terminals. - Optional:
pm-auditrunning, if you want to see a populated reply instead ofAUDIT_INDEX_UNAVAILABLEfrom the audited order-lifecycle call in Exercise 10. - This chapter is a hands-on tour of the normative REST and WebSocket API Reference — keep it open alongside this chapter for the full endpoint-by-endpoint contract (arguments, replies, and error codes) behind every call made here.
Recommended startup order:
- Generate
engine_config.yaml. - Start
pm-engine --verbose. - Start
pm-stats. - Start
pm-api-gwy --instance desk.
Background¶
pm-api-gwy is an HTTP and WebSocket wrapper around the matching engine. It
uses bearer tokens from api_gateways.<NAME>.credentials. A credential with a
non-null gateway_id can submit and manage orders for that ALF gateway. A
credential with gateway_id: null is read-only and can use the session, halts,
reference, market-data, history, and public WebSocket paths.
Multiple API gateway processes are configured with top-level api_gateways:
api_gateways:
desk:
port: 8080
credentials:
- api_key: key-trader-demo
gateway_id: TRADER01
algos:
port: 8081
credentials:
- api_key: key-algo-demo
gateway_id: ALGO01
A non-null gateway_id may appear in only one api_gateways entry. This keeps
private session and event state owned by one API gateway process. Read-only
gateway_id: null credentials may appear in multiple entries.
Authentication model: API key → gateway_id → engine role¶
Every REST and WebSocket call carries one bearer token:
Authorization: Bearer <api_key>. The gateway looks the key up in its own
credential table and resolves it to a session holding just api_key,
gateway_id, and description — nothing more. Three access levels follow
directly from that one resolved gateway_id:
Resolved gateway_id |
Access |
|---|---|
null |
Read-only: session, halts, reference data, history, public market data, /healthz |
a configured ALF gateway_id |
Trading: everything read-only gets, plus order/quote/combo/OCO submission and cancellation scoped to that one gateway |
a configured ALF gateway_id whose engine role is ADMIN |
Everything trading gets, plus every /api/v1/admin/* endpoint |
The important detail is the third row: the API gateway does not store an
"is admin" flag on the credential itself. It asks the engine what role the
resolved gateway_id has, and only accepts ADMIN for admin endpoints. This
is why OPS01:ADMIN in Exercise 1 configures the role on the ALF gateway,
not on the API key — the key is only a pointer to that gateway identity.
See Appendix: REST API Reference — Auth and roles for the normative statement of this model, including how keys are provisioned.
Exercise 1: Generate a Single API Gateway Config¶
Generate a local lab config with trading keys and one read-only dashboard key.
--sessions-enabled lets Exercise 10 drive the session, and --cb-levels L1:0.05:30
gives every symbol a 5% circuit-breaker level with a 30-minute halt, which the
admin halt in the same exercise needs (a symbol with no circuit breaker
configured refuses it with HALT_REJECTED, AAPL has no circuit breaker configured):
pm-config-gen \
--symbols AAPL MSFT \
--participants TRADER01 TRADER02 OPS01:ADMIN \
--outstanding-shares AAPL:15400000000 \
--outstanding-shares MSFT:7430000000 \
--sessions-enabled \
--cb-levels L1:0.05:30 \
--api-gateway \
--api-gateway-name desk \
--api-key key-trader-demo:TRADER01 \
--api-key key-trader02-demo:TRADER02 \
--api-key key-ops-demo:OPS01 \
--api-key key-readonly-demo:null \
--no-api-gateway-generate-keys \
--api-gateway-host 127.0.0.1 \
--api-gateway-port 8080 \
--seed 20260624 \
--output engine_config.yaml --force
Back up, check, deploy — then restart
This chapter starts from a fresh configuration (its operator is OPS01),
and --force replaces your engine_config.yaml: save a copy first if you
want to come back to it (cp engine_config.yaml engine_config.before-ch25.yaml).
As always, nothing changes until you deploy it and restart the engine:
pm-cverifier engine_config.yaml, then pm-config-deploy engine_config.yaml.
Inspect the generated section:
Expected behavior:
- top-level
api_gateways:exists desk:hasport: 8080- credentials exist for
TRADER01,TRADER02,OPS01, and one read-only key
For this training exercise, the command supplies fixed, easy-to-type demo
keys. Define shell variables once in each terminal you use for curl:
KEY_TRADER=key-trader-demo
KEY_OPS=key-ops-demo # OPS01's resolved engine role is ADMIN
KEY_RO=key-readonly-demo
You can also use the Config GUI's API subtab to generate keys for selected participants and choose the generated key length.
Exercise 10 uses $KEY_OPS for every admin REST call. Where a tool cannot
expand shell variables (the WebSocket sessions in Exercise 9), enter the
matching demo key shown above.
Checkpoint: the config contains the
key-trader-demo, key-ops-demo, and key-readonly-demo credentials bound to
the expected gateway identities.
Exercise 2: Start the API Gateway Process¶
Start the core processes with the generated config:
If the config contains only one api_gateways entry, --instance can be omitted.
Use it anyway in labs so the selected process is explicit.
Sessions are enabled, so the engine starts in CLOSED and refuses orders until
the day is opened. Open it from the operator console (pm-admin --id OPS01,
then SESSION|STATE=PRE_OPEN and SESSION|STATE=CONTINUOUS) before Exercise 4;
Exercise 10 shows the REST equivalent.
Expected behavior:
pm-api-gwybinds to127.0.0.1:8080- startup fails if
api_gateways.desk.enabledisfalse - startup fails if the same non-null
gateway_idis assigned to two named entries
Checkpoint: GET /api/v1/status is reachable once the process is running.
Exercise 3: Explore Swagger with a Bearer Key¶
Open Swagger in a browser:
Use the Authorize control and enter the trading key key-trader-demo. Then
run:
Try the same request with a read-only key, then try a write endpoint with the read-only key.
Expected behavior:
- status works for a trading key and answers
403 READ_ONLYfor a read-only key (it reports the caller's gateway, which a read-only key does not have) - order entry rejects a read-only key
- Swagger lists the same route shapes exposed by
/openapi.json
Checkpoint: you can authenticate in Swagger and explain which key is allowed to submit orders.
Exercise 4: Submit and Cancel an Order over REST¶
Submit a limit order with a trading key:
curl -X POST http://127.0.0.1:8080/api/v1/orders \
-H "Authorization: Bearer $KEY_TRADER" \
-H 'Content-Type: application/json' \
-d '{
"symbol": "AAPL",
"side": "BUY",
"order_type": "LIMIT",
"quantity": 100,
"price": 140.00,
"tif": "DAY"
}'
The price is well below AAPL's market on purpose, so the order rests instead
of trading. Capture the returned order_id, then cancel it with a request tag:
curl -X DELETE 'http://127.0.0.1:8080/api/v1/orders/ORDER_ID?request_tag=rest-cxl-001' \
-H "Authorization: Bearer $KEY_TRADER"
Expected behavior:
- the submit call returns an accepted response or an engine validation error
- a valid cancel request targets the authenticated credential's
gateway_id request_tagis echoed on the cancel response/event and identifies this request, not the original order- synchronous validation errors include both API
error.codeand canonicalerror.reject_code; when supplied,client_tagorrequest_tagis echoed in the error body - read-only keys cannot submit or cancel orders
Checkpoint: you can submit an order and observe its cancel path or explain the engine-side validation error.
Exercise 5: Query Reference Data¶
Reference-data endpoints expose compiled, mostly-static configuration: tick sizes, risk bands, index definitions, and the session schedule. They change only when an admin reloads the reference bundle (Exercise 10) — never per trade or per order.
Fetch the full bundle in one call:
Then fetch the individual sections used most often:
curl -H "Authorization: Bearer $KEY_RO" \
http://127.0.0.1:8080/api/v1/reference/symbols
curl -H "Authorization: Bearer $KEY_RO" \
http://127.0.0.1:8080/api/v1/reference/risk
curl -H "Authorization: Bearer $KEY_RO" \
http://127.0.0.1:8080/api/v1/reference/schedule
curl -H "Authorization: Bearer $KEY_RO" \
http://127.0.0.1:8080/api/v1/reference/config-version
Every reference reply carries config_version — a content hash of the
compiled bundle. Compare the value returned by /reference/config-version
against the one embedded in /reference/symbols; they must match. A client
can poll this one small value instead of every field to notice a config
change.
Expected behavior:
- a read-only key can call every
/reference/*endpoint tick_decimalsforAAPL/MSFTin/reference/symbolsmatches what you passed topm-config-genconfig_versionis identical across every/reference/*reply taken at the same point in time
Checkpoint: you can explain the difference
between GET /api/v1/reference (one round-trip) and the per-section
endpoints, and what config_version is for.
Exercise 6: Use the Python REST Example¶
From the REST example directory, run the Python client or adapt it with the key and port from your config:
cd docs/examples/REST/python # or: cd $EX/REST/python
EDUMATCHER_API_URL=http://127.0.0.1:8080 \
EDUMATCHER_API_KEY=$KEY_TRADER \
python3 demo_info.py
Use the example source as a reference for adding status, order-entry, and cancel calls to a test harness.
Checkpoint: the Python client reaches the gateway and sends authenticated requests.
Optional Exercise 7: Use the C REST Example¶
Build and run the C example from the REST example directory:
Expected behavior:
- the client sends an Authorization header
- status or order-entry responses are printed as JSON
- connection errors identify an unavailable gateway process or wrong port
Optional checkpoint: the C example can call the running API gateway with a configured key.
Exercise 8: Query Order and Trade History¶
History endpoints read from pm-stats's database, so pm-stats must be
running and must have been running while your orders and trades happened —
it cannot reconstruct events from before it started.
Submit and fill an order (repeat Exercise 4 with a marketable price, or cross
TRADER01 against TRADER02), then query its lifecycle with a trading key:
curl -H "Authorization: Bearer $KEY_TRADER" \
"http://127.0.0.1:8080/api/v1/history/orders?symbol=AAPL&limit=10"
curl -H "Authorization: Bearer $KEY_TRADER" \
http://127.0.0.1:8080/api/v1/history/orders/ORDER_ID
curl -H "Authorization: Bearer $KEY_TRADER" \
"http://127.0.0.1:8080/api/v1/history/fills?symbol=AAPL"
The public trade tape and daily OHLCV accept any authenticated key, not only a trading key:
curl -H "Authorization: Bearer $KEY_RO" \
"http://127.0.0.1:8080/api/v1/history/trades?symbol=AAPL&limit=5"
curl -H "Authorization: Bearer $KEY_RO" \
"http://127.0.0.1:8080/api/v1/history/daily?symbol=AAPL"
Walk a second page using the cursor from the first reply:
curl -H "Authorization: Bearer $KEY_RO" \
"http://127.0.0.1:8080/api/v1/history/trades?symbol=AAPL&limit=5&after=NEXT_CURSOR"
Every history call returns rows oldest first, a page at a time. On a data
directory that has seen earlier chapters the first page is therefore old
traffic, not your new order, and /history/daily totals include everything
pm-stats has recorded today. Look an order up by its id
(/history/orders/ORDER_ID), narrow the range with from=<ISO timestamp>
(or date=YYYY-MM-DD), or follow next_cursor to reach the recent rows.
Expected behavior:
/history/ordersand/history/fillsare scoped to the caller's own gateway —TRADER01's key never seesTRADER02's orders/history/tradesand/history/dailyare public and show both sides- every page carries
next_cursor: a string while"has_more": true(pass it back unchanged asafterfor the next page) andnullon the last page - if
pm-statsis not running, every history call returns503 STATS_DB
Checkpoint: you can explain why
/history/orders is gateway-scoped but /history/trades is not.
Exercise 9: Observe WebSocket Events¶
Connect to private events before submitting new orders:
Once connected, the CLI waits for you to type. Send the authentication message as your first input, then press Enter:
> {"api_key": "key-trader-demo"}
< {"type": "authenticated", "gateway_id": "TRADER01", "stream_seq": 10}
< {"type": "orders.snapshot", "gateway_id": "TRADER01", ...}
(stream_seq is the gateway's private-stream counter, so the number differs;
the orders.snapshot that follows lists your current orders and positions
before any live event.)
In another terminal, submit or cancel an order with the same key. Observe the private event stream.
Then connect to a public market-data WebSocket with a read-only key:
After authentication, send a subscription control message:
> {"api_key": "key-readonly-demo"}
< {"type": "authenticated"}
> {"action": "subscribe", "symbols": ["AAPL"], "channels": ["book", "trades"]}
< {"type": "subscription", "data": {"items": [...], "always": ["session", "circuit_breaker", "news"], "rejected": []}}
Each rule can have its own symbols, so one socket can carry an overview plus a focused instrument:
> {"action": "subscribe", "items": [
{"symbols": ["*"], "channels": ["book", "trades"]},
{"symbols": ["AAPL"], "channels": ["depth"]}
]}
The book is also available without a socket. Compare it with what the subscription just sent:
curl -s -H "Authorization: Bearer $KEY_RO" \
"http://127.0.0.1:8080/api/v1/book/AAPL?levels=3"
curl -s -H "Authorization: Bearer $KEY_RO" http://127.0.0.1:8080/api/v1/book
Each price comes as a display price and as *_ticks. The reply's seq is the
seq of the book frame for the same snapshot, so a client that reads REST
first and then subscribes can drop frames at or below it.
Expected behavior:
GET /book/AAPLmatches thebookframe the subscription sent, with the sameseqwhen the book has not changed sinceGET /book/NOSUCHanswers404 UNKNOWN_SYMBOL- private events correspond to the authenticated trading gateway
- public market-data access does not require a non-null
gateway_id - stale or unknown bearer keys are rejected
- every event carries
topicand a per-topicseq; a jump inseqfor a topic means your client read too slowly and events were dropped session,circuit_breakerandnewsarrive whether or not you subscribed — the ack lists them underalways- an
order.fillevent'sdata.liquidity_flagtells you whether your order supplied liquidity (MAKER, it was resting) or removed it (TAKER, it crossed the spread) — no join against another feed required
Checkpoint: you can explain when to use REST responses versus WebSocket events for order outcomes.
Exercise 10: Admin REST — Session Control, Risk, and Cross-Gateway Orders¶
Confirm the resolved role first, with OPS01's key from Exercise 1:
gateway_role must read ADMIN; the reply also includes gateway_count, a
field only ADMIN callers get back. Now try the same idea with a trading key:
Expect 403 ROLE_DENIED — a trading key is never accepted on /admin/*,
regardless of which gateway it is bound to.
Trigger and resume a circuit-breaker halt with the ADMIN key. The symbol is
part of the path; the body carries only the optional level (and reason):
curl -X POST http://127.0.0.1:8080/api/v1/admin/symbols/AAPL/halt \
-H "Authorization: Bearer $KEY_OPS" \
-H 'Content-Type: application/json' \
-d '{"level": "L1"}'
curl -H "Authorization: Bearer $KEY_OPS" \
http://127.0.0.1:8080/api/v1/halts
curl -X POST http://127.0.0.1:8080/api/v1/admin/symbols/AAPL/resume \
-H "Authorization: Bearer $KEY_OPS"
Request a session transition. This call waits for the engine's verdict
instead of firing and forgetting. If you have not opened the day yet, send
PRE_OPEN first; the engine only accepts the legal next phase:
curl -i -X POST http://127.0.0.1:8080/api/v1/admin/session/transition \
-H "Authorization: Bearer $KEY_OPS" \
-H 'Content-Type: application/json' \
-d '{"to_state": "CONTINUOUS"}'
A successful transition returns 200 with status: "APPLIED" and a
command_id. Requesting a state the engine cannot apply because sessions are
disabled returns 409 TRANSITION_REJECTED with the engine's reason as
message. A transition that is simply not legal from the current phase
(for example CONTINUOUS when the market is already CONTINUOUS) gets no
answer from the engine at all: the call waits about three seconds and ends
with 503 ENGINE_TIMEOUT. Check GET /api/v1/session first, as for the
console.
Look at cross-gateway order visibility:
This is the only endpoint that shows every gateway's resting orders in one
call; a trading key never sees this. Note the retention_sec field in the
reply — filled/cancelled/expired orders are evicted from this view after
order_retention_sec (default one hour, configurable per api_gateways
instance), while resting orders are never evicted regardless of age.
Fetch one order's full cross-gateway lifecycle from the audit trail:
curl -i -H "Authorization: Bearer $KEY_OPS" \
http://127.0.0.1:8080/api/v1/admin/orders/ORDER_ID/audit
This endpoint is optional: it depends on a separate pm-audit index that
this gateway only reads, never writes. If pm-audit has not built one,
expect 503 AUDIT_INDEX_UNAVAILABLE — every other admin endpoint keeps
working normally.
Finally, cancel resting exposure with the kill-switch family:
curl -X POST http://127.0.0.1:8080/api/v1/admin/symbols/AAPL/cancel-orders \
-H "Authorization: Bearer $KEY_OPS" \
-H 'Content-Type: application/json' \
-d '{"reason": "training exercise"}'
curl -X POST http://127.0.0.1:8080/api/v1/admin/gateways/TRADER01/kill-switch \
-H "Authorization: Bearer $KEY_OPS" \
-H 'Content-Type: application/json' \
-d '{"reason": "training exercise"}'
Expected behavior:
- a trading key gets
403 ROLE_DENIEDon every/admin/*path - halting a symbol the engine does not know returns
409 HALT_REJECTEDwithmessage: "Unknown symbol: ..."— the engine refused, your role is fine /admin/session/transitionblocks for the engine's verdict (applied, or409when sessions are disabled; an illegal next phase ends in503 ENGINE_TIMEOUT)/admin/ordersreturns orders from every gateway, not justOPS01's/admin/orders/{order_id}/auditeither returns the full lifecycle or a clear503 AUDIT_INDEX_UNAVAILABLEif no audit index is deployed
Checkpoint: you can explain why role resolution happens against the engine rather than being a property of the API key itself.
Exercise 11: Configure Multiple Logical API Gateways¶
Generate two API gateway process configs, one for desk trading and one for algorithmic trading:
pm-config-gen \
--symbols AAPL MSFT \
--participants TRADER01 ALGO01 OPS01:ADMIN \
--api-gateway-instance desk:TRADER01:8080 \
--api-gateway-instance algos:ALGO01:8081 \
--seed 20260624 \
--output engine_config.yaml --force
Start each named process in a separate terminal:
Try an invalid duplicate assignment:
pm-config-gen \
--symbols AAPL \
--participants TRADER01 \
--api-gateway-instance desk:TRADER01 \
--api-gateway-instance algos:TRADER01 \
--dry-run
Expected behavior:
desklistens on8080and ownsTRADER01algoslistens on8081and ownsALGO01- duplicate non-null
gateway_idassignment is rejected before runtime
Checkpoint: you can run two API gateway processes and explain why a write-capable gateway ID is globally unique across them.
Exercise 12: Write a Python CLI Client for LIMIT Order Entry¶
Use the ApiGatewayClient library from docs/examples/REST/python to write a
small script that submits a LIMIT order and prints the engine response.
import argparse, json, os, sys
sys.path.insert(0, os.path.join(os.environ.get("EX", "docs/examples"), "REST/python"))
from api_gateway_client import ApiGatewayClient
parser = argparse.ArgumentParser()
parser.add_argument("--side", required=True, choices=["BUY", "SELL"])
parser.add_argument("--symbol", required=True)
parser.add_argument("--qty", required=True, type=int)
parser.add_argument("--price", required=True, type=float)
parser.add_argument("--wait-ack", action="store_true")
args = parser.parse_args()
client = ApiGatewayClient(
os.environ.get("EDUMATCHER_API_URL", "http://127.0.0.1:8080"),
os.environ.get("EDUMATCHER_API_KEY", "key-trader-demo"),
)
path = "/api/v1/orders" + ("?wait=ack" if args.wait_ack else "")
result = client.post_json(path, {
"symbol": args.symbol.upper(),
"side": args.side,
"order_type": "LIMIT",
"quantity": args.qty,
"price": args.price,
})
print(json.dumps(result, indent=2))
Save it as limit_order.py, set EDUMATCHER_API_KEY to a trading key (export EDUMATCHER_API_KEY=$KEY_TRADER;
the built-in default is only a placeholder) and run it from the repo root (or
from anywhere with EX set as above):
python3 limit_order.py --side BUY --symbol AAPL --qty 100 --price 140.00
python3 limit_order.py --side BUY --symbol AAPL --qty 100 --price 140.00 --wait-ack
Expected behavior:
- without
--wait-ack:statusisPENDINGandeventis null - with
--wait-ack:statusisACKEDandeventcontains the engine response - sending a read-only key returns
403 READ_ONLY - omitting
--priceis stopped by the script's own argument parser (error: the following arguments are required: --price) before any request is sent; to see the gateway's400 VALIDATIONfor aLIMITorder withoutprice, post the JSON body withcurland leavepriceout
A fully documented MARKET order version that follows the same pattern is
available at docs/examples/REST/python/submit_market_order.py.
Checkpoint: your script prints order_id and status from a live gateway.
Support Libraries and Example Clients¶
Reference examples used in this training chapter:
docs/examples/REST/pythondocs/examples/REST/c
Use these examples as small integration clients when building course labs, smoke tests, or external adapter prototypes.
Summary¶
You can now:
- Generate one or more
api_gatewaysprocess configs with trading, read-only, and ADMIN-resolved bearer credentials. - Start and reach
pm-api-gwyover REST, Swagger, and WebSocket. - Submit, cancel, and observe orders through REST and private WebSocket events.
- Query the reference-data, history, and admin REST surfaces, and explain what each returns and who is allowed to call it.
- Explain why a write-capable ALF
gateway_idmust be unique across every configuredapi_gatewaysprocess, and why ADMIN access is resolved from the engine rather than stored on the API key.
See REST and WebSocket API Reference for the full normative contract behind every endpoint used in this chapter.
Reflection¶
If two API gateway processes both listed the same gateway_id as a
read-only (gateway_id: null) credential, would that be a problem? Why does
the constraint only apply to non-null gateway_id values?
GET /api/v1/status returns a different gateway_role for the same running
engine depending on which key you authenticate with. Where does that role
actually come from, and what would go wrong if the API gateway instead
trusted a role embedded directly in the key?
Handoff for Chapter 26¶
Before starting 26 — Market Index (pm-index), you can stop
pm-api-gwy; it is not required there. Keep pm-engine and pm-stats
running if you want to reuse the same session, or start fresh — Chapter 26
generates its own config with pm-config-gen.