A dependency-light Go SDK and shn CLI for participating in the Smart Health
Network — the secure router for exchanging healthcare data between participants. It
implements the participant wire protocol — holder identity, per-operation
authorization, sealed envelopes, and FHIR payloads — so a Go participant integrates
without running the Smart Gateway binary. The first workflow it carries end-to-end is
Da Vinci prior authorization (CRD+DTR+PAS), with the payer's decision record on its
Patient Access API.
Depends only on the Go standard library, golang.org/x/crypto, and
github.com/samply/golang-fhir-models. It never imports SHN-internal code.
Preview network — synthetic data only. Never send production PHI. Not for production deployment.
- Register a participant client (self-serve, invite-gated).
- Run eligibility checks against synthetic covered / not-covered members.
- Run a full CRD → DTR → PAS prior-authorization flow.
- Exercise the approved, pended → amended, and denied scenarios.
- Validate your setup end-to-end with
shn doctor.
The network uses synthetic data only — never send production PHI.
This is not a production connection to the Smart Health Network. It is a public SDK and CLI for exercising the participant wire protocol — holder identity, per-operation authorization, sealed-envelope routing, FHIR payloads, and the prior-authorization scenarios — against synthetic data only.
It helps you test technical readiness for CMS-0057-style prior-authorization workflows. It does not by itself certify regulatory compliance: each participant remains responsible for its own compliance, data quality, policies, endpoints, and operational readiness.
Provider / Partner Smart Health Hub Payer / Responder
────────────────── ──────────────── ─────────────────
build FHIR payload
seal + authorize ────────▶ route sealed envelope ─────▶ open + validate
verify authz + audit; adjudicate
cannot read payload
open + verify ◀──────── route sealed response ◀───── seal + authorize
bound response
The Hub routes sealed envelopes and verifies each leg's authorization metadata. It holds no decryption key and cannot read the payload.
docs/PREVIEW.md— start here: install → register → run → validate, with the exactshncommands.docs/PARTICIPANT_PROTOCOL.md— the language-neutral wire protocol for direct integration.docs/TECHNICAL_ARCHITECTURE.md— the system architecture and security model.testdata/vectors/README.md— canonical wire vectors (the SDK's hermetic conformance contract).SUPPORT.md— where to ask, and what to include.SECURITY.md— private vulnerability reporting.CONTRIBUTING.md— how this snapshot relates to the internal repository; versioning.
go get github.com/SmartHealthNetwork/shn-sdk
From shn-sdk v1.0.0, each release attaches a signed shn CLI.
The CLI:
go install github.com/SmartHealthNetwork/shn-sdk/cmd/shn@latest
The preview network is live at shn-preview.org (synthetic data only). The public surfaces:
| Service | Base URL |
|---|---|
Hub (POST /route) |
https://hub.shn-preview.org |
Authorization Framework (POST /authorize) |
https://authz.shn-preview.org |
Registrar (POST /register) |
https://registrar.shn-preview.org |
FHIR / Patient Access (GET /metadata) |
https://fhir.shn-preview.org |
| Developer accounts / client registration | https://accounts.shn-preview.org |
accounts.shn-preview.org is the Accounts service — the self-serve
developer-onboarding control plane for client registration, Cognito-gated (browser
login, token cached at ~/.shn/credentials). It also serves the machine-readable
discovery descriptor at GET /discovery (live endpoints, responders, seeded
personas).
New here? Start with the getting-started guide:
docs/PREVIEW.md — the Install → Discover → Register → Build →
Run → Validate path with the exact shn commands.
You also need the payer's holder id + X25519 public key and the Authorization Framework's Ed25519 verifying key — published in the network's holder feed / manifest. Keys are generated client-side (proof-of-possession): your private keys never leave your process.
Use this path to test the public preview network. Registration is invite-gated and goes
through the Accounts service; keys are generated locally and your private keys never
leave your machine. The browser sign-in happens once and the token is cached at
~/.shn/credentials.
# 1. Log in (opens a browser for sign-in; token cached at ~/.shn/credentials).
shn login --accounts https://accounts.shn-preview.orgOn a machine with no browser (SSH session, container, CI), use the headless flow — it auto-activates over SSH and on Linux without a display, or force it with the flag:
shn login --accounts https://accounts.shn-preview.org --no-browser
The CLI prints a sign-in URL. Open it in a browser on any machine, sign in, and the
page shows a login code — paste it back into the CLI prompt. Codes expire after about
5 minutes; if the CLI reports the code expired, just re-run shn login. Only paste
codes from a shn login you started yourself — treat a sign-in URL someone sends you
as a phishing attempt.
# 2. Register a client (keys generated locally; only public keys are sent to the
# Accounts service). The holder id is server-assigned, e.g. acme-7f3a.
shn register --accounts https://accounts.shn-preview.org \
--role provider --name acme --base-url https://acme.example -out ./keys
# 3. Validate your setup (eligibility + prior-auth round-trips; see the known issue
# under "Self-validate" below).
shn doctor --discovery https://accounts.shn-preview.org --id acme-7f3a -keys ./keys
# 4. Run a prior-authorization (CRD→DTR→PAS) yourself. The payer, the endpoints and
# the persona's order are resolved from the discovery descriptor.
shn priorauth --member MBR-D-UC04 \
--discovery https://accounts.shn-preview.org --id acme-7f3a -keys ./keys
# → outcome=pended needed=… resume=shn-resume.jsonManage your clients with shn clients --accounts <url> (list) and
shn revoke <id> --accounts <url> (revoke). To re-key an existing holder, rotate is
a holder-self RFC 7592 path you run directly against the registrar
(shn rotate <id> --registrar <url>) — it never goes through the Accounts service.
shn register and shn rotate declare the request frames this build supports
(currently v1,v1op). Declare v1op (framed DTR operations) only when the Smart
Gateway serving your --base-url is v0.44.0 or newer: for an older gateway pass
--request-frames v1, and run shn rotate (without the option) after you upgrade
it. Rotating issues new keys into -out: restart your gateway with that key
directory. See docs/PARTICIPANT_PROTOCOL.md §6.3.
They also declare the contract versions your gateway declares, which peers select
your gateway's lines by. shn register declares this build's default
(currently pa.crd@2.0,pa.dtr@2.0,pa.pas@2.0,pa.pdex@2.1) unless you pass
--contract-versions with your gateway's set: its SHN_CONTRACT_VERSIONS, or the
set its boot line prints. shn rotate re-declares the set the registrar holds for you
now (read from its public /holders feed), unless you pass --contract-versions,
and refuses to rotate when it cannot read that set. After you change your gateway's
declared set, rotate with --contract-versions and the new set: a rotate without it
re-declares the old one. Every run prints the set it sends.
Drive a gateway's prior-authorization scenarios end-to-end with
shn send-test --gateway <gateway-url> — it fires all eight use cases at a
running gateway's /scenario routes and tabulates pass/fail (--json for
machine-readable output). Handy when you're evaluating a gateway you've stood up
from the shn-gateway
evaluation bundles. From shn-sdk v0.62.0 it also takes --discovery <accounts-url>: when
the network's discovery descriptor states the scenario routes' personas
(scenarioPersonas), each scenario sends its persona (member, order and claim facts),
which a gateway from shn-gateway v0.62.0 requires; otherwise it sends what earlier
releases sent.
For operator-managed deployments (not self-serve onboarding), registration is gated by an operator-admin credential supplied out of band. Keys are still generated locally; your private keys never leave your machine.
# 1. Generate keys + a public manifest snippet (private keys stay local, 0600).
shn keygen --id ext-provider --role provider \
--base-url https://ext-provider.example.com -out ./keys
# 2. Register directly against the registrar with the operator-admin assertion.
shn register --role provider --name ext-provider \
--base-url https://ext-provider.example.com \
--registrar https://registrar.shn-preview.org \
--admin-assertion "$ADMIN_ASSERTION" -out ./keysMost developers should use the self-serve Accounts path above. The direct
POST /register path is for operator-managed and non-self-serve environments (see
docs/PARTICIPANT_PROTOCOL.md §2.3).
One command answers "am I wired up + do my eligibility AND prior-auth round-trips conform". It fetches the discovery descriptor and runs eligibility for every persona it advertises, then — once eligibility passes — runs a prior authorization (CRD→DTR→PAS) for each persona that advertises an expected outcome, amending the pend where a persona also advertises an outcome after an amendment. It uses your OWN registered identity and needs no FHIR validator (the network validates server-side).
shn doctor --discovery https://accounts.shn-preview.org --id acme-7f3a -keys ./keys
# ✓ network discovery reachable …
# ✓ wire protocol "1.1.0" supported
# ✓ test counterparties resolve in the directory (1 payer(s))
# ✓ your client "acme-7f3a" is registered
# ✓ MBR-D-UC01: covered=true (expected "covered")
# ✓ MBR-D-UC01-NC: covered=false (expected "not-covered")
# …
# ✓ priorauth MBR-D-UC04: pendedKnown issue: the amend step. On the preview environment the reference payer answers an amendment with another pend and decides later, and
shn doctordoes not yet inquire for that decision, so a full run fails at the amend step with exit code40. Run the other personas one at a time with--persona. The amend step sends the supplemental report the network states for the persona, as it is, and builds none. See docs/PREVIEW.md §4 for both.
Checks run in order — the network first, then the wire-version check (before any eligibility leg), then your registration and outcomes — with a stable exit code per phase so a script can tell whose problem a failure is:
| Code | Phase | Meaning |
|---|---|---|
| 0 | — | all checks passed |
| 2 | usage | a required flag is missing, or --persona names no advertised persona |
| 10 | network health | discovery/authz/registrar/payer unreachable or missing, or an eligibility or prior-authorization exchange failed |
| 20 | wire version | the network speaks a wire version this CLI doesn't — upgrade |
| 30 | your registration | your client isn't in /holders (run shn register, or it was revoked), or your keys could not be loaded |
| 40 | outcome | an eligibility run returned the wrong coverage, or a prior-auth run returned the wrong outcome |
Use --persona <memberId> to run a single seeded persona.
id, _ := shnsdk.GenerateIdentity("ext-provider") // ed25519 + X25519 keypairs
covered, reason, err := id.RunEligibility(ctx, http.DefaultClient,
shnsdk.Endpoints{
HubURL: "https://hub.shn-preview.org",
AuthzURL: "https://authz.shn-preview.org",
},
// Identity is the payer as the member's coverage names it: the request's insurer.
shnsdk.Payer{ID: "payer", Identity: payerIdentifier, EncPub: payerEncPub, AuthzPub: authzPub},
"1234567890", // ordering NPI
"MBR-COVERED", "1975-04-02", "Johansson",
)
// RunEligibility: identify the patient → build CoverageEligibilityRequest → authorize the
// leg → seal+route the envelope → verify the bound response token → open → parse.accounts/— developer-account sign-in (loopback-PKCE and headless copy-paste code) + Accounts API client — shared by theshnCLI and the SHN Kit.
Most users drive everything through the shn CLI (above). Use the Go API
directly if you are building a native integration or test harness.
| Symbol | Purpose |
|---|---|
GenerateIdentity(holderID) |
Fresh Ed25519 (signing) + X25519 (encryption) keypairs. |
ResolvePCI(memberID, birthDate, familyName) |
The network's patient identifier for a member, for a custom system-of-record connector. Opaque: carry it as returned and as tokens name it; do not parse it, reimplement how it is produced, or depend on its format beyond the pci: prefix. |
Identity.Assertion(audience, now, ttl) |
Signed holder assertion (the X-Holder-Assertion header value). |
Identity.Authorize(ctx, client, authzURL, req) |
Obtain a per-operation, scope-bound Token. |
Identity.Registration(role, baseURL) |
Build a proof-of-possession RegistrationRequest. |
Identity.RunEligibility(ctx, client, endpoints, payer, npi, member, dob, family) |
End-to-end coverage-eligibility round-trip. |
Seal(meta, payload, recipientEncPub) / Open(env, encPub, encPriv) |
NaCl anonymous-sealed-box envelope crypto (payload-blind routing). |
EncodeEnvelope / DecodeEnvelope |
Envelope wire (JSON) codec. |
BuildEligibilityRequestExact / ParseEligibilityResponse |
FHIR CoverageEligibilityRequest / …Response helpers: the request names the payer by the identifier your member's Coverage states (from v0.62.0; the Deprecated BuildEligibilityRequest names the placeholder Organization/payer and is removed in the release after it). |
BuildCRDRequest(CRDRequestInputs{…}) / ParseCRDResponse |
CRD request (a CDS Hooks order-select, order-sign or order-dispatch request) built from your own Patient, your Coverage search result (or null) and the hook's context, each resource as your exact bytes, with no fhirServer or fhirAuthorization (CRD 2.2.1's request model marks both 1..1; the network omits them by design — the payer answers from context and prefetch, and the network never hands a payer a route into your system); ParseCRDResponse → a CRDObservation: every order the payer returned with its coverage information (from update/create system actions, card suggestions, or the card extension object earlier SDK versions wrote; a card extension object of any other shape is the card author's own and is not read as coverage), every sub-extension exactly as sent, and .Primary() → a CardCoverage (Covered, PANeeded, Questionnaires[], SatisfiedPaID) with .PARequired() / .NeedsDTR() helpers. Deprecated: BuildConformantOrderSelectRequest / BuildConformantOrderDispatchRequest (they take no Patient, so from v0.59.0 they build nothing and return an error naming BuildCRDRequest; earlier releases sent an id-only Patient) and ParseCards. |
BuildCRDResponse(line, CRDResponseInputs{…}) / CheckCDSHooksResponse(body, line) |
A payer's CRD answer: each order returned in an update system action carrying the Da Vinci coverage-information extension with your own coverage-assertion-id, and cards: [] unless you supply cards for a person to read (each with source.label and source.topic); values the CRD line does not define are refused. CheckCDSHooksResponse certifies any CDS Hooks response against the CDS Hooks 2.0 response rules and the line's CRD card rules and returns every Violation{Rule, Path, Severity}; CDSHooksRules() lists the rules with their specification text. Deprecated: BuildCards / BuildCardsAtLine, which build nothing and return ErrBuildCardsReplaced (a card extension object is not a CRD answer, and a CardCoverage names no order). |
BuildQuestionnairePackageParameters(line, QuestionnairePackageInputs{…}) / ExtractQuestionnaireFromPackage / ParseQuestionnaireURL |
DTR $questionnaire-package input Parameters at a DTR line: your Coverage(s) and the order(s) (after CRD, the payer's updated order from ParseCRDResponse) embedded as your exact bytes, the records the payer needs to resolve references in them (Referenced — the payor Organization your Coverage names by reference, above all: a payer that maps payer identity at its edge refuses a Coverage whose payor it cannot resolve) as referenced parameters, each questionnaire canonical exactly as the payer stated it (a |version kept), and the payer's coverage-assertion-id as context when you hold one; .Copied reports each embedded span. Send it as a framed questionnaire-package operation, and only to a payer whose requestFrames declares "v1op" (SupportsRequestFrameV1Op; otherwise refuse: ErrFramedDTRUnsupported) — see docs/PARTICIPANT_PROTOCOL.md §6.3. The response is a Da Vinci $questionnaire-package collection Bundle (Questionnaire + dependent Libraries/ValueSets) — ExtractQuestionnaireFromPackage returns the bare Questionnaire, then ParseQuestionnaireURL reads its canonical url. (BuildQuestionnairePackage is the responder-side wrapper; a Responder whose Adjudicator implements NextQuestionAdjudicator also serves next-question.) Deprecated: BuildQuestionnaireFetch / BuildQuestionnaireFetchWithCoverage, the older questionnaire request for a payer that has not declared "v1op" (the WithCoverage form carries your Coverage, which a pa.dtr@2.2 responder needs for its QuestionnaireResponse shell's coverage reference). |
FillQuestionnaire(questionnaireJSON, cc, qc) |
Fill the built-in prior-auth DTR questionnaire into a conformant QuestionnaireResponse (LOCAL answers + information-origin attribution). Targets only the built-in questionnaire: FAILS LOUDLY on an unrecognized questionnaire (never a half-filled QR). |
FillQuestionnaireFromAnswers(questionnaireJSON, answers, author, qc) |
Fill ANY DTR questionnaire into a conformant QuestionnaireResponse from a caller-supplied map[string]Answer (keyed by linkId; an Answer carries a typed value or an AnswerCoding{System,Code,Display}), with information-origin attribution — for manually/attestation-sourced answers when the questionnaire isn't the built-in one. |
BuildConformantClaimBundleExact(ConformantClaimInputs{QR, SR, Provider, Coverage, Insurer, PatientRef, CoverageRef, MemberID, Corr, Created, Payer, PASClaimFacts}) / ParseClaimResponse |
PAS preauthorization submit Bundle (from v0.62.0 it carries your stated claim facts, PASClaimFacts, and writes no default; the Deprecated BuildConformantClaimBundle keeps the v0.61.1 bytes and is removed in the release after it) (the conformant Da Vinci lean shape — Claim + Patient + Coverage + requesting provider + payer Organization + ServiceRequest + QuestionnaireResponse; Created drives the deterministic bundle id/timestamp). From v0.59.0 Insurer, your own Organization record for the payer your member's Coverage names, is required on every call: it rides the Bundle as the entry Claim.insurer and Coverage.payor name, and the builder refuses without it or when it does not carry the Payer identity (earlier releases, unless PayerOrgEntry was set, named a payer Organization the SDK made up; PayerOrgEntry and ContainedInsurer are now deprecated and have no effect) + the ClaimResponse parser → PriorAuthResult (Outcome:"approved" + PreAuthRef/ValidUntil; on approvals and denials also the payer's ProcessNotes with their types, the deciding ReviewAction with its X12 886 reasons, and any CARC/RARC DenialReasons, all as sent). Denied (the X12 review-action code A3 "Not Certified" — this network's own conformant denial code; the reference payer's observed A2 denial shape is also accepted) and pended responses parse to their own outcomes; a X12 A2 that carries an authorization number parses as Outcome:"approved" + Partial:true instead (X12 306's actual meaning for A2 is "Certified – partial") — an ambiguous response returns an error, never a wrong Outcome. The amended re-POST sibling is BuildConformantClaimUpdateBundleExact(ConformantClaimUpdateInputs{…}). MemberID is required and is the bare member id ("MBR-COVERED"). It is stamped in two places: the bundle's Coverage carries it as the urn:shn:coverage Member-Number identifier, and the Claim's insurance[0].coverage is a logical reference to that same business identifier — {"identifier": {"system": "urn:shn:coverage", "value": "MBR-COVERED"}}, with no literal reference, so the bundle is self-consistent and does not ask a receiving payer to resolve an SHN-local resource id. CoverageRef stays the FHIR reference ("Coverage/MBR-COVERED") for the caller's other roles. The two are deliberately different spellings of different things — BuildCoverage/BuildCoverageWithPayer take the bare member id too, and refuse a Coverage/-prefixed value rather than stamp it. |
CoverageParty(coverage, contained) |
Whether a resource in a Coverage's contained list is the Coverage's party: a contained Patient (typically a dependent's parent carrying only an MRN) whose id no other contained resource has, that the Coverage's subscriber or policyHolder names by the slot's own reference (#<id>), that nothing else in the Coverage references, that contains nothing and lists its identifier (if present), and whose rule members are spelled exactly (no Reference, Id, Contained or Identifier variants). From shn-sdk v0.60.0 the PAS builders carry such a subscriber or policyHolder, and the contained Patient, with the same JSON values your record has (re-encoded, as every carried record is; any other subscriber or policyHolder naming someone other than the member is still refused, as is a reference in a party's slot, or in the party, to a record the request does not carry), and a Responder's PAS response does not read the party as the patient. A Smart Gateway reads it from shn-gateway v0.61.0; an earlier gateway refuses that answer at strict (docs/PARTICIPANT_PROTOCOL.md §7a.3). |
VerifyBound(tok, authzPub, now, frame, op, corr, holder, subject, payloadHash) |
Verify a token is bound to exactly this leg, INCLUDING payloadHash = sha256hex(ciphertext) (STRICT, AI-2) — the SDK verifies, never mints. Seal-then-authorize: seal the payload first, then authorize against its ciphertext. Refuses a token carrying Involvement (minted for another patient a leg involves, never a leg's own), as VerifyBoundNoPayload does. |
Also exported (responder + participation helpers; see godoc and docs/PREVIEW.md §3c):
NewResponder / ResponderConfig (the payer-side inbound responder handling all five
transaction types; an Adjudicator that also implements CoverageAssertionRecorder receives
each CRD coverage assertion it sends), ParsePayerIdentifier / ParseCoveragePayer
(coverage-derived payer routing, from a Coverage or a Bundle of Coverages naming one payer), FetchHolders
/ NewFeedEncResolver (registry-feed holder + encryption-key resolution),
FetchHubTransportKey, and WriteBundle / LoadBundle (registration-bundle I/O).
Developer-account sign-in (loopback-PKCE, browser mode; headless copy-paste code,
manual mode) and client management — the same flows the shn login / register /
clients / revoke CLI and the SHN Kit's first-run sign-in drive. Import
github.com/SmartHealthNetwork/shn-sdk/accounts to build your own sign-in flow
instead of shelling out to the CLI.
| Symbol | Purpose |
|---|---|
FetchCLIConfig(ctx, hc, accountsURL) → CLIConfig |
Fetch the Accounts service's OIDC issuer + public client id (GET {accounts}/cli-config). |
FetchOIDC(ctx, hc, issuer) → OIDC |
Fetch the issuer's OIDC discovery document (authorize + token endpoints). |
StartPKCE(hc, cfg, oidc, ports, now) → *PKCEFlow |
Start a loopback-redirect PKCE authorization-code flow on one of ports (browser mode). |
PKCEFlow.AuthorizeURL() |
The browser URL to open for sign-in. |
PKCEFlow.Wait(ctx) → Tokens |
Block until the browser redirect completes; returns the id / access / refresh tokens. |
PKCEFlow.Close() |
Tear down the loopback listener (also unblocks Wait). |
StartManualPKCE(hc, cfg, oidc, accountsURL, now) → *ManualFlow |
Start a headless PKCE flow with no loopback listener: the redirect targets the accounts service's /cli/code copy-paste page (manual mode). |
ManualFlow.AuthorizeURL() |
The hosted-UI URL to open in any browser, on any machine. |
ManualFlow.Exchange(ctx, pasted) → Tokens |
Parse the pasted "<state>.<code>" string from /cli/code, verify it belongs to this flow, and exchange it for tokens. Returns ErrCodeExpired for an expired/replayed code. |
Refresh(ctx, hc, tokenEndpoint, clientID, refreshToken, now) → Tokens |
Refresh an expired session without re-authenticating. |
EmailFromIDToken(idToken) |
The signed-in developer's email from the id token (display only). |
NewClient(baseURL, token) → *Client |
Accounts API client, authenticated with a session bearer (the id token). |
Client.Create(ctx, name, role, encPub, signPub, baseURL) → id |
Register a client; returns the server-assigned holder id. |
Client.SubmitPoP(ctx, id, reg) |
Submit the proof-of-possession for a pending registration. |
Client.List(ctx) → []ClientRow |
List the developer's registered clients. |
Client.Revoke(ctx, id) |
Revoke a client by id. |
vectors_test.go verifies the SDK against canonical wire-vectors in
testdata/vectors/ (a sealed envelope, a holder assertion, an authorize token, and
CER/CRR fixtures). It imports only shnsdk + stdlib + golang.org/x/crypto, so it is
the SDK's standalone hermetic contract. See testdata/vectors/README.md.