Using the UCP Simulator API
The simulator (simulator/) exposes a small HTTP + WebSocket API that
5. Using the UCP Simulator API
The simulator (simulator/) exposes a small HTTP + WebSocket API that
you can drive from your own automation: CI pipelines, integration tests,
acceptance tests, even bots that benchmark UCP compliance across
multiple shops.
This page documents the API and shows how to embed the simulator into a CI workflow.
5.1 Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | / | The web UI (3-pane layout) |
GET | /profile.json | The simulator's own UCP platform profile |
GET | /api/scenarios | List all available scenarios + their params |
POST | /api/run | Kick off a scenario run; returns a runId |
GET | /api/runs/:id | Snapshot of a run (trace events + conversation + http exchanges) |
POST | /webhooks/order?business_url=… | Receives signed UCP order webhooks; verifies signature |
GET | /oauth/callback | OAuth 2.0 redirect handler (identity linking handshake) |
WS | /ws | Live event stream: trace, conversation, http, webhook, done |
/profile.json accepts ?ap2=0|1; /api/run sets this automatically per
scenario so standard checkout runs do not negotiate AP2, while AP2 scenarios
do.
The full source for these is in simulator/src/server.ts. There's no
authentication — the simulator is a development tool meant to run on a
trusted network.
5.2 Listing scenarios
curl -s http://localhost:4100/api/scenarios | jq '.scenarios[] | {id, category, requires: .requiresCapabilities}'{"id":"discovery.basic","category":"discovery","requires":[]}
{"id":"discovery.version-pin","category":"discovery","requires":[]}
{"id":"catalog.search.basic","category":"catalog","requires":["dev.ucp.shopping.catalog.search"]}
{"id":"catalog.search.browse","category":"catalog","requires":["dev.ucp.shopping.catalog.search"]}
{"id":"catalog.cursor-pagination","category":"catalog","requires":["dev.ucp.shopping.catalog.search"]}
{"id":"catalog.lookup","category":"catalog","requires":["dev.ucp.shopping.catalog.lookup"]}
{"id":"cart.full-lifecycle","category":"cart","requires":["dev.ucp.shopping.cart"]}
{"id":"cart.multi-item","category":"cart","requires":["dev.ucp.shopping.cart"]}
{"id":"checkout.happy-path","category":"checkout","requires":["dev.ucp.shopping.checkout"]}
{"id":"checkout.cart-conversion-idempotent","category":"checkout","requires":["dev.ucp.shopping.cart","dev.ucp.shopping.checkout"]}
{"id":"extension.discount","category":"extensions","requires":["dev.ucp.shopping.cart","dev.ucp.shopping.discount"]}
{"id":"extension.ap2-mandate","category":"extensions","requires":["dev.ucp.shopping.ap2_mandate"]}
{"id":"extension.ap2-sd-jwt","category":"extensions","requires":["dev.ucp.shopping.ap2_mandate"]}
{"id":"identity.oauth-discover","category":"identity","requires":["dev.ucp.common.identity_linking"]}
{"id":"identity.oauth-initiate","category":"identity","requires":["dev.ucp.common.identity_linking"]}
{"id":"webhook.listener-only","category":"webhook","requires":[]}
{"id":"compliance.full-suite","category":"compliance","requires":[]}5.3 Running a scenario from your code
curl -s -X POST http://localhost:4100/api/run \
-H "Content-Type: application/json" \
-d '{
"businessUrl": "http://localhost:8080",
"transport": "rest",
"scenarioId": "checkout.happy-path",
"params": { "query": "product", "quantity": 2 }
}'{
"runId": "abc123def456",
"profileUrl": "http://localhost:4100/profile.json?ap2=0",
"webhookUrl": "http://host.docker.internal:4100/webhooks/order?business_url=http%3A%2F%2Flocalhost%3A8080"
}Then poll the run for its outcome:
curl -s http://localhost:4100/api/runs/abc123def456 | jq '{status, exchanges: (.exchanges | length), conv: (.conversation | length)}'{ "status": "success", "exchanges": 4, "conv": 8 }5.4 Subscribing to the live event stream
For interactive use (a dashboard, a CI summary that highlights failures
in real time), subscribe to the WebSocket at /ws:
const ws = new WebSocket('ws://localhost:4100/ws')
ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
switch (msg.type) {
case 'trace': console.log('[trace]', msg.payload.step, msg.payload.message); break
case 'http': console.log('[http ]', msg.payload.request.method, msg.payload.request.url, msg.payload.response.status, msg.payload.durationMs + 'ms'); break
case 'conversation': console.log('[chat ]', msg.payload.role, '-', msg.payload.title); break
case 'webhook': console.log('[hook ]', msg.payload.result.ok ? '✓' : '✗', msg.payload.result.reason ?? msg.payload.result.kid); break
case 'done': console.log('[done ]', msg.payload.status); break
}
}5.5 Wrapping the simulator in a CI job
The simulator script that produced the screenshots in this
documentation lives at simulator/scripts/capture-screenshots.mjs and
is a worked example of driving the simulator from Node. The same
approach works as a smoke-test job:
# .github/workflows/ucp-smoke-test.yml
jobs:
ucp-smoke:
runs-on: ubuntu-latest
services:
shopware:
image: dockware/dev:latest
ports: ['8080:80']
env:
UCP_SERVER: 1
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- working-directory: simulator
run: |
npm ci
npm run build
node dist/server.js &
sleep 3
# Run the compliance suite via curl + poll
curl -s -X POST http://localhost:4100/api/run \
-H "Content-Type: application/json" \
-d '{"businessUrl":"http://localhost:8080","transport":"rest","scenarioId":"compliance.full-suite"}' \
| jq -r '.runId' > /tmp/runId
sleep 30
curl -s "http://localhost:4100/api/runs/$(cat /tmp/runId)" \
| jq -e '.status == "success"'For a full local matrix against a running Dockware shop, run every scenario
once with transport=rest and once with transport=mcp. The current Shopware
adapter has been verified this way: 34/34 simulator runs passed.
The official upstream UCP conformance suite is covered separately by
shopware/.github/workflows/ucp-conformance.yml. It runs with
UCP_CONFORMANCE_MODE=1, seeds the flower_shop fixture, and currently
passes locally with TOTAL=13 FAILURES=0.
5.6 Adding a custom scenario
A scenario is a declarative entry in simulator/src/agent/scenarios.ts.
The full structure:
{
id: 'my-scenario.id',
title: 'Human-readable title',
category: 'discovery' | 'catalog' | 'cart' | 'checkout'
| 'extensions' | 'identity' | 'webhook' | 'compliance',
description: 'A sentence shown in the UI under the title.',
requiresCapabilities: ['dev.ucp.shopping.cart', …], // optional
params: [
{ id: 'query', label: 'Search query', type: 'string', default: 'shoes' },
{ id: 'qty', label: 'Quantity', type: 'number', default: 1 },
// 'string' | 'number' | 'boolean' | 'select' (with options[])
],
async run(agent, params) {
await agent.connect()
const products = await agent.searchCatalog(String(params.query), 6)
// ... your scenario logic, using the agent's public methods
},
},Drop it into the SCENARIOS array; no other file needs editing. The
simulator picks it up at startup and renders it in the picker with its
own parameter form.
5.7 Replaying past runs
Each run's full transcript (events + conversation + HTTP exchanges) is
held in memory and exposed via GET /api/runs/:id. You can persist
this JSON to disk and either:
- Diff it against a previous run to catch regressions
- Replay it through your own renderer for CI reports
- Re-build a deterministic test fixture (each exchange is sufficient to reproduce the request)
A persistent SQLite store for runs is on the roadmap (see
simulator/progress.md).
→ Next: Diagrams