Junction

@crvouga/mockingbird-service-junction

Stateful emulator of the Junction (Vital) user API driven by its OpenAPI contract and verified by differential property tests against the Junction sandbox.

FeaturedParity: Users, orders, and lab resultsHealth & labs42 of 42 operationsIn-process fetchNode servermockingbird-junction
$
npm install -D @crvouga/mockingbird-service-junction

Playground

Requests go to the real emulator running in this tab. State persists across requests, and the journal shows what it received. A green dot marks operations whose sample succeeds as-is; others need ids from earlier responses.

Loads the real @crvouga/mockingbird-service-junction into this tab on first send
https://api.sandbox.us.junction.com

Send a request to see the emulator's response.

Operations

42 of 42 operations in the vendored contract are emulated. Hand-authored subset from Junction public OpenAPI snippets in the docs.

MethodPathOperationStatus
GET/v2/userget_teams_users_v2_user_get
POST/v2/usercreate_user_v2_user_post
GET/v2/user/{user_id}get_user_v2_user__user_id__get
PATCH/v2/user/{user_id}patch_user_v2_user__user_id__patch
DELETE/v2/user/{user_id}delete_user_v2_user__user_id__delete
GET/v2/user/resolve/{client_user_id}get_user_by_client_user_id_v2_user_resolve__client_user_id__get
PATCH/v2/user/{user_id}/infopatch_user_info_v2_user__user_id__info_patch
GET/v2/user/{user_id}/info/latestget_latest_user_info_user_v2_user__user_id__info_latest_get
GET/v3/lab_testget_paginated_lab_tests_for_team_v3_lab_test_get
POST/v3/lab_test/estimate_order_set_pricingestimate_order_set_pricing_v3_lab_test_estimate_order_set_pricing_post
GET/v3/lab_test/lab_accountget_team_lab_accounts_v3_lab_test_lab_account_get
GET/v3/lab_testsget_lab_tests_for_team_v3_lab_tests_getGet Lab Tests For Team
GET/v3/lab_tests/{lab_test_id}get_lab_test_for_team_v3_lab_tests__lab_test_id__get
GET/v3/lab_tests/labsget_labs_v3_lab_tests_labs_get
GET/v3/lab_tests/markersget_markers_v3_lab_tests_markers_get
GET/v3/lab_tests/{lab_test_id}/markersget_markers_for_lab_test_v3_lab_tests__lab_test_id__markers_get
POST/v3/lab_tests/list_order_set_markerslist_order_set_markers_v3_lab_tests_list_order_set_markers_post
POST/v3/ordercreate_order_v3_order_post
GET/v3/order/area/infoget_area_info_v3_order_area_info_get
GET/v3/order/psc/infoget_psc_info_v3_order_psc_info_get
POST/v3/order/phlebotomy/appointment/availabilityget_phlebotomy_appointment_availability_v3_order_phlebotomy_appointment_availability_post
GET/v3/order/phlebotomy/appointment/cancellation-reasonsget_phlebotomy_appointment_cancellation_reason_v3_order_phlebotomy_appointment_cancellation_reasons_get
POST/v3/order/psc/appointment/availabilityget_psc_appointment_availability_v3_order_psc_appointment_availability_post
GET/v3/order/psc/appointment/cancellation-reasonsget_psc_appointment_cancellation_reason_v3_order_psc_appointment_cancellation_reasons_get
GET/v3/order/{order_id}/phlebotomy/appointmentget_phlebotomy_appointment_v3_order__order_id__phlebotomy_appointment_get
POST/v3/order/{order_id}/phlebotomy/appointment/bookbook_phlebotomy_appointment_v3_order__order_id__phlebotomy_appointment_book_post
PATCH/v3/order/{order_id}/phlebotomy/appointment/reschedulereschedule_phlebotomy_appointment_v3_order__order_id__phlebotomy_appointment_reschedule_patch
PATCH/v3/order/{order_id}/phlebotomy/appointment/cancelcancel_phlebotomy_appointment_v3_order__order_id__phlebotomy_appointment_cancel_patch
GET/v3/order/{order_id}/psc/appointmentget_psc_appointment_v3_order__order_id__psc_appointment_get
POST/v3/order/{order_id}/psc/appointment/bookbook_psc_appointment_v3_order__order_id__psc_appointment_book_post
PATCH/v3/order/{order_id}/psc/appointment/reschedulereschedule_psc_appointment_v3_order__order_id__psc_appointment_reschedule_patch
PATCH/v3/order/{order_id}/psc/appointment/cancelcancel_psc_appointment_v3_order__order_id__psc_appointment_cancel_patch
GET/v3/order/{order_id}get_order_v3_order__order_id__get
POST/v3/order/{order_id}/cancelcancel_order_v3_order__order_id__cancel_post
POST/v3/order/{order_id}/testsimulate_order_v3_order__order_id__test_post
GET/v3/order/{order_id}/resultget_result_raw_v3_order__order_id__result_get
GET/v3/order/{order_id}/result/metadataget_result_metadata_v3_order__order_id__result_metadata_get
GET/v3/order/{order_id}/result/pdfget_result_pdf_v3_order__order_id__result_pdf_get
GET/v3/order/{order_id}/requisition/pdfget_order_requisition_pdf_v3_order__order_id__requisition_pdf_get
GET/v3/ordersget_orders_v3_orders_get
GET/v3/order_transaction/{transaction_id}get_order_transaction_v3_order_transaction__transaction_id__get
GET/v3/order_transaction/{transaction_id}/resultget_order_transaction_result_v3_order_transaction__transaction_id__result_get

Documentation

The package README, the same file that ships in the npm tarball.Edit on GitHub

Local emulators. Real API contracts. Part of Mockingbird.

Stateful emulator of the Junction (formerly Vital) API: users (/v2/user), the lab-testing catalog, lab orders (create, cancel, simulate, results, requisitions), at-home phlebotomy and patient-service-center (PSC) scheduling, and the labtest.order.* / labtest.appointment.updated webhooks. All 40 operations in the vendored OpenAPI subset are served, and behaviour is checked by differential property tests against the Junction sandbox.

Use it to take Junction off your suite's critical path: no sandbox key, no shared 50-user sandbox cap (the emulator enforces no sandbox limit unless a test asks for one), no cross-suite interference. Serve it as a local origin with one command, or run it in-process.

Shell
npx mockingbird-junction serve   # http://127.0.0.1:8787, recorded sandbox corpus loaded

Install

Shell
npm install -D @crvouga/mockingbird-service-junction

ESM only; Node >= 22 or Bun >= 1.2 (CommonJS callers on Node >= 22.12 can require() it). No native dependencies: state lives in an in-memory SQLite engine written in TypeScript, bundled in.

Entry point Runtime What it is
@crvouga/mockingbird-service-junction any (Node, Bun, Workers, browsers) JunctionAPI, createRuntime, corpus and verify tools
@crvouga/mockingbird-service-junction/server Node createServer() — a listening HTTP server
@crvouga/mockingbird-service-junction/corpus any defaultCorpus — the recording shipped in the package (a separate entry, so importing the emulator never parses it)
mockingbird-junction (bin) Node serve, corpus pull, corpus diff, verify

Usage

mockingbird-junction serve

Shell
npx mockingbird-junction serve --port 8787
npx mockingbird-junction serve --corpus ./test/junction-corpus.json --lab-accounts ./test/lab-accounts.json
npx mockingbird-junction serve --webhook-url http://127.0.0.1:3100/webhooks/junction --webhook-secret whsec_...
npx mockingbird-junction serve --help
Flag Default Meaning
--port <n> 8787 Port to listen on (0 for any free port; the bound URL is printed)
--host <host> 127.0.0.1 Interface to bind
--corpus <default|none|file> default The shipped corpus, no corpus (synthetic data), or a file from corpus pull
--geo <corpus|synthetic|corpus+synthetic> corpus with a corpus How unknown ZIPs are answered; see Geo
--zip-overlays <file> — ZIP aliases and per-ZIP overrides
--catalog-overlays <file> — Lab tests derived from catalog ones at boot
--marker-catalog <file> — Markers GET /v3/lab_tests/markers offers, with pricing
--add-on-source-markers off Name the markers behind each add-on result line
--lab-accounts <file> built-in fixtures Lab accounts: a JSON array, or { "presets": [...], "accounts": [...] }
--team-id <uuid> the corpus's team The team the emulator answers as
--max-users <n> unlimited Enforce the sandbox's live-user cap
--identity <strict|adopt-users> strict Unknown user_ids: 404, or create on first use
--default-billing-type <lab=type,…> client_bill The billing_type an order for that lab gets when it omits one (billing)
--fixtures <file> — Users and orders every namespace starts with
--journal-size <n> 1000 Requests each namespace's journal keeps
--webhook-url <url> / --webhook-secret <whsec_…> off Deliver signed webhooks (secret also from MOCKINGBIRD_JUNCTION_WEBHOOK_SECRET)
--webhook-retry-delays <ms,…> Svix's schedule Delay before each delivery attempt, e.g. 0,1000,5000 in tests
--webhook-scope <scope> — Sent as x-mockingbird-scope on every delivery
--admin-key <key> open Require x-mockingbird-admin-key on /__admin/* (also MOCKINGBIRD_ADMIN_KEY)
--seed <seed> 0 Seeds fault rates and retry jitter
--log <pretty|json|off> pretty One line per request: operation id, status, duration, namespace, fault
--log-requests — Same as --log json: one JSON line per request with the ids it touched, never bodies
--seed-url <url> / --seed-key <key> — Pull a corpus from a live team at boot. Slow and a live dependency; prefer a committed corpus pull file
--config <file> — Serve every service in a mockingbird.json instead (below)

At startup it prints the listen address, the loaded corpus (its version label, observation, ZIP, lab-test and lab-account counts, recording date and source), the geo mode, the team id, the lab-account count, which sandbox limits are on (normally none), the identity mode, and whether webhook delivery is on.

Many services from one config

Any Mockingbird service's CLI can boot every service in a config file, so a stack adds a config entry per vendor instead of a wrapper process per vendor. Each service named must be installed (junction loads @crvouga/mockingbird-service-junction).

JSON
{
  "log": "json",
  "services": {
    "junction": {
      "port": 8787,
      "adminKey": "local-admin",
      "options": { "corpus": "./test/junction-corpus.json", "webhook-url": "http://127.0.0.1:3100/webhooks/junction" }
    },
    "stripe": { "port": 12111 }
  }
}
Shell
npx mockingbird-junction serve --config mockingbird.json

options takes the service's own serve flags by long name.

createServer (Node)

TypeScript
import { VitalClient } from "@tryvital/vital-node"
import { createServer } from "@crvouga/mockingbird-service-junction/server"

const server = await createServer() // shipped corpus, any free port

// The Vital SDK your application already uses. Only `environment` changes.
const junction = new VitalClient({ apiKey: "sk_us_mockingbird", environment: server.url })
const user = await junction.user.create({ clientUserId: "app-user-1" })
console.log(user.userId) // "<uuid>"

await server.close()

createServer(options) takes every createRuntime option plus port (default 0), host (default 127.0.0.1) and corpus ("default", "none", a file path or a loaded corpus). The result has url, port, runtime and close().

createRuntime (any Fetch server)

createRuntime is the whole served emulator — health, admin, namespaces, clock, faults, metrics — as a single runtime-neutral fetch(request). Hand it to Bun.serve, a Worker, or any other Fetch server:

TypeScript
import { VitalClient } from "@tryvital/vital-node"
import { createRuntime } from "@crvouga/mockingbird-service-junction"
import { defaultCorpus } from "@crvouga/mockingbird-service-junction/corpus"

const junction = createRuntime({ corpus: defaultCorpus })
const server = Bun.serve({ port: 0, fetch: (request) => junction.fetch(request) })

// One SDK client per worker. The URL prefix isolates that worker's data.
const as = (worker: string) =>
  new VitalClient({
    apiKey: "sk_us_mockingbird",
    environment: `${server.url.origin}/__admin/ns/${worker}`,
  })

await as("worker-1").user.create({ clientUserId: "patient-1" })
const other = await as("worker-2").user.getAll()
console.log(other.users) // [] (worker-2 sees none of worker-1's users)

await junction.reset("worker-1") // or junction.reset("*") for every namespace
await server.stop()

Pointing @tryvital/vital-node at it

The official SDK takes the base URL as environment (this is how the package's SDK drop-in test constructs it). It has no option for a custom fetch, so it needs a served URL; a client that accepts a fetch can call createRuntime().fetch in-process instead.

TypeScript
import { VitalClient } from "@tryvital/vital-node"
import { createServer } from "@crvouga/mockingbird-service-junction/server"

// Test setup: the emulator, with its clock set.
const server = await createServer()
await fetch(`${server.url}/__admin/clock`, {
  method: "POST",
  body: JSON.stringify({ set: "2030-01-01T00:00:00Z" }),
})

// The application's calls, through the SDK.
const junction = new VitalClient({ apiKey: "sk_us_mockingbird", environment: server.url })
const user = await junction.user.create({ clientUserId: "app-user-1" })

const labTests = await junction.labTests.get()
const testkit = labTests.find((test) => test.method === "testkit")
if (testkit === undefined) throw new Error("default catalog has a testkit test")

const { order } = await junction.labTests.createOrder({
  userId: user.userId,
  patientDetails: {
    firstName: "Ada",
    lastName: "Lovelace",
    dob: "1990-01-01",
    gender: "female",
    phoneNumber: "+14155551234",
    email: "ada@example.com",
  },
  patientAddress: {
    firstLine: "1 N Central Ave",
    city: "Phoenix",
    state: "AZ",
    zip: "85004",
    country: "US",
  },
  orderSet: { labTestIds: [testkit.id] },
})
console.log(order.id, order.status) // "<uuid>" "received"

// Test assertion: every event the emulator published, oldest first.
const events = server.runtime.instance().webhookEvents()
console.log(events.map((event) => event.event_type)) // ["labtest.order.created"]

await server.close()

The clock (POST /__admin/clock, or now in milliseconds on JunctionAPI) drives created_on/updated_at fields, webhook timestamps, and when delayed simulations (POST /v3/order/{id}/test?final_status=completed&delay=<seconds>) become due; the delayed transition is applied on the next GET /v3/order/{id} whose clock is past the due time.

If your app guards Junction hosts with an allowlist, allow 127.0.0.1 / localhost for tests while keeping the production and sandbox guards intact.

The service contract

Every Mockingbird service answers the same control surface, outside the vendor's auth gate:

  • GET /__admin/health — unauthenticated readiness probe: { "status": "ok", "service": "junction", "corpus": "<label>", "geo": "corpus", … }.
  • /__admin/* — the control plane. Open by default; with --admin-key / adminKey it requires x-mockingbird-admin-key, which is separate from any vendor key. Admin errors have one shape, { "error": { "type": "mockingbird_admin", "message": "…" } }, so they can never be confused with a Junction error. GET /__admin lists every route.
  • x-mockingbird-namespace: <name> — isolates a request's data ([A-Za-z0-9_.-]{1,64}). Each namespace is a separate team over one shared database, so parallel workers share one process without seeing each other. The corpus is shared read-only; creating a namespace is cheap. Admin routes take the namespace from ?namespace=, then the header, then default. Configuration set through the admin API (lab accounts, limits, identity, geo) belongs to one namespace and survives that namespace's POST /__admin/reset; data does not.
  • x-mockingbird: junction@<version>; ns=<namespace> — on every response: vendor answers, errors, faults, admin and health. Junction never sends it, so it tells the emulator from the vendor (see Is this the emulator?).
Route Does
POST /__admin/reset Reset the request's namespace; ?all=1 resets every namespace
GET /__admin/namespaces Namespaces created so far
POST /__admin/snapshots Snapshot the namespace; returns { id }
POST /__admin/snapshots/{id}/restore Restore it (an assignment: later writes are discarded)
DELETE /__admin/snapshots/{id} Forget a snapshot
GET / POST /__admin/clock Read or move the clock: { "set": "2030-01-01T00:00:00Z" }, { "advance": "2h" }, { "freeze": true }, { "reset": true }
GET / POST / DELETE /__admin/faults List, add or clear fault rules (DELETE ?id= removes one)
POST /__admin/faults/presets/{name} Add a named Junction fault; body may override count, rate, operationId…
GET /__admin/metrics Requests by operation and status, fault count, and unmatched routes
GET /__admin/orders Orders in the namespace with their current status, the lab_account_id each was placed with (null: Junction's platform account) and lab_account_request (below)
POST /__admin/orders/{id}/transition Move an order: { "to": "completed", "result": "abnormal" } (order control)
GET / PUT /__admin/results/{id} Read or install an exact result payload / PDF for an order
GET /__admin/result-fixtures The named results
GET / PUT /__admin/quirks Read or replace the namespace's opt-in sandbox quirks ({ "quirks": { "sameSecondCancelStatus": true } })
GET / PUT /__admin/lab-accounts Read or replace the namespace's lab accounts ({ "accounts": [...], "presets": [...] }; { "accounts": null } restores the default)
POST /__admin/lab-accounts Add one account; 409 if the id exists
PATCH / DELETE /__admin/lab-accounts/{id} Merge fields into an account ({ "status": "suspended" }, { "states": [...] }) / remove it; 404 if absent
GET /__admin/lab-accounts/presets Every preset, with its full record, plus aliases (deprecated names) and synthetic (presets that are not a recorded account)
POST /__admin/lab-accounts/presets/{name} Add a preset account; body { "id": "…" } overrides its id
GET /__admin/team { "teamId" } — the team the namespace answers as
GET / PUT /__admin/limits Read or change the namespace's sandbox limits
GET / PUT /__admin/default-billing-types Read or replace { "defaultBillingTypes": { "<lab slug>": "<billing_type>" } } (billing); {} restores client_bill
GET / PUT /__admin/identity Read or set { "mode": "strict" | "adopt-users" } (identity)
POST /__admin/users, POST /__admin/users/bulk Insert a user (or { "users": [...] }, all or none) with a chosen user_id; 409 on a duplicate id or client_user_id
DELETE /__admin/users/{id} Remove a user outright, leaving no deletion tombstone
POST /__admin/orders Insert an order, optionally already in a status, through create_order's record builder
POST /__admin/import { "users": [...], "orders": [...] } in one call
GET / DELETE /__admin/requests The namespace's request journal (?operationId=, ?status=, ?since=, ?limit=, ?all=1) / clear it
GET /__admin/corpus, PUT /__admin/geo The loaded corpus; switch geo mode
GET /__admin/webhooks Deliveries with every attempt
POST /__admin/webhooks/{id}/replay, POST /__admin/webhooks/flush Redeliver one message; run pending retries now
GET /__admin/webhooks/events Every event recorded, delivered or not

GET /__admin/metrics → unmatched counts requests to paths the contract does not have. It is how a new SDK call shows up — as a count — before it fails a suite as a 404.

Corpus

Provider-owned inventory — area serviceability, PSC site lists, the lab catalog, labs, lab accounts — cannot be synthesized faithfully. A corpus is an exact recording of those parameter-stable reads, and the emulator answers them byte for byte.

The shipped corpus (defaultCorpus, and what serve loads unless told otherwise) was recorded from the Junction sandbox. It covers area and PSC serviceability for 57 ZIPs across the US, the PSC labs those ZIPs reach, and the recording team's catalog. The catalog and lab accounts belong to the recording team: lab-test ids are per team, so your product's ids will not be in it. For your catalog and your lab accounts, record your own team and commit the file:

Shell
npx mockingbird-junction corpus pull --real-key "$JUNCTION_SANDBOX_KEY" --out test/junction-corpus.json
npx mockingbird-junction corpus pull --real-key "$JUNCTION_SANDBOX_KEY" --base test/junction-corpus.json \
  --zip 10001,94105 --coverage-only --out test/junction-corpus.json   # add ZIPs to it
npx mockingbird-junction corpus diff test/junction-corpus.json /tmp/fresh.json   # exit 1 when they differ

corpus pull only issues GETs, refuses non-sandbox keys (sk_us_… / sk_eu_…) unless given --allow-any-key, and stamps the file with a SHA-256 fingerprint of its content (not its recording time), so re-pulling unchanged data yields the same fingerprint. Availability is never recorded: its slot dates and single-use booking_keys cannot be replayed, so it always comes from the deterministic generator.

Geo: corpus or synthetic

With a corpus loaded, serviceability reads (area/info, psc/info, phlebotomy and PSC availability) answer only for ZIPs the corpus covers. Any other well-formed ZIP gets:

JSON
{
  "detail": {
    "error_type": "MOCKINGBIRD_UNKNOWN_ZIP",
    "error_message": "ZIP 00501 is not in the loaded Junction corpus (57 ZIPs covered). Record it with `mockingbird-junction corpus pull --zip 00501`, or serve with --geo synthetic to invent coverage.",
    "zip_code": "00501",
    "corpus": "v1-2026-09-18-HqFDuEAuhkwD"
  }
}

with status 424, which Junction never sends and HTTP clients do not retry: a fixture gap fails loudly instead of silently disagreeing with production. A malformed ZIP still gets Junction's own 422. --geo synthetic (the default without a corpus) invents plausible, deterministic coverage instead — right for property tests that walk random ZIPs, wrong for a suite standing in for production.

A covered ZIP is answered from its recordings, never next to them. The corpus records area/info and psc/info at the default radius (25 miles, so radius=25 and no radius are the same read) and at 100 miles. A psc/info read at another radius is the smallest wider recording filtered to distance < radius: the sandbox lists the nearest sites (at most 30), and distance is rounded, so this reproduces every recorded 25-mile answer from its 100-mile one exactly. An area/info read at a radius with no recording, or any read with parameters the corpus did not record (for example capabilities), gets the same 424, because within_radius counts cannot be derived. In --geo synthetic the derivation still applies, and only reads with nothing to derive from are invented.

Hybrid geo and ZIP overlays

--geo corpus+synthetic (geo: "corpus+synthetic") replays the recording for the ZIPs the corpus covers and synthesizes every other ZIP, so a suite needs no second, synthetic runtime as a fallback. A covered ZIP is still never answered next to its recording: a read the recording cannot answer (see above) refuses with the same 424 as --geo corpus. Switch it per namespace with PUT /__admin/geo {"mode": "corpus+synthetic"}.

zipOverlays (--zip-overlays <file>, or PUT /__admin/geo {"overlays": {...}}) edits answers for individual ZIPs, in any geo mode:

JSON
{
  "aliases": { "07105": "85004" },
  "overrides": { "07105": { "phlebotomy_served": true }, "10118": { "no_pscs": true } }
}

An alias makes a ZIP answer as another one (coverage, recordings and the synthetic model all use the target; the response echoes the requested ZIP). phlebotomy_served sets phlebotomy.is_served (and providers) on area/info; no_pscs empties the psc/info list and zeroes the within_radius counts on area/info. An override is looked up by the requested ZIP, then by its alias target. Overlays are configuration: they survive reset().

The sealed sandbox recording agrees with the report behind this feature that NJ 07030 is not served for at-home phlebotomy, and the synthetic model reports 10118, 11201, 12207, 07030 and 07601 as not served. It does not know that NJ 07105 is served (the synthetic model only serves a handful of ZIP prefixes), so set that one with an override.

Catalog overlays and a-la-carte markers

Catalog overlays (catalogOverlays, --catalog-overlays <file>) add lab tests derived from ones the catalog already holds, with no network access, to every namespace at boot and again on every reset():

TypeScript
import { createRuntime } from "@crvouga/mockingbird-service-junction"

createRuntime({
  catalogOverlays: [
    {
      from: "lipid_panel_athome", // id or slug of a catalog lab test (or an earlier overlay)
      id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
      name: "Lipid Panel: Quest",
      slug: "lipid_panel_quest", // default: derived from name
      lab: "sonora_quest", // a lab slug the catalog lists, or a lab record { id, slug, name, ... }
      stripAoe: true, // markers lose their ask-on-order questions
      autoGenerated: true, // lists with generation_method=auto, not by default
    },
  ],
})

method, price (dollars) and status can be overridden too. Moving the clone to a lab gives its markers and expected results that lab's id. A bad overlay (unknown from, non-UUID or duplicate id, unknown lab) fails at boot naming catalogOverlays[n].<field>.

A-la-carte markers. GET /v3/lab_tests/markers lists the markers a team can order one by one: those embedded in active catalog lab tests, plus markerCatalog entries (--marker-catalog <file>) with their own price, pricing, aoe, expected_results and a_la_carte_enabled. It takes lab_id, lab_slug (wins over lab_id), name (a name substring, or a provider test code), a_la_carte_enabled, lab_account_id (narrows to the account's lab), include_pricing, page and size, and answers { markers, total, page, size, pages } (plus pricing: { currency, data } keyed by marker id for the page's markers with include_pricing=true). A marker price is read as dollars ("12.50" is base_amount_minor: 1250); "N/A" is { "unspecified": "price_unavailable" }.

POST /v3/lab_test/estimate_order_set_pricing prices each order set (lab_test_ids and/or add_on.marker_ids / provider_ids) as one lab_charge component: the sum of the lab tests' price and the add-on markers' prices, with a marker_breakdown for add-ons. If any add-on marker has no stated price the whole estimate is price_unavailable. modality, us_state and billing are validated but do not change the price, and no other charge components are modelled.

Both routes follow the published contract (markers, estimate); neither has been compared with the live sandbox (prices are contract-owned; best guess, not oracle-verified), so their parity is disabled and the 400 messages for unknown tests/markers are borrowed from create_order.

addOnSourceMarkers: true (--add-on-source-markers) makes each biomarker line of an add-on order carry source_markers: [{ marker_id, name, slug, provider_id }]. It is off by default, because the default result lines carry no source_markers, and was reported from a sandbox probe that has not been re-verified.

GET /v3/lab_tests lists every lab test unless filtered; pass status=active to exclude inactive ones. The documented default of that filter is not stated, so the emulator does not change it: default-excludes-inactive (as #470 reports) is an unverified claim and is not implemented (best guess, not oracle-verified; junction.markers-overlays.test.ts pins the current contract).

Lab accounts

create_order selects explicit linked accounts in either active or ready_to_launch status. With no id, exactly one orderable linked account is selected. Multiple orderable accounts use the per-lab labAccountOmissionPolicies control (platform, the default, or reject). Platform selection uses an orderable linked account with org_id: null, or an opaque platform account for a supported platform lab. It never chooses a customer account by listing order. With no linked accounts, supported labs use the platform; with linked accounts but none orderable, ordering fails. Billing type and patient state are checked against the selected account. This follows the Junction guide; the choice between platform fallback and rejection varies by lab/team, so configure reject where appropriate. Set { labAccountOmissionPolicies: { bioreference: "platform" } } at runtime creation, or PUT /__admin/lab-account-omission-policies with { "policies": { "bioreference": "reject" } }. GET reads the effective overrides. Overrides are namespace isolated and kept across reset.

Explicit ids naming another lab's account remain accepted, matching the sandbox probe in corpus/lab-account-probes.json and #138. Delegation alone does not change account selection.

Requisition PDFs are synthetic and contain labelled Account number: evidence copied from the selected account's provider_account_id when the order is created. Later account edits, reordering or routing changes cannot change that evidence. Opaque platform accounts show unknown. Use GET /__admin/requisitions/:id to inspect it, or PUT with { "provider_account_id": "MOCK-CUSTOMER" } (or null for explicitly unknown evidence) to install a namespace-isolated fixture. Reset removes the orders and their requisition fixtures. This models account evidence, not a vendor's exact PDF layout or proof of a real association.

Junction's order (ClientFacingOrder) has no lab_account_id, so neither does the emulator's: not on the create response, GET /v3/order/{id}, GET /v3/orders or webhooks. The account an order was placed with is reported only by GET /__admin/orders and instance().orderLabAccount(orderId).

The accounts come from, in order: labAccounts / --lab-accounts, else the corpus (a pulled corpus carries your team's real accounts), else built-in fixtures. Configure them as ClientFacingLabAccounts, with states as shorthand for client-bill states:

TypeScript
import { createRuntime, US_STATES } from "@crvouga/mockingbird-service-junction"

const junction = createRuntime({
  labAccounts: [
    { id: "acct-bioref-nynj", lab: "bioreference", states: ["NY", "NJ"] },
    {
      id: "acct-labcorp-47",
      lab: "labcorp",
      states: US_STATES.filter((state) => !["NY", "NJ", "RI"].includes(state)),
    },
    { id: "acct-quest", lab: "quest", delegated_flow: "order_delegated" },
  ],
})
console.log(junction.instance().labAccounts().length) // 3

Rejections keep Junction's body shape, 400 {"detail": "Lab account is not linked to your team"}. delegated_flow is stored and listed, but see not modelled.

Availability reads that take a lab_account_id (GET /v3/order/area/info) check it against the same live list, so an id create_order accepts is never a 404 there.

Default billing type

An order that omits billing_type is evaluated as client_bill, Junction's documented default. A sandbox team has been observed evaluating BioReference orders as patient_bill_passthrough instead (whether that is a team, lab or account setting is not visible from outside), so the default is configurable per lab slug:

TypeScript
import { createRuntime } from "@crvouga/mockingbird-service-junction"

const junction = createRuntime({ defaultBillingTypes: { bioreference: "patient_bill_passthrough" } })
console.log(junction.instance().defaultBillingTypes) // { bioreference: "patient_bill_passthrough" }

--default-billing-type bioreference=patient_bill_passthrough and PUT /__admin/default-billing-types (per namespace, kept across its reset) do the same. When the default resolves to a billing type the account lacks, the rejection is worded as the sandbox words it, 400 {"detail": "Lab 13 does not support billing type patient_bill_passthrough"}; an explicit billing_type the account lacks keeps Billing type … is not supported by the lab account used for this order. verify --orders orders through each active account with billing_type omitted, so a run against your team shows which default it uses.

Changing accounts at runtime

Each namespace has its own layout, kept across its own reset. Change one account at a time, or replace them all:

Shell
curl -X POST  localhost:8787/__admin/lab-accounts/presets/bioreference_ny_nj_delegated
curl -X PATCH localhost:8787/__admin/lab-accounts/$ID -d '{"status": "suspended"}'
curl -X POST  localhost:8787/__admin/lab-accounts -d '{"id": "…", "lab": "quest", "states": ["AZ"]}'
curl -X DELETE localhost:8787/__admin/lab-accounts/$ID
curl -X PUT   localhost:8787/__admin/lab-accounts -d '{"presets": ["quest_platform"], "accounts": []}'

A bad field is a 400 in the admin shape naming it, e.g. lab account x: states: ZZ is not a US state code. --lab-accounts and the labAccounts option take the same layout, so a stack can declare it at boot with no admin call:

JSON
{ "presets": ["bioreference_ny_nj_delegated", { "name": "quest_platform", "id": "…" }], "accounts": [] }

Lab-account presets

Named after what they model; ids are deterministicUuid("junction:lab-account:<name>") unless given. *_platform presets are Junction's own accounts (org_id: null, team_id_allowlist: []); the other active presets are customer-owned (an Org's account linked to the team). Where a preset copies an account recorded from a real BioReference-linked sandbox team (2026-09-21), its billing states are the recording's. PLATFORM_ACCOUNT_STATES is every state but NJ, NY and RI (47).

Preset lab delegated_flow allowed_billing team_id_allowlist Source
bioreference_ny_nj_delegated bioreference order_delegated client_bill: NY, NJ the team requested shape, not a recorded account
bioreference_delegated_multi_state bioreference order_delegated client_bill: every state but NY, NJ (48) the team recorded
bioreference_platform bioreference not_delegated client_bill: PLATFORM_ACCOUNT_STATES [] recorded ("Junction BioReference Account")
bioreference_patient_bill_passthrough bioreference not_delegated patient_bill_passthrough: NJ, NY the team recorded
quest_platform / labcorp_platform quest / labcorp not_delegated client_bill: PLATFORM_ACCOUNT_STATES [] recorded ("Junction Quest/Labcorp Account")
bioreference_current_platform bioreference not_delegated patient_bill_passthrough: NJ, NY [] team listing reported 2026-10-18 (#475)
quest_sandbox_platform quest not_delegated client_bill: PLATFORM_ACCOUNT_STATES; commercial_insurance: all 50 [] sandbox listing reported 2026-10-18 (#475)
sonora_quest_production_platform sonora_quest not_delegated client_bill: PLATFORM_ACCOUNT_STATES [] production listing reported 2026-10-18 (#475)
suspended_bioreference / suspended_quest / suspended_labcorp as above not_delegated as the platform accounts [] the platform shapes with status: "suspended"

The September recording's bioreference_platform is illustrative, not a current listing: the house BioReference account now lists only patient_bill_passthrough for NJ and NY (use bioreference_current_platform, and see omitted house-account billing for how production still accepts client_bill when no account is named). No account at any lab lists DC, so no preset does either. bioreference_current_platform, quest_sandbox_platform and sonora_quest_production_platform follow listings an agent reported from GET /v3/lab_test/lab_account (a consumer-audit report with synthetic ids, not a repository probe, so best guess, not oracle-verified); the older presets keep their shapes so pinned suites do not change.

Every preset is status: "active" unless named suspended_*. GET /__admin/lab-accounts/presets returns the full records, the deprecated aliases, and the synthetic presets (not a recorded account). bioreference_customer_multi_state is a deprecated alias of bioreference_platform: it was misnamed, since the record is Junction's platform account, not a customer-owned one. It still works and keeps its own default id (presetAccountId("bioreference_customer_multi_state")).

Team identity

The emulator answers as one team: team_id on users, orders and webhooks, and the team that team_id_allowlist is checked against. It is, in order: teamId / --team-id, the team a version-2 corpus recorded (corpus pull reads it from a listed user, else from the accounts' allowlists), else the fixed MOCK_TEAM_ID. GET /__admin/team reports it.

A version-2 corpus keeps each account's allowlist verbatim, so an account the real team is not linked to answers 400 {"detail": "Lab account is not linked to your team"} exactly as it does live. A version-1 corpus (no teamId) still loads and behaves as 0.2.0 did: every recorded account is also linked to the emulator team.

Empty allowlists. Junction's own Quest, Labcorp and BioReference accounts carry team_id_allowlist: [], and a real team's listing returns them, so the emulator lists them and treats them as linked. Ordering through one by explicit id, and with the id omitted when it is the lab's only active account, places the order on the sandbox (corpus/lab-account-probes.json, recorded by the parity run), as the emulator does. verify --orders re-checks it whenever the corpus has such an account.

Sandbox limits

The emulator exists to escape the sandbox's restrictions, so it enforces none of them unless a test asks — this is a tested guarantee (500 users in a loop all succeed), not an accident.

Sandbox limit Modelled Default Turn it on
50 live users per team yes, byte-exact body; deleted users free their slot off (unlimited) limits: { maxUsers: 50 }, --max-users 50, PUT /__admin/limits {"maxUsers": 50}
POST /v3/order/{id}/test only in sandbox as a switch: refuses a key that is not sk_us_…/sk_eu_… (400, body not recorded) off simulateRequiresSandbox: true
Rate limits as a per-namespace budget: 429 {"detail":"Too Many Requests"}, retry-after: 1 (shape not recorded) off rateLimitPerSecond: n

PUT /__admin/limits merges (null lifts a numeric limit) and is per namespace; GET /__admin/limits returns the effective values. For a one-off failure instead of a standing limit, the sandbox_user_quota fault preset is still there.

Fixtures and identity

A suite often needs state the public API cannot build: database fixtures that already carry real Junction user_ids, or an order already in some status. Insert them directly:

Shell
curl -X POST localhost:8787/__admin/users -d '{"user_id": "3f0c…", "client_user_id": "patient-1"}'
curl -X POST localhost:8787/__admin/orders -d '{"user_id": "3f0c…", "lab_test_id": "…", "status": "completed"}'
curl -X POST localhost:8787/__admin/import -d @test/junction-fixtures.json
  • Users take user_id?, client_user_id, created_on?, fallback_time_zone?, fallback_birth_date?, ingestion_start?, ingestion_end?. A duplicate user_id or client_user_id is a 409; POST /__admin/users/bulk inserts all or none.
  • Orders take order_id?, user_id, lab_test_id, lab_account_id?, status? (any transition target), collection_method?, patient_details?, patient_address?, billing_type?, created_at? and result_fixture? (as PUT /__admin/results/{id} takes it). Request validation is skipped; references are not — the user and lab test must exist (404 otherwise).
  • Inserted records go through the same builders as create_user / create_order, so every read path (GET /v2/user/{id}, resolve by client_user_id, GET /v3/order/{id}, GET /v3/orders?user_id=) sees them as API-created. Default timestamps read the emulator clock.
  • No webhook fires unless the body has "emitWebhooks": true: fixtures should not trigger backend side effects.
  • fixtures / --fixtures <file> loads { "users": [...], "orders": [...] } into every namespace at creation and again on each reset.

For flow suites that do not care about Junction identity, identity: "adopt-users" (or --identity adopt-users, or PUT /__admin/identity {"mode": "adopt-users"} per namespace) creates an unknown, well-formed user_id on first use — in create_order and GET /v3/orders?user_id= — and the request proceeds. The journal marks the request adopted: true. Orders are never adopted: an unknown order id always 404s, because inventing an order hides real bugs. The default, strict, is Junction's own behaviour.

Is this the emulator?

An emulator 404 and a sandbox 404 are byte-identical, which is how a backend half-pointed at the sandbox goes unnoticed. Three things make it visible:

  • x-mockingbird on every response. Assert on it in a test helper:

    TypeScript
    import { expect } from "vitest"
    
    /** Wrap the fetch your Junction client uses; fails any call that did not reach the emulator. */
    export const mockOnlyFetch =
      (inner: typeof fetch = fetch): typeof fetch =>
      async (input, init) => {
        const response = await inner(input, init)
        expect(response.headers.get("x-mockingbird"), `${String(input)} did not reach the Junction emulator`).toMatch(
          /^junction@/,
        )
        return response
      }
  • The request journal. Each namespace keeps its last 1000 requests (--journal-size, journalSize): operation id, method, path, status, duration, fault id, the user / order / lab-account ids it touched, and when (emulator clock). GET /__admin/requests?operationId=create_order_v3_order_post proves an order was placed here; an empty answer proves it was not. Never bodies.

  • Miss headers on 404s. A missing user or order keeps Junction's body byte for byte and adds x-mockingbird-miss: order <id> (or user <id>, user client:<client_user_id>) and x-mockingbird-known: users=<n> orders=<n>.

--log-requests prints the same fields as one JSON line per request.

Order control

simulate_order (POST /v3/order/{id}/test) behaves like the sandbox's, including its stepwise rules. For direct control, the admin API moves an order straight to a status, recording the same event, dates and appointment cascade, and publishing the same webhook, a real transition does:

Shell
curl -X POST localhost:8787/__admin/orders/$ORDER/transition -d '{"to": "collected"}'
curl -X POST localhost:8787/__admin/orders/$ORDER/transition -d '{"to": "completed", "result": "critical"}'
curl -X POST localhost:8787/__admin/orders/$ORDER/transition -d '{"to": "cancelled.testkit.do_not_process"}'

to is a full status (completed.testkit.completed), a <phase>.<event> pair, or a name that maps onto the contract's own status for the order's collection method:

Name at_home_phlebotomy on_site_collection testkit walk_in_test
requisition_created received.…requisition_created same same same
scheduled collecting_sample.…appointment_scheduled — — collecting_sample.…appointment_scheduled
shipped / delivered — — collecting_sample.…transit_customer / …with_customer —
collected collecting_sample.…draw_completed sample_with_lab.…draw_completed collecting_sample.…transit_lab —
at_lab — sample_with_lab.…draw_completed sample_with_lab.…delivered_to_lab —
partial_results sample_with_lab.…partial_results same — same
completed / corrected / cancelled / failed completed.…completed / completed.…corrected / cancelled.…cancelled / failed.…sample_error same same same

A name with no real status for the method (collected for a walk-in test) is refused with the valid choices, never approximated. result picks a named result — normal, abnormal, critical, missing_results — which steers the generator so the payload keeps Junction's exact shape. For an exact payload, PUT /__admin/results/{id} with { "results": [...], "missing_results": null, "interpretation": "critical", "pdf_base64": "…" }.

Same-second cancel. See the opt-in sandbox quirks.

There is no isTestCancelled in Junction's contract; cancelling produces the vendor's cancelled.<method>.cancelled event and webhook, which is what a consumer derives that flag from.

Time. Every timestamp reads the runtime clock. POST /__admin/clock {"advance": "2h"} makes delayed simulations (?delay=<seconds>) due and moves appointment windows, with no sleeping.

Faults and error shapes

Error bodies the emulator reproduces byte for byte, checked against the sandbox by verify and the parity suite (ORDER_NOT_FOUND holds the unknown-order text per operation id):

Case Status Body
GET /v2/user/{unknown id} 404 {"detail":"Not found"}
GET /v2/user/resolve/{unknown client_user_id} 404 {"detail":"User not found"}
Unknown order: get order, requisition PDF 404 {"detail":"This order doesn't exist"}
Unknown order: cancel, simulate 404 {"detail":"Order doesn't exist"}
Unknown order: result, result metadata, result PDF 404 {"detail":"Order not found"}
Unknown order: every phlebotomy / PSC appointment route 404 {"detail":"This order doesn't exist."} (trailing period included)
Missing x-vital-api-key 401 {"detail":"Missing x-vital-api-key"}
Request validation 422 {"detail":[{"type":…,"loc":[…],"msg":…,"input":…}]} (Pydantic shape)
Business rule 400 {"detail":"<message>"} or {"detail":{"error_type":"INVALID_REQUEST","error_message":"…"}}

Faults reproduce the sandbox's failure modes on demand, for retry paths and error handling:

Preset Fault Fidelity
sandbox_user_quota create_user → 400 {"detail":{"error_type":"INVALID_REQUEST","error_message":"You have reached the maximum of 50 Sandbox users"}} byte-exact, as observed from a sandbox team
rate_limited 429 {"detail":"Too Many Requests"}, retry-after: 1 shape-plausible, not verified
server_error / bad_gateway / unavailable 500 / 502 / 503 shape-plausible, not verified
TypeScript
import { createRuntime, FAULT_PRESETS } from "@crvouga/mockingbird-service-junction"

const junction = createRuntime()
junction.faults.add({ ...FAULT_PRESETS.sandbox_user_quota, id: "quota", count: 1 }) // next create_user only
junction.faults.add({ id: "flaky-orders", operationId: "create_order_v3_order_post", status: 503, rate: 0.25 })

A rule matches on operationId, method, pathPrefix and namespace (all optional), fires count times or forever, for a rate of matching requests (seeded, so a run replays identically), after an optional delayMs. A rule added through the admin API defaults to the calling namespace, so one worker's injected failure never lands on another's request; one added in-process, as above, applies to every namespace unless given a namespace. Over HTTP: POST /__admin/faults with the same fields, or POST /__admin/faults/presets/sandbox_user_quota.

A rule can also match on the request itself: path, pathPattern, requestHeaders, query, and requestBody, a map from a dotted path in the JSON body to the value it must equal. That targets one member when the backend sends no namespace header, with an optional body to answer with:

Shell
# Refuse one member's order
curl -X POST localhost:8787/__admin/faults -d '{"method":"POST","path":"/v3/order",
  "requestBody":{"patient_details.email":"member-a@example.test"},"status":503}'
# No phlebotomy slots for one ZIP code
curl -X POST localhost:8787/__admin/faults -d '{"method":"POST",
  "pathPrefix":"/v3/order/phlebotomy/appointment/availability",
  "requestBody":{"zip_code":"00000"},"status":200,"body":{"slots":[],"timezone":"America/Phoenix"}}'

A body that is missing, not JSON, or not a JSON object never matches. See the fault rule reference.

Webhooks

With webhooks / --webhook-url, every event is delivered signed, with retries:

  • Standard Svix signatures: svix-id, svix-timestamp, and svix-signature: v1,<base64 HMAC-SHA256 of "<svix-id>.<svix-timestamp>.<body>">, keyed by the base64 part of whsec_…. The official svix verifier, and a receiver written for real Junction, accept it unchanged. (Signing "<timestamp>.<body>" without the message id, as this package's own server script did before, is not Svix-compatible.)
  • Timestamps are wall-clock even when the emulator clock is moved, because receivers reject timestamps outside a few minutes of their own clock.
  • Any 2xx is delivered; anything else is retried on Svix's schedule (immediately, 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h) or retryDelaysMs. POST /__admin/webhooks/flush runs pending retries now; POST /__admin/webhooks/{id}/replay redelivers one message.
TypeScript
import { Webhook } from "svix"
import { createRuntime } from "@crvouga/mockingbird-service-junction"

const secret = "whsec_bW9ja2luZ2JpcmQtdGVzdC1zZWNyZXQ="
const junction = createRuntime({
  webhooks: { url: "http://127.0.0.1:3100/webhooks/junction", secret, retryDelaysMs: [0, 500, 2000] },
})

// The application's receiver, verifying with the official svix package as it does for real Junction:
const accept = async (request: Request) => {
  const body = await request.text()
  new Webhook(secret).verify(body, {
    "svix-id": request.headers.get("svix-id") ?? "",
    "svix-timestamp": request.headers.get("svix-timestamp") ?? "",
    "svix-signature": request.headers.get("svix-signature") ?? "",
  }) // throws unless the signature is valid
  return JSON.parse(body) as { event_type: string }
}
console.log(typeof accept, junction.webhooks?.deliveries().length) // "function" 0

Verify

verify is the fidelity guarantee: it runs the same requests against real Junction and the emulator and reports every divergence.

Shell
npx mockingbird-junction verify --real-key "$JUNCTION_SANDBOX_KEY" --corpus test/junction-corpus.json
npx mockingbird-junction verify --real-key "$JUNCTION_SANDBOX_KEY" --orders --sample 50 --json
  • Drift: re-fetches each recorded observation (or --sample n of them) and reports any that no longer match production — the signal to re-pull.
  • Scenario: creates a user, reads it back, provokes the documented 404s (including every order-scoped operation against a random unknown order), reads the catalog (paged and bare) and a covered ZIP's serviceability, and deletes the user; with --orders, also places, reads and cancels an order for the corpus's first lab test, and — when the corpus has an account with an empty team_id_allowlist — orders through it with and without its id. Status codes must match; bodies must match in shape, and byte for byte for the documented error bodies.

It exits 1 on any divergence. The scenario always deletes its user (and cancels its order), because it runs against the same capped sandbox the emulator exists to relieve. verifyAgainstReal is the same check as a function. This repo runs it in CI against the Junction sandbox.

Determinism

With a fixed clock (clock / POST /__admin/clock {"set": …, "freeze": true}) and seed, a run replays exactly. Stable across runs: user ids, order ids, event ids, sample ids, booking keys and webhook message ids (derived from sequence numbers), timestamps (from the clock), generated availability, and which requests a partial-rate fault hits. Wall-clock by design: webhook svix-timestamp, /__admin/health uptime, and request log durations.

What is and is not modelled

An emulator that is silent about its gaps is how a green suite starts lying. Specifically:

  • Corpus-backed (byte-exact): area and PSC serviceability for covered ZIPs; the catalog, labs, expected results and lab accounts of the team the corpus was recorded from.
  • Synthetic (deterministic, shape-faithful, values invented): availability slots and booking keys; results (biomarker lines and PDFs) unless a fixture is installed; requisition PDFs; coverage for any ZIP in --geo synthetic mode.
  • Not modelled:
    • delegated_flow-specific ordering. Accounts carry the field and orders follow the documented selection and billing rules above, but any additional behaviour Junction applies to delegated accounts is not reproduced. Run verify --orders with your team's corpus to check what you rely on.
    • Rate limits and outages, except when injected as faults or switched on as limits.
    • Lab/team-specific omission defaults must be configured through labAccountOmissionPolicies (see Lab accounts); exact vendor requisition layout is not modelled.
    • Adopted users (adopt-users) have client_user_id equal to their user_id; nothing in Junction corresponds to adoption.
    • Real 429 / 5xx bodies: the presets are plausible, not recorded.
    • Phlebotomy availability breadth: the sandbox serves it only for 85004; the emulator generates slots for any covered ZIP.
    • Webhook retry jitter across a snapshot restore (the jitter stream is not part of a snapshot).
    • Operations outside the vendored OpenAPI subset: they return 404 and are counted as unmatched in /__admin/metrics.

API

Main entry (@crvouga/mockingbird-service-junction, runtime-neutral):

Export Description
createRuntime (options?: JunctionRuntimeOptions) => JunctionRuntime — the served emulator (health, admin, namespaces, clock, faults, metrics, journal, webhooks) as one fetch. Options: corpus, geo, zipOverlays, catalogOverlays, markerCatalog, addOnSourceMarkers, labAccounts, teamId, limits, identity, defaultBillingTypes, labAccountBehavior, quirks, fixtures, journalSize, webhooks, onWebhook, sqlite, clock, seed, adminKey, onLog.
JunctionAPI Class. new JunctionAPI(options?): one namespace's emulator, implementing fetch(request: Request): Promise<Response>.
JUNCTION_NAMESPACE "junction" — the default namespace's storage key when sharing a sqlite client.
document The vendored Junction OpenAPI document (Mockingbird subset) that drives routing.
operationIds Every operationId in document.
supportedOperationIds The operationIds the emulator implements (all of them).
pullCorpus (options: PullCorpusOptions) => Promise<SealedCorpus> — record a corpus from a real team (GETs only).
diffCorpus (before, after) => CorpusDiff — ZIPs, lab tests, lab accounts and observations added / removed / changed.
fingerprintCorpus (corpus) => Promise<string> — SHA-256 of a corpus's content.
corpusLabel (corpus) => string — the short label /__admin/health and logs show, e.g. v1-2026-09-18-HqFDuEAuhkwD.
parseSealedCorpus (value: unknown) => SealedCorpus; throws on a non-object or unsupported version.
SEALED_CORPUS_VERSION Current corpus format version (2: adds teamId).
SUPPORTED_SEALED_CORPUS_VERSIONS Versions parseSealedCorpus loads ([1, 2]).
MOCK_TEAM_ID The team id used when neither teamId nor the corpus gives one.
OTHER_TEAM_ID A team that is not the emulator's, for fixtures modelling another team's account.
isLinkedToTeam (account, teamId) => boolean — the allowlist rule ([] counts as linked).
LAB_ACCOUNT_PRESETS The lab-account presets, without ids.
LAB_ACCOUNT_PRESET_ALIASES Deprecated preset names → the preset each stands for (bioreference_customer_multi_state → bioreference_platform).
SYNTHETIC_LAB_ACCOUNT_PRESETS Presets that model a requested shape, not a recorded account.
BILLING_TYPES The billing_type values create_order accepts.
presetAccountId (name) => string — a preset's default id.
PLATFORM_ACCOUNT_STATES / DELEGATED_ACCOUNT_STATES The recorded 47- and 48-state client-bill lists.
LabAccountConflict Thrown by addLabAccount for an id that exists.
DEFAULT_LIMITS Every sandbox limit, off.
sandboxUserQuotaBody (max) => body — the sandbox's user-cap 400 body.
IDENTITY_MODES ["strict", "adopt-users"].
FixtureError Thrown by the fixture inserts; status is 400, 404 or 409.
ORDER_NOT_FOUND Junction's unknown-order detail, by operation id.
MISS_HEADER / KNOWN_HEADER "x-mockingbird-miss" / "x-mockingbird-known".
verifyAgainstReal (options: VerifyOptions) => Promise<VerifyReport> — the verify command as a function.
DEFAULT_JUNCTION_BASE_URL "https://api.sandbox.tryvital.io".
isSandboxKey / SANDBOX_KEY_PREFIXES Whether a key is a sandbox team key (sk_us_ / sk_eu_).
UNKNOWN_ZIP_STATUS / UNKNOWN_ZIP_ERROR_TYPE 424 / "MOCKINGBIRD_UNKNOWN_ZIP" — the corpus-mode unknown-ZIP error.
labAccountFromInput (input: LabAccountInput) => LabAccountRecord — normalize (and validate) a configured account.
TEAM_LAB_ACCOUNTS The built-in lab-account fixtures.
US_STATES The 50 USState codes Junction accepts.
ORDER_STATUSES_BY_METHOD Every order status in the contract, by collection method.
TRANSITION_ALIASES The transition names above, by method.
resolveTransition (method, target) => { status } | { error } — how /__admin/orders/{id}/transition reads to.
RESULT_FIXTURES Named results (normal, abnormal, critical, missing_results).
FAULT_PRESETS Named Junction faults (sandbox_user_quota, rate_limited, server_error, bad_gateway, unavailable).
createWebhookDispatcher (endpoint: WebhookEndpoint) => WebhookDispatcher — signed delivery with retries, standalone.
signSvix / verifySvix Produce / check a standard Svix svix-signature.
observationCacheKey (url, method = "GET", body?) => string — the "<METHOD> <path>?<sorted query> <json>" key corpora use.
prefetchCoverageObservations Parity helper: records coverage-ZIP reads from the sandbox into an observation cache before seedFrom.
reshapeCoverageGeoCommand Parity-walk hook that pins geo/availability parameters onto the coverage corpus.
COVERAGE_ZIPS The ZIPs the shipped corpus covers.
PSC_AVAILABILITY_ZIPS ZIPs that also get PSC availability observations.
PHLEBOTOMY_AVAILABILITY_ZIPS ZIPs the sandbox serves phlebotomy availability for (["85004"]).
PSC_LAB_IDS Lab ids used for PSC reads ([4, 6, 13, 25]).
AVAILABILITY_ADDRESS Stable Phoenix, AZ address used in availability requests so cache keys match.
AVAILABILITY_START_DATE Far-future availability start_date ("2099-06-15").

./server (Node): createServer(options?) => Promise<JunctionServer>, serveTarget (what serve and serve --config use), loadCorpus(source), DEFAULT_PORT (8787). ./corpus: defaultCorpus.

JunctionRuntime (from createRuntime) members: fetch, instance(namespace?), namespaces(), reset(namespace? | "*"), snapshot(namespace?), restore(snapshot, namespace?), clock (now, set, advance, freeze, unfreeze, reset, state), faults (add, list, remove, clear), metrics (report, reset), journal (list, clear, size), webhooks (deliveries, replay, flush, idle, clear), sqlite, rng.

JunctionAPI members:

Member Description
fetch(request) Handle one Junction REST request.
reset() Promise<void> — clear state; re-install the corpus or default catalog, and the configured lab accounts.
installCorpus(corpus) Install a SealedCorpus: observations (shared, read-only), catalog, labs, lab accounts, ZIP coverage.
corpusInfo() CorpusInfo | undefined — label, recording date, source and counts.
geoMode "corpus" | "synthetic" | "corpus+synthetic", settable.
zipOverlays ZIP aliases and overrides for this namespace; settable, kept across reset().
configureLabAccounts(layout?) / labAccounts() Replace (a list, or { presets, accounts }; undefined restores the default) / read the team's lab accounts.
addLabAccount(input) / addLabAccountPreset(name, id?) / patchLabAccount(id, patch) / removeLabAccount(id) Change one account; kept across reset().
teamId The team this namespace answers as.
limits / configureLimits(input) Read / change the sandbox limits; kept across reset().
identity "strict" | "adopt-users", settable.
defaultBillingTypes Lab slug → the billing_type an order gets when it omits one (default billing type); settable, kept across reset().
insertUsers(users) / insertOrders(orders, { emitWebhooks? }) / importFixtures(fixtures, options?) / hardDeleteUser(id) The fixture backdoor.
order(id) / orders() Read orders.
orderLabAccount(id) The lab account id an order was placed with (null: Junction's platform account; undefined: unknown order).
transitionOrder(id, status, { now, flags? }) Move an order to a full status and publish its webhook.
installResultFixture(orderId, fixture) / resultFixture(orderId) Serve an exact result payload / PDF for an order.
seedFrom(source, observations?) Promise<SeedReport> — copy users, orders, appointments, catalog and labs from a real Junction environment.
ensureLabTests(source, ids) / ensureOrders(source, ids) Promise<number> — fetch specific lab tests / orders from source if missing.
markUserDeleted(userId) Mark a user as deleted (subsequent reads behave like a deleted user).
webhookEvents() JunctionWebhookEvent[], oldest first.
webhookDeliveryAttempts() The retry schedule Junction would follow per event (a record; see webhooks for delivery).
app / sqlite The underlying Hono app and SqliteClient.

Options and types:

text
type JunctionAPIOptions = {
  sqlite?: SqliteClient          // share one client across services; default: fresh in-memory DB
  namespace?: string             // storage namespace; default "junction"
  now?: () => number             // clock in ms; default Date.now
  corpus?: SealedCorpus          // installed now and on every reset()
  geo?: "corpus" | "synthetic" | "corpus+synthetic"   // default "corpus" with a corpus, else "synthetic"
  zipOverlays?: { aliases?: Record<string, string>; overrides?: Record<string, { phlebotomy_served?: boolean; no_pscs?: boolean }> }
  catalogOverlays?: CatalogOverlay[]    // lab tests cloned from catalog ones, now and on every reset()
  markerCatalog?: MarkerCatalogEntry[]  // markers GET /v3/lab_tests/markers offers on top of the catalog's
  addOnSourceMarkers?: boolean          // add-on result lines carry source_markers; default false
  labAccounts?: LabAccountInput[] | { presets?: (string | { name; id? })[]; accounts?: LabAccountInput[] }
  teamId?: string                // default: the corpus's recorded team, else MOCK_TEAM_ID
  limits?: { maxUsers?: number | null; simulateRequiresSandbox?: boolean; rateLimitPerSecond?: number | null }
  identity?: "strict" | "adopt-users"  // default "strict"
  defaultBillingTypes?: Record<string, BillingType>  // lab slug → billing_type when omitted; default client_bill
  fixtures?: { users?: UserFixture[]; orders?: OrderFixture[] }  // loaded now and on every reset()
  onWebhook?: WebhookPublisher   // called with every recorded event
  webhook?: JunctionWebhookOptions
}
type LabAccountInput = { id; lab; status?; delegated_flow?; account_name?; provider_account_id?; org_id?;
  business_units?; default_clinical_notes?; allowed_billing?: Record<billingType, State[]>; states?: State[];
  team_id_allowlist? }
type WebhookEndpoint = { url; secret: "whsec_…"; retryDelaysMs?: number[]; timeoutMs?; headers?; fetch? }
type ResultFixture = { name; results?: unknown[]; missing_results?: unknown[] | null; interpretation?; pdf_base64? }
type FaultRule = { id; operationId?; method?; pathPrefix?; status; body?; headers?; count?; rate?; delayMs? }
type JunctionWebhookOptions = { seed?: number; jitterRatio?: number /* 0..1, default 0.1 */; timeoutMs?: number }
type JunctionWebhookEvent = {
  event_type: "labtest.order.created" | "labtest.order.updated" | "labtest.appointment.updated"
  data: Record<string, unknown>; team_id: string; user_id: string; client_user_id: string
}
type SealedCorpus = { version: 1 | 2; recordedAt; source; teamId?; fingerprint?; observations; catalog: { labTests; labs; expectedResults }; labAccounts }
type SeedSource = { fetch: (request: Request) => Promise<Response>; baseUrl: string; headers: Record<string, string> }
type OperationId / SupportedOperationId  // string unions of operationIds / supportedOperationIds

Seeding from the real sandbox (needs network and a real key):

TypeScript
import { JunctionAPI } from "@crvouga/mockingbird-service-junction"

const junction = new JunctionAPI()
const report = await junction.seedFrom({
  fetch: (request) => globalThis.fetch(request),
  baseUrl: "https://api.sandbox.tryvital.io",
  headers: { "x-vital-api-key": process.env.JUNCTION_API_KEY ?? "" },
})
console.log(report.users, report.orders)

Development

For contributors to the mockingbird repo only.

Shell
bun test                                   # offline suites, incl. scheduling state space
bun test junction.runtime.property.test.ts # service contract, corpus, lab accounts, faults, webhooks, verify
bun test junction.sdk.property.test.ts     # drives the served emulator through @tryvital/vital-node

bun run emulator:serve                         # env-configured `serve`: HOST, PORT, MOCKINGBIRD_JUNCTION_CORPUS,
                                           # MOCKINGBIRD_JUNCTION_WEBHOOK_URL / _SECRET / _SCOPE
bun run corpus:record -- --force           # re-record the shipped corpus/sandbox-sealed.json (JUNCTION_API_KEY)
bun run verify -- --real-key "$KEY"        # `mockingbird-junction verify` from source

Live parity (primary proof is seedParity: warmup N on the sandbox, seedFrom, then lockstep M):

Shell
bun run parity                                   # default mode=seed against api.sandbox.tryvital.io
bun run parity -- --warmup 15 --compare 30 --runs 25
bun run parity -- --mode=empty                   # legacy empty-start differential

Webhook parity needs a public receiver; see docs/webhook-parity.md.

Asynchronous testkit simulation

A completion-target simulation on an active testkit advances through delivery and queues completion for the next millisecond of the injected now clock. This is a deterministic emulator scheduling boundary, not a claim about Junction's processing latency. Order, result, transaction, and order-list reads apply due work to its owning order. A second simulation is not required. Results remain unavailable while the testkit is delivered_to_lab. Cancelled orders never complete from queued simulation work.

The parity harness polls order reads after successful immediate completion simulations on both targets. Testkit completion must settle within 15 seconds or verification fails; explicitly delayed simulations and unsuccessful requests are not treated as settled. The USSL testkit result metadata follows the observed sandbox fixture (null specimen dates and CLIA 05D2130115); other laboratories retain their existing metadata behavior.

Part of mockingbird — agent integration guide: README · llms.txt · report an issue or request a feature.

Lab-account behavior profiles

createRuntime({ labAccountBehavior: { profile, accounts } }) selects a named policy. native is the backward-compatible default. expected-contract enforces the documented lab/account association and billing rules, including rejecting a selected account belonging to another lab. This is an assumption from the public contract, not measured production parity. Both expected-contract and sandbox-observed return the persisted account association in POST/GET; a synthetic requisition uses that same stored provider account number.

For #440, the scoped account-owned billing control is a best guess, not oracle-verified. The reporter observed an explicit client_bill refusal for a root-selected delegated BioReference account even though its listing allowed that billing in AZ. They reported 400 {"detail":"Lab 13 does not support billing type patient_bill_passthrough"} in the sandbox and the same error with Lab 14 in production, including sandbox panel/add-on, home/walk-in and physician-present controls. These are report-derived errors; neither their cause nor a general vendor billing rule has been established. The repository's 2026-10-20 sanitized probe could not find matching account/test inventory, recorded no order observations and created no user or orders. This evidence limit does not prevent a suite from testing the explicit fallback.

Configure the reported refusal against the ID of a synthetic account loaded by labAccounts or your corpus. For example:

TypeScript
import { createRuntime } from "@crvouga/mockingbird-service-junction"

const delegatedAccountId = "11111111-1111-4111-8111-111111111111"
const runtime = createRuntime({
  labAccounts: [{
    id: delegatedAccountId,
    lab: "bioreference",
    org_id: "55555555-5555-4555-8555-555555555555",
    delegated_flow: "order_delegated",
    allowed_billing: { client_bill: ["AZ"] },
  }],
  labAccountBehavior: {
    profile: "observed",
    oracle: "sandbox",
    accounts: {
      [delegatedAccountId]: {
        rejectBilling: { client_bill: "Lab 13 does not support billing type patient_bill_passthrough" },
      },
    },
  },
})

Load a BioReference panel or marker catalog in the same runtime's corpus, then submit your normal valid order with root lab_account_id: delegatedAccountId and billing_type: "client_bill". This configuration returns the exact configured 400 detail before creating an order or webhook, including when physician is present. It applies to both panel and add-on orders and home/walk-in collection. To model the production report, choose oracle: "production" and change the configured detail to Lab 14 ...; the oracle label describes the intended environment, not proof of verification. sandbox-observed remains an alias for observed with oracle: "sandbox".

The fallback is deliberately narrow: only that listed account's explicit root ID and explicit billing activate rejectBilling. Other delegated accounts keep their own policies. Omitting billing_type still uses defaultBillingTypes (or client_bill when no default is configured) and checks the account's listed billing. Thus this fixture accepts an omitted default client_bill even though it refuses explicit client_bill; configuring an omitted patient_bill_passthrough default rejects because the account lacks that billing, independently of rejectBilling. This distinction is the chosen emulator contract, not a verified production omission behavior. Nested-only IDs do not select the customer account. In the sandbox profile they follow the platform fallback with null POST/GET association; a successful root-selected account retains its persisted ID on both responses. Never treat nested-only acceptance as a successful delegated-account selection or retry a rejected customer order by dropping its ID.

These controls do not model why Junction overrides the requested billing, validate physician selection, or establish any production omission/returned-identity rule. Native behavior remains the default. The separately reported NY/NJ acceptance uses the allowBilling positive control below. GET/PUT /__admin/lab-account-behavior inspect/change one namespace's policy; changes survive its reset and never affect another namespace. Unknown profile names are rejected. CLI/server equivalents are --lab-account-profile and --lab-account-sandbox-policies <json-file> (the account-policy object).

sandbox-observed also encodes the owner-recorded October 2026 sandbox omission controls (#439), which are recorded observations, best guess, not oracle-verified by this repository (its sandbox probe still finds no matching account inventory; no live POST/GET identity comparison has been run here): an order whose root lab_account_id is omitted, nested only in order_set, or an unknown UUID is placed on the lab's platform account (lab_account_id: null on POST and GET, no listed allowed_billing check), while a root ID naming a listed account keeps its identity and policy. expected-contract keeps the strict behavior (unknown ID → 400, omission → the house account). The native profile keeps its historical response shape (no lab_account_id on the order); the explicit profiles return the stored association, never the requested id, so a test that asserts identity on both POST and an independent GET cannot pass on an ignored id. junction.account-profiles.test.ts holds the positive control and the six negative controls.

Explicit Junction-owned account in an uncovered state (#494)

Under sandbox-observed (or observed with oracle: "sandbox"), a root lab_account_id naming a Junction-owned account (org_id: null) whose listing has no billing type for the patient's state is refused with 400 {"detail":"Lab <lab name> does not support state <ST>"} (for example Lab BioReference does not support state AZ) before any billing-type check, and no order or webhook is created. This follows the reported sandbox refusal (2026-10-19); it is a best guess, not oracle-verified here, and the reporter did not re-probe a covered state. It is kept apart from the billing-type message (Lab 13 does not support billing type ...) that a customer-owned account returns. A state that the account lists for another billing type keeps the billing message, an account-owned allowBilling exception counts as coverage, and customer-owned accounts, native, expected-contract and the production oracle keep their existing messages (the state rule is a sandbox observation only).

The explicit profiles also accept order_set.add_on with provider_ids or marker_ids from one lab's configured marker catalog. The selected markers determine the synthetic custom panel; unresolved or cross-lab combinations refuse without placing an order. Combining lab_test_ids and add_on in one request is explicitly refused by these profiles until the combined-panel semantics are verified. Malformed add-ons also refuse without placing an order; requested add-on markers are never silently discarded. Panel and custom orders share account/billing checks for home and walk-in collection. The native profile keeps its existing panel-only contract.

Explicit NY/NJ customer-account billing (#441)

The positive control for #441 is a best guess, not oracle-verified. The reporter observed a non-delegated customer-owned BioReference account accepting explicit client_bill in NY despite advertising only patient_bill_passthrough for NY/NJ. The reported POST and independent GET both retained the requested account ID and client_bill. The public billing rules require the requested billing type and patient state to be listed; they do not establish this account-specific exception. The repository's 2026-10-20 sandbox probe returned complete: false with matching account inventory unavailable, no observations and no resources created. No vendor acceptance was verified by that run.

Load a synthetic active BioReference customer account with delegated_flow: "not_delegated" and allowed_billing: { patient_bill_passthrough: ["NY", "NJ"] }, plus an orderable BioReference panel. Set labAccountBehavior to the following, replacing the synthetic UUID with that fixture account's ID:

JSON
{
  "profile": "observed",
  "oracle": "sandbox",
  "accounts": {
    "22222222-2222-4222-8222-222222222222": {
      "allowBilling": { "client_bill": ["NY", "NJ"] }
    }
  }
}

Submit a valid order with that ID at the root lab_account_id, explicit billing_type: "client_bill" and a NY or NJ patient address. POST and independent GET preserve both the selected account ID and client_bill; the account listing still advertises only its original patient_bill_passthrough states. This models an ordering exception without rewriting inventory or changing the requested billing. Including NJ is an explicit fixture assumption from the reported NY/NJ account, not an observed NJ oracle response.

The exception applies only to the configured account, explicit billing type and configured states under observed (or its sandbox-observed alias). An unconfigured account or a state outside NY/NJ still fails the listed billing/state checks. Omitting billing_type evaluates defaultBillingTypes against the listed account billing: the default client_bill rejects, while an explicitly configured patient_bill_passthrough default accepts in NY/NJ and is returned as such. Native and expected-contract profiles retain their existing validation. The separate delegated refusal in #440 still requires its own rejectBilling entry; neither control establishes a universal rule based on delegation or lab. See junction.account-profiles.test.ts for the acceptance cases and strict negative controls.

Observed profile and oracle label

profile: "observed" with oracle: "sandbox" | "production" is the general form of sandbox-observed (an alias that always reports oracle: "sandbox"), so GET /__admin/lab-account-behavior says which vendor environment a configuration reproduces. Account-owned rejectBilling / allowBilling observations apply to either oracle, per account and never as a blanket delegated-account rule: a delegated customer account refuses explicit client_bill only when its own entry says so. The October 2026 sandbox omission recordings (#439) are sandbox observations, so they apply only to oracle: "sandbox"; with oracle: "production" an omitted account selects the house account as expected-contract does, except that the production oracle defaults omittedAccountBilling to { bioreference: "unchecked" } (#495, below). Whether production also refuses a delegated account when billing_type is omitted is unverified, so it is not modelled. oracle on any other profile, or production on the sandbox-observed alias, is refused.

Omitted house-account billing (#467)

A reporter observed production accepting an AZ client_bill order with no lab_account_id on a team whose house BioReference account lists only patient_bill_passthrough for NY and NJ, placing it on the house account (its requisition prints the house account number). That is a single consumer report, not a repository probe, so the default is unchanged (the listing is enforced). profile: "observed", oracle: "production" is the named profile for this production recording (#495) and turns it on for BioReference by default, best guess, not oracle-verified (sandbox, by contrast, places an omitted-id order on the platform account with a null association, see #439 above). With it, an omitted lab_account_id places the order on the Junction-owned account, the requisition names that account's number, and the account listing is unchanged (it still lists client_bill for no state). Set omittedAccountBilling: { bioreference: "listed" } to turn it off. For any other profile, opt in per lab:

TypeScript
import { createRuntime } from "@crvouga/mockingbird-service-junction"

createRuntime({
  labAccountBehavior: {
    profile: "expected-contract",
    omittedAccountBilling: { bioreference: "unchecked" },
  },
})

With unchecked, an order whose root lab_account_id is omitted (a nested-only id is ignored) and that resolves to a Junction-owned account (org_id: null) skips that account's listed billing and state check. The order keeps the house account's identity on POST, GET and the requisition. Explicit selection of the house account keeps its listed policy (or an account-owned rejectBilling), and a customer-owned account chosen by routing keeps its own listing. GET/PUT /__admin/lab-account-behavior read and change omittedAccountBilling.

Where an order's account id was sent (#473)

GET /__admin/orders adds lab_account_request: { labAccountId, nestedLabAccountId } per order (instance().orderLabAccountRequest(id)): the id carried at the request root, which selects the account, and the one nested in order_set, which Junction ignores. Each is null when absent. The request body itself is still not stored; orders seeded by a fixture have lab_account_request: null.

Sandbox quirks

Behaviors the sandbox shows that clients must tolerate, every one off by default (createRuntime({ quirks }), or per namespace GET/PUT /__admin/quirks; they survive reset()).

Quirk Effect
sameSecondCancelStatus (#474) POST /v3/order/{id}/cancel in the same wall-clock second as the order's creation records a cancelled.<method>.cancelled event with the same timestamp, but status, last_event and updated_at stay received; a repeat cancel is a no-op. A cancel at least one second later reads back cancelled.

This was reported from the sandbox (not re-measured here, production unverified), so it is a best guess, not oracle-verified. It is off by default; a client that tests it should read the event log and wait at least a second between creating and cancelling. The quirk does not publish a webhook or cascade-cancel appointments for the stale cancel; the report does not say what the vendor does there.

Requisition text layout (#464)

The synthetic requisition PDF prints two text lines: Requisition <order id> and Account number: <provider number> (unknown when the order has no frozen number), so an extractor that accepts a field label only at the start of a line finds it. The layout is for interoperability, not vendor pixel fidelity.

↑↓ to navigate↵ to open