AHA

@crvouga/mockingbird-service-aha

Stateful mock of the AHA at-home phlebotomy partner API: HMAC-signed create-order and cancel, raw or wrapped envelopes, idempotency keys, and the order-status webhooks (Scheduled, Check Out, …) our bloodwork handler consumes.

Work in progress. Usable, but incomplete: operations, response shapes and options can still change between releases. Pin an exact version. Check the README's "Deliberately not modelled" notes before relying on it.
Work in progressHealth & labs Runs in the browser2 of 2 operationsIn-process fetchNode servermockingbird-aha
$npm install -D @crvouga/mockingbird-service-aha

Playground

Requests go to the real mock 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-aha into this tab on first send
https://stage-api.mobileaha.com

Unix epoch milliseconds. base64(HMAC-SHA256(apiSecret, "<apiKey>:<path>:<timestamp>")).

Send a request to see the mock's response.

Operations

2 of 2 operations in the vendored contract are mocked. Hand-authored from aha.service.ts, aha.types.ts (zod request/response schemas) and aha.lab-provider.ts. Success responses come in one of two envelopes (raw, or {success, data}) because our two clients disagree (G-A1); the mock serves either.

MethodPathOperationStatus
POST/v1/geviti/create-orderCreateOrderCreate an order, or update it when partner_order_id already exists (the same order_number is returned). Honors X-Idempotency-Key.
POST/v1/geviti/cancelCancelOrderCancel an order by partner_order_id. The mock also accepts the AHA order_number here, because our lab-provider client sends it in that fi…

Documentation

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

Stateful mock of the AHA (Advanced Health Academy) at-home phlebotomy partner API for test suites: HMAC-signed create-order and cancel, and — its main job — the order-status webhooks AHA posts back. The vendor has no pull API, so every downstream effect (EMR appointment booking, storefront status, "blood drawn") starts with a webhook; the mock emits one on demand, with every field our handler reads, so the ZIP-routed bloodwork path can finally be tested.

  • Operation coverage: SUPPORT.md
  • The vendor publishes no spec: the contract (openapi.yaml) is hand-authored from our consumers' zod schemas and wire shapes.

Install

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

ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with npx mockingbird-aha serve, createServer from ./server (Node), or createRuntime with any Fetch server.

Usage

Point the app at the mock:

Env Value
AHA_API_URL http://127.0.0.1:8799 (the lab-provider path already allows loopback; AhaService needs an http exception, see G-A1 / S10.2)
AHA_API_KEY / AHA_API_SECRET anything, or the pair passed as --api-key / --api-secret to verify signatures exactly
AHA_USE_LEGACY_AUTH true switches to X-Geviti-Auth-Key; both modes are accepted
AHA_WEBHOOK_SECRET the same value as --webhook-secret
Shell
npx mockingbird-aha serve --port 8799 \
  --webhook-url http://127.0.0.1:3000/bloodwork/aha-webhook \
  --webhook-secret "$AHA_WEBHOOK_SECRET" \
  --api-key "$AHA_API_KEY" --api-secret "$AHA_API_SECRET" \
  --envelope raw --auto-schedule 2000
TypeScript
import { createRuntime } from "@crvouga/mockingbird-service-aha"

const aha = createRuntime({
  webhooks: { url: "http://127.0.0.1:3000/bloodwork/aha-webhook", secret: "aha-webhook-secret" },
})
const admin = (path: string, body: unknown) =>
  aha.fetch(
    new Request(`http://aha.test/__admin${path}`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(body),
    }),
  )

// …the app's checkout calls POST /v1/geviti/create-order for GV-101…

// AHA books the draw: our handler books the EMR appointment for 10:30 Denver time.
await admin("/orders/GV-101/transition", {
  status: "Scheduled",
  scheduledAt: "2026-10-01T16:30:00Z",
  timeZone: "America/Denver",
})
// The phlebotomist checks out with a sample: our handler sets vitalBloodDrawn.
await admin("/orders/GV-101/transition", { status: "Check Out", drawStatus: "Sample Collected" })

Routes

Route Behaviour
POST /v1/geviti/create-order Validates the body (partner_order_id, patient fields, biological_sex, service_type, npi, ordering_physician, test_codes, optional preferred_schedule_date/time, patient_timezone). Create or update: a repeated partner_order_id keeps its order_number. Answers {content: {partner_order_id, order_number}, message, status: "SUCCESS"}.
POST /v1/geviti/cancel {partner_order_id, notes: [{note_type: "CANCELLATION", notes}]}{message, status}. Emits a Cancelled webhook (turn off with cancelWebhook: false). Unknown id → 404; an order whose sample was collected → 200 with status: "ERROR". The AHA order_number is accepted in partner_order_id too, because our lab-provider client sends it there (G-A1).

Auth. HMAC mode: X-API-KEY, X-TIMESTAMP (epoch ms, within ±5 min of wall-clock time), X-SIGNATURE = base64 HMAC-SHA256(secret, "<apiKey>:<path>:<timestamp>"), where path is the request path without host, body or /ns/<name> prefix. With a known key + secret (--api-key/--api-secret or credentials in settings) the signature is verified exactly; with none configured any key is accepted and the signature is checked for shape only. Legacy mode: X-Geviti-Auth-Key (+ X-API-Version: 1.0). Failures are 401 {status: "ERROR", message}.

Envelope (G-A1). AhaService expects the raw {content, message, status}; AhaLabProvider expects {success: true, data: {…}}. Raw is the default; choose with --envelope raw|wrapped or per namespace with PUT /__admin/settings {"envelope": "wrapped"}.

Idempotency. X-Idempotency-Key (the lab-provider path): the same key and body replays the stored response (idempotent-replayed: true); the same key with a different body is 409.

Webhooks

POST <webhook-url> (our route: POST /bloodwork/aha-webhook) with Authorization: Token <AHA_WEBHOOK_SECRET>. Every body has status, partnerOrderId, ahaOrderId, plus, by status (all local times in the order's IANA zone):

status Extra fields
Scheduled, Rescheduled scheduleServiceTime (YYYY-MM-DDTHH:mm:ss, moment-parsable) and scheduleServiceTimeZone (IANA) — required by our handler though absent from the DTO — plus scheduledServiceDate/Time/TimeZone and scheduleConfirmationDate/Time/TimeZone
Check In checkInDate, checkInTime, checkInTimeZone
Check Out drawStatus (default Sample Collected), drawStatusDate/Time/TimeZone
Lab Testing In Progress dropOffDate, dropOffTime, dropOffTimeZone
Cancelled, Non Scheduled Update

drawStatus values: Sample Collected, Completed (drawn), Patient Refused, UTO, Patient Not Home, Patient Rescheduled, Order Cancelled, Others, Patient Asked to Reschedule (draw failed). Non-2xx answers are retried (immediately, 5 s, 5 min, 30 min, 2 h); GET /__admin/webhooks, …/events, …/replay, …/flush as usual.

Admin (beyond the standard contract)

Route Effect
POST /__admin/orders/:partnerOrderId/transition {status, drawStatus?, scheduledAt?, timeZone?} emits the webhook. status is any value above (case and _ forgiven; unknown values are sent verbatim). scheduledAt (ISO or epoch ms) defaults to the order's preferred slot, else the next hour 24 h out; Rescheduled defaults to one day later. timeZone defaults to the order's patient_timezone, else America/New_York. :partnerOrderId may also be the order_number.
PUT /__admin/settings {envelope?, credentials?: [{apiKey, apiSecret?}], allowLegacy?, timestampToleranceMs?, defaultTimeZone?, cancelWebhook?, autoSchedule?: {afterMs, leadMs?} | ms | null} for the calling namespace. GET shows them with secrets masked.
POST /__admin/tick Emit every autoSchedule webhook that is due on the mock clock (the served mock ticks every 100 ms).
GET /__admin/orders The namespace's orders (ids, status, appointment, zone — no patient data).

Fault presets (POST /__admin/faults {"preset": "<name>", "count"?: n}): bad_signature (401), rate_limited (429 → rate_limit), server_error (500), order_error (200 with inner status: "ERROR"), invalid_response (200 with a body neither zod schema accepts), webhook_duplicate, webhook_reorder, webhook_drop.

Namespaces

x-mockingbird-namespace, a /ns/<name> prefix on AHA_API_URL (the signature still covers only the path after it), or by API key: PUT /__admin/credentials {"credentials": {"<AHA_API_KEY>": "<namespace>"}}.

Deliberately not modelled

  • Results. AHA delivers results as HL7 over SFTP into S3 (ahaResultFilesaha_results_queue); none of that is HTTP.
  • Real scheduling: AHA contacts the patient; nothing moves unless a test transitions the order or sets autoSchedule.
  • The serviceable-ZIP list (our app's own fixture decides eligibility before calling AHA).
  • Patient details are validated, never stored or echoed.
  • Which envelope the real vendor uses (G-A1): both are served, one per namespace.

API

Export Kind Description
AhaAPI class The in-process mock: fetch(request), reset(), transition(id, {status, drawStatus?, scheduledAt?, timeZone?}), tick(), orders(). Options: sqlite, now, namespace, settings, onWebhook, wallClock.
createRuntime function The mock with the full service contract. Options: webhooks: {url, secret, retryDelaysMs?, fetch?}, settings, tickMs, wallClock, clock, seed, adminKey, onLog.
AHA_PRESETS object Every named fault preset.
AHA_NAMESPACE string The service name, "aha".
WEBHOOK_PATH string "/bloodwork/aha-webhook", our receiver's route.
ORDER_STATUSES, DRAW_STATUSES arrays The vendor status spellings the webhooks use.
verifyAuth function The HMAC / legacy verification the mock applies (an error message, or undefined).
apiKeyCredential function The API key a request carries (how credentials map to namespaces).
isTimeZone, zonedParts, zonedToEpoch functions IANA-zone helpers used to fill the local date/time fields.
document, operationIds, supportedOperationIds values The vendored OpenAPI contract and its operation ids.
createServer, serveTarget, DEFAULT_PORT (./server) Node Serve over node:http (autoSchedule ticks every 100 ms); the serve CLI target; port 8799.

Part of mockingbird.

to navigate to open