Developer guide

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

MethodPathPurpose
GET/The web UI (3-pane layout)
GET/profile.jsonThe simulator's own UCP platform profile
GET/api/scenariosList all available scenarios + their params
POST/api/runKick off a scenario run; returns a runId
GET/api/runs/:idSnapshot of a run (trace events + conversation + http exchanges)
POST/webhooks/order?business_url=…Receives signed UCP order webhooks; verifies signature
GET/oauth/callbackOAuth 2.0 redirect handler (identity linking handshake)
WS/wsLive 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

bash
curl -s http://localhost:4100/api/scenarios | jq '.scenarios[] | {id, category, requires: .requiresCapabilities}'
json
{"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

bash
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 }
  }'
json
{
  "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:

bash
curl -s http://localhost:4100/api/runs/abc123def456 | jq '{status, exchanges: (.exchanges | length), conv: (.conversation | length)}'
json
{ "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:

js
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:

yaml
# .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:

ts
{
  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