@haau3/sdk
v0.2.10
Published
TypeScript SDK for the haau3 platform API: appointment slots from SMART Scheduling Links publishers, Medicare Advantage and ACA Marketplace plan benefits, medication list reconciliation, RxNorm and LOINC terminology, the CMS National Provider Directory an
Maintainers
Readme
@haau3/sdk
The TypeScript SDK for the haau3 platform API, as typed calls:
- Scheduling: open appointment slots from many SMART Scheduling Links publishers, in one search.
- Plan Benefits: Medicare Advantage and ACA Marketplace plans as FHIR
InsurancePlan, searchable by state or county, and which providers are in a plan's network. - MedList: one reconciled medication list from one patient's FHIR medication records.
On the same key and FHIR base: RxNorm and LOINC lookups, value sets and code validation (Terminology), practitioners and organizations from CMS's National Provider Directory (Provider directory), and standard FHIR Questionnaires such as the PHQ-9.
The haau3 platform API is in preview: it may change as we build, usually to add features or improve performance.
Also for Python: haau3 on PyPI, and for Go:
github.com/haau3/haau3-go.
Install
npm install @haau3/sdkNode 22 or newer. The SDK runs on a server only: it refuses to start in a browser, where your API key would be public.
Authenticate
Create an API key on platform.haau3.com, then set it as
HAAU3_API_KEY or pass it as apiKey.
import { Haau3 } from "@haau3/sdk";
const haau3 = new Haau3(); // reads HAAU3_API_KEYA first call
const plan = await haau3.readInsurancePlan("H2406-013-000");
console.log(plan.name); // "AARP Medicare Advantage from UHC FL-0021 (PPO)"FHIR answers are @types/fhir's R4 types, also
exported as fhir4 (import type { fhir4 } from "@haau3/sdk").
One call per service
// Scheduling: open appointment slots, here in Boston.
const { slots, total } = await haau3.searchSlots({ city: "Boston", state: "MA", limit: 10 });
// Provider directory: a practitioner by NPI, from CMS's National Provider Directory.
const practitioners = await haau3.searchPractitioner({ identifier: "1093209884" });
// Terminology: a LOINC code's display and properties.
const glucose = await haau3.codeSystemLookup({ system: "http://loinc.org", code: "2345-7" });
// Questionnaires: a form by its id, such as the PHQ-9.
const phq9 = await haau3.readQuestionnaire("44249-1");
// Plan Benefits: a Medicare Advantage or ACA Marketplace plan, and which providers are in its network.
const plan = await haau3.readInsurancePlan("H2406-013-000");
const network = await haau3.checkInsurancePlanNetwork("H3359-038-000", { npi: ["1518929058"] });
// Pharmacy Benefits: what a medication would cost, as the CARIN pharmacy benefit check, from a FHIR
// message Bundle. A member's check carries patient data, so send it only under an executed agreement.
const checked = await haau3.processMessage(message);
// MedList: one reconciled medication list from one patient's FHIR medication records. It receives
// patient data, so send it only under an executed agreement.
const list = await haau3.reconcileMedications(records);Every method, grouped by service, with its request and a link to its page on docs.haau3.com, is in api.md. Each is named by its operationId, with its parameters and its answer typed from the published spec.
With an AI coding agent
An agent can read the docs directly:
- docs.haau3.com/llms.txt lists every docs page as Markdown.
- docs.haau3.com/skill.md is an
Agent Skill that says what the platform does and how to call
it.
npx skills add https://docs.haau3.comadds it to your project's coding agents. https://docs.haau3.com/mcpis an MCP server that searches the docs. To add it:- Claude Code:
claude mcp add --transport http haau3 https://docs.haau3.com/mcp - Codex:
codex mcp add haau3 --url https://docs.haau3.com/mcp - Cursor, in
.cursor/mcp.json:{ "mcpServers": { "haau3": { "url": "https://docs.haau3.com/mcp" } } } - VS Code:
code --add-mcp '{"name":"haau3","type":"http","url":"https://docs.haau3.com/mcp"}'
- Claude Code:
Options
| Option | Default | |
|---|---|---|
| apiKey | process.env.HAAU3_API_KEY | Your platform API key. |
| baseURL | process.env.HAAU3_BASE_URL, then https://api.haau3.com | Where the API is. The API key is sent there only. |
| timeout | 65000 | Milliseconds one attempt may take. |
| maxRetries | 1 | Retries after a timeout, a network failure, a 429 or a 5xx. |
| fetch | the global fetch | The fetch to send requests with. |
| fetchOptions | { cache: "no-store" } | Passed on to fetch with every request, for any runtime. |
Each call takes { timeout, maxRetries, signal, correlationId, headers, fetchOptions } as its last
argument, for that call only:
await haau3.searchSlots({ state: "MA" }, { timeout: 10_000, maxRetries: 0 });fetchOptions passes anything your runtime's fetch takes, on the client or on one call: a proxy
dispatcher from undici in Node, Cloudflare's cf, Next.js's next, keepalive. The SDK's own
method, headers, body, signal and redirect always win.
import { ProxyAgent } from "undici";
const haau3 = new Haau3({ fetchOptions: { dispatcher: new ProxyAgent("http://proxy.internal:8080") } });Unless fetchOptions sets cache, every request is sent with cache: "no-store" where the
runtime's fetch takes the field (Node's does; on a runtime that refuses it, the SDK sends none). An
answer is live data for your key, not something a fetch cache should keep. Frameworks that cache
fetch follow it: in Next.js it also keeps next build from sending a request while it prerenders
a page, and { next: { revalidate: 60 } } caches an answer there instead. { cache: undefined }
sends no cache at all.
A retry waits a short, growing interval, or what the API's Retry-After header says when that is
60 seconds or less. A longer Retry-After fails at once, carrying it.
Errors
Every error is a Haau3Error, with the requestId the API answered with. There are three kinds,
and AuthError has a subclass for a 403:
import { AuthError, ForbiddenError, NoAnswerError, RefusedError } from "@haau3/sdk";
try {
await haau3.readInsurancePlan(id);
} catch (error) {
if (error instanceof RefusedError) {
error.status; // 400, 404, 410 and so on
error.outcome; // the OperationOutcome; Scheduling's errors carry `problem`
} else if (error instanceof NoAnswerError) {
error.reason; // "timeout", "network", "rate-limited", "unavailable", "server-error" or "unreadable"
error.retryAfter; // seconds, when the API said
error.detail; // for a timeout or a network failure: what failed, such as "ECONNREFUSED" or "TimeoutError"
} else if (error instanceof ForbiddenError) {
// 403: the key was accepted, and its organization may not make this request
error.outcome; // why, in the API's words
} else if (error instanceof AuthError) {
// 401: the key was missing, unknown or revoked
}
}ForbiddenError extends AuthError, so test for it first, as above. A 403 never means the key is
wrong: there is nothing to rotate, and the body says what the organization may not do.
No answer is never reported as an empty result, and an empty result is never an error. An error's
message never includes the body the API answered with, and the body (outcome or problem) is
never printed when the error is logged. Every error the API answered carries attempts and
durationMs too.
A FHIR answer is checked against the resource types its operation answers with. Anything else, a
Bundle where an InsurancePlan was asked for, say, is a NoAnswerError with reason
"unreadable". The check is of the resource type, not of every element inside it: those are typed
as FHIR R4 declares them, and the SDK does not validate each one.
error.toJSON() gives an error as a plain object that is safe to log: its name, its message and the
fields above that are set, and never the body or the cause. JSON.stringify(error) prints the
same.
Request ids and timing
Every method's answer can be awaited as it is, or through .withResponse() for the request id and
timing that came with it, in the same names request() uses:
const { data: plan, requestId, durationMs, attempts } = await haau3.readInsurancePlan(id).withResponse();Quote the requestId when you ask for help: it finds the request in the API's own logs.
To find a request by an id of your own, such as your job's or your trace's, pass it as
correlationId. It must be a UUID, which is all the API keeps; anything else is refused before it
is sent. The API keeps it with its record of the request, and it comes back on the answer and on
every error the API answered:
const { data, correlationId } = await haau3.readInsurancePlan(id, { correlationId: jobId }).withResponse();Paging
pages() yields a FHIR search's first page and every page after it, following each next link on
the client's own host and stopping at a link to a page already read:
const first = await haau3.searchInsurancePlan({ "coverage-area-state": "NC" });
for await (const page of haau3.pages(first)) {
for (const entry of page.entry ?? []) console.log(entry.resource?.id);
}resources() yields every resource the search found, across every page:
for await (const resource of haau3.resources(first)) {
if (resource.resourceType === "InsurancePlan") console.log(resource.name);
}It yields each entry whose search mode is match, or says none. A resource an _include or
_revinclude brought, and a search's own OperationOutcome, are not among them; pages() gives
every entry. Scheduling's searches page with limit and offset, and answer with a total.
matches() gives one page's matches of a type, typed, with no request, and resolve() finds the
resource a reference names in that page, such as one an _include brought:
const page = await haau3.searchHealthcareService({
"service-category": "pharm",
"location.address-state": "FL",
"location.address-postalcode": "33131",
_include: ["HealthcareService:organization", "HealthcareService:location"],
});
for (const pharmacy of haau3.matches(page, "HealthcareService")) {
const organization = haau3.resolve(page, pharmacy.providedBy, { type: "Organization" });
const store = haau3.resolve(page, pharmacy.location?.[0], { type: "Location" });
console.log(pharmacy.name, organization?.identifier?.[0]?.value, store?.address?.city);
}resolve() follows FHIR's rules for references in a Bundle: an absolute reference is the entry with
that fullUrl; a relative one is resolved against the address of the entry holding it when you pass
that resource as from, and otherwise is the one entry whose address ends in that type and id; #id
is found in from's contained. It is undefined when the page does not hold the resource, and
throws when two entries could be the one meant or the resource is not of type.
Several requests in one
batch() sends several operations of the FHIR base as one request. The function you pass gets a
builder with the client's own methods, called the same way, which make entries instead of sending.
Each entry gets its own result, in the order given:
const [plan, counties] = await haau3.batch((b) => [
b.readInsurancePlan("H2406-013-000"),
b.listInsurancePlanCounties({ state: "NC" }),
]);
if (plan.ok) {
plan.data; // fhir4.InsurancePlan
} else {
plan.error; // a RefusedError or a NoAnswerError, as readInsurancePlan alone would have thrown
}A list built in code works the same way:
const lookups = await haau3.batch((b) =>
codes.map((code) => b.codeSystemLookup({ system: "http://loinc.org", code, property: "SHORTNAME" })),
);Each entry is checked as the call alone is, and fails alone while the others answer. An entry that
fails is not retried. The batch itself is retried, and throws, as any call does, and
.withResponse() gives its request id. The API takes up to 100 entries in up to 1 MiB and gives a
batch 60 seconds, so send a longer list as several batches. Every operation on the FHIR base can go
in a batch except processMessage and reconcileMedications, which the spec marks
x-haau3-batchable: false.
To send a batch Bundle you built yourself, fhirBatch() takes it and returns the
batch-response as it is.
Any request
request() sends a request as you give it and returns what the API answered, whatever the
status, with its status text, headers, request id, attempts and duration:
const { status, statusText, body, requestId } = await haau3.request({ method: "GET", path: "/v1/scheduling/publishers" });Versions
The SDK is 0.y.z while the API is in preview: a minor release may change what a method takes or
answers, and a patch release never does. CHANGELOG.md lists every release, and a
release that takes a new version of the API's spec names that version.
