NewInstance Documentation
View as Markdown

Integration guide

This is the API surface for NewInstance EHR (product code ehr): REST endpoints for third-party integrations, the webhook event system, and the FHIR R4 facade. It does not cover the GraphQL schema used by the dashboards; that is internal to the platform.

Base path: /api/v1/ehr, and /fhir/r4 for the FHIR facade (section 11). Authentication: API key, header x-api-key: <keyId>:<secretKey>. Every request needs a key scoped to the organization and to the ehr product, except POST /api/v1/ehr/sms/inbound, which takes no API key and is authenticated by the SMS provider's signature (section 8), and GET /fhir/r4/metadata, which needs no authentication (section 11). Every read that returns or names a client is written to the organization's PHI access log, one entry for each client, with actor type api_key; reference codes, webhook subscriptions and the FHIR capability statement name no client and are not written to it.

Reference codes (section 2), webhook management (section 3), event replay (section 7), appointments, client lookup by medical record number and message replies from the SMS provider (section 8), the FHIR capability statement (section 11), and reads of claims and of a charge (section 12) are what is served. Enrollments, coverages, consents, documents, encounters, remittances and client import are not part of the REST surface, a client is read only by looking up its medical record number, and the billing routes write nothing.

1. API keys and EHR permission groupslink

An EHR-scoped API key is granted one or more permission groups (defaultProductApiPermissionGroups.json, productCode: "ehr"). A key with no EHR product scope at all, and without FULL_ACCESS, is rejected on every EHR route.

Group codeActionsWhat it unlocks
full-access*Every route in this document, and any other EHR route, since * carries every action
clients-readclients:readPOST /api/v1/ehr/clients/lookup, and the client identifiers carried by appointment and billing responses
clients-writeclients:read, clients:writeNo route: no client write route is built
schedulingappointments:read, appointments:writeGET and POST /api/v1/ehr/appointments
documents-readdocuments:readNo route: no document route is built
billing-readbilling:readGET /api/v1/ehr/billing/claims, GET /api/v1/ehr/billing/claims/{id}, GET /api/v1/ehr/billing/charges/{id} (section 12)
fhir-readfhir:readNo route: the only FHIR route served (GET /fhir/r4/metadata) does not require authentication at all
fhir-writefhir:read, fhir:writeNo route: no FHIR write route is built
webhookswebhooks:manageGET/POST /api/v1/ehr/webhooks, PATCH/DELETE /api/v1/ehr/webhooks/{id}, POST /api/v1/ehr/webhooks/{id}/test, GET /api/v1/ehr/events

A permission group on a key names what the key may ask for. It does not grant it. Every authenticated EHR route resolves what a key reaches as the permissions its groups name, kept to the permissions held by the organization member who created the key, so a key can never reach further than the person who issued it. A key issued by someone who has since left the organization reaches nothing.

Seven routes act on a client, and each of them also needs a key issued by a member who reaches every client: GET /api/v1/ehr/appointments, POST /api/v1/ehr/appointments, POST /api/v1/ehr/clients/lookup, GET /api/v1/ehr/billing/claims, GET /api/v1/ehr/billing/claims/{id}, GET /api/v1/ehr/billing/charges/{id} and GET /api/v1/ehr/events. A member reaches every client when they own the organization or their role holds ehr.client.all. Of the seeded roles, EHR Admin, Clinical Supervisor, Front Desk, Biller, Quality Assurance and Auditor hold it; Clinician, Clinician (co-sign required), Prescriber and Nurse read a caseload and do not. A key issued by any other member is answered 403 on all seven before a record is read, whatever groups the key carries, full-access included. Where the key would otherwise reach the route, the refusal says why and names what the key cannot do, for example: "This key was issued by a member who cannot read every client record, so it cannot book an appointment. Issue the key from a member whose role reaches every client, such as the front desk or an administrator."

Three webhook routes need the same key, because a webhook is sent events about every client: POST /api/v1/ehr/webhooks, PATCH /api/v1/ehr/webhooks/{id} and DELETE /api/v1/ehr/webhooks/{id}. A key issued by any other member is answered 403 on all three before anything is written, whatever groups the key carries, full-access included, and where the key would otherwise reach the route the refusal says: "This key was issued by a member who cannot read every client record, so it cannot configure a webhook. Issue the key from a member whose role reaches every client, such as the front desk or an administrator." Of the seeded roles only EHR Admin both manages integrations and reaches every client; a key from the front desk is refused these routes earlier, because its member cannot manage integrations. Listing subscriptions and sending a test event need only the webhooks group. What happens to a webhook once the member it is delivered under loses that reach is in section 3.

A key these routes serve gets client identifiers on appointment, claim and charge responses only when its groups ask to read clients, as clients-read does, and its member holds ehr.client.view. Any other key they serve gets the same rows with no client identifier on them.

GET /api/v1/ehr/reference-codes is the exception: it only requires the key to have the ehr product enabled at all, with any permission group (or FULL_ACCESS), not a specific group.

Keys also support resourceScopes, which can bind a key to specific programs. No EHR route consults them: every route in this document is organization-wide, so a key's resource scopes change nothing on it.

2. Reference codeslink

GET /api/v1/ehr/reference-codeslink

Looks up billing and clinical reference codes by code prefix or display text.

Query parameterRequiredNotes
systemNoOne of ICD10CM, HCPCS, POS, MODIFIER, LOINC, RXNORM, TAXONOMY
qNoSearch text, 1 to 80 characters, matched against the code prefix or a display substring, case insensitive
limitNo1 to 100, default 25

Seed data is loaded for POS (place of service), MODIFIER (HCPCS modifiers used in behavioral health billing), and a starter HCPCS subset of behavioral health service codes. ICD10CM, LOINC, RXNORM, and TAXONOMY are valid values for system but have no seeded codes, and no loader for ICD-10-CM codes is built.

Example request:

GET /api/v1/ehr/reference-codes?system=HCPCS&q=H000&limit=5
x-api-key: key_abc123:secret_xyz789

Example response, 200 OK:

{
  "items": [
    { "system": "HCPCS", "code": "H0001", "display": "Alcohol and/or drug assessment", "attributes": {} },
    { "system": "HCPCS", "code": "H0002", "display": "Behavioral health screening to determine eligibility for admission to treatment program", "attributes": {} },
    { "system": "HCPCS", "code": "H0004", "display": "Behavioral health counseling and therapy, per 15 minutes", "attributes": {} }
  ]
}

3. Webhook managementlink

All webhook endpoints require the webhooks permission group (or full-access) and, on a self-hosted deployment, a configured platform SECRET_KEY; without it, these routes return 503. Creating, changing and deleting a subscription also need a key issued by a member who reaches every client (section 1).

The {id} in PATCH /api/v1/ehr/webhooks/{id}, DELETE /api/v1/ehr/webhooks/{id} and POST /api/v1/ehr/webhooks/{id}/test is the subscription's id, 24 hexadecimal characters. Anything else, a 12 character text included, is refused with 400 "Invalid webhook subscription id", and nothing is written.

The member a webhook is delivered under. Every create, change and delete records the member who issued the key as the member who configured the subscription; a change made in the dashboard records the member who made it. A webhook is delivered only while that member is still in the organization and still reaches every client. Once they leave, or their role no longer reaches every client, nothing more is sent to it: each delivery that falls due fails at once with this reason, which the delivery log shows: "Nothing was sent, because the member who last configured this webhook no longer reaches every client or has left the organization. A member whose role reaches every client can save the webhook again and then redeliver this." A member who reaches every client takes the webhook over by saving it again, with PATCH /api/v1/ehr/webhooks/{id} or from the dashboard, and the failed deliveries can then be redelivered. A subscription a key created before this rule shipped, on 26 September 2026, recorded no member as its configurer, only the organization owner as its creator, so it stops in the same way until someone who reaches every client saves it again. One created in the dashboard before then is delivered under the member who created it.

GET /api/v1/ehr/webhookslink

Lists the organization's webhook subscriptions. The stored secret is never returned, only a masked preview.

Example response, 200 OK:

{
  "items": [
    {
      "id": "66f1a2b3c4d5e6f7a8b9c0d1",
      "name": "Claims system integration",
      "url": "https://integrations.example.com/newinstance/webhooks",
      "events": ["ehr.breakglass.started", "ehr.breakglass.reviewed"],
      "status": "active",
      "includePhi": false,
      "headers": {},
      "failureCount": 0,
      "secretMasked": "whsec_••••a1b2",
      "createdAt": "2026-09-17T12:00:00.000Z",
      "updatedAt": "2026-09-17T12:00:00.000Z"
    }
  ]
}

POST /api/v1/ehr/webhookslink

Creates a subscription and returns the signing secret in clear text once. Store it now; it cannot be retrieved again.

Body fieldRequiredNotes
nameYes1 to 120 characters
urlYesPublic HTTPS URL, up to 2000 characters
eventsYesArray of event types from the catalog in section 10, or ["*"] for every event type
includePhiNoDefault false
phiAcknowledgmentTextOnly if includePhi is trueAt least 20 characters

url must resolve to a public address. Loopback, private, link-local, multicast, and NAT64 ranges are rejected, as is every form of an IPv4-mapped IPv6 address. A hostname that cannot be resolved by DNS at creation time is accepted (DNS can be transient), but every delivery attempt against it fails until it resolves; the REST API does not accept a custom headers field, unlike the dashboard's GraphQL-backed form.

Example request:

POST /api/v1/ehr/webhooks
x-api-key: key_abc123:secret_xyz789
Content-Type: application/json

{
  "name": "Claims system integration",
  "url": "https://integrations.example.com/newinstance/webhooks",
  "events": ["ehr.breakglass.started", "ehr.breakglass.reviewed"],
  "includePhi": false
}

Example response, 201 Created:

{
  "id": "66f1a2b3c4d5e6f7a8b9c0d1",
  "name": "Claims system integration",
  "url": "https://integrations.example.com/newinstance/webhooks",
  "events": ["ehr.breakglass.started", "ehr.breakglass.reviewed"],
  "status": "active",
  "includePhi": false,
  "headers": {},
  "failureCount": 0,
  "secretMasked": "whsec_••••a1b2",
  "createdAt": "2026-09-17T12:00:00.000Z",
  "updatedAt": "2026-09-17T12:00:00.000Z",
  "secret": "whsec_5f2e1a9c8b7d6e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d"
}

PATCH /api/v1/ehr/webhooks/{id}link

Updates name, url, events, or status (active or paused) on an existing subscription. At least one field is required. Returns 200 OK with the updated subscription (no secret), or 404 if the subscription does not exist.

DELETE /api/v1/ehr/webhooks/{id}link

Soft deletes the subscription. Returns 204 No Content. It stops receiving deliveries and no longer appears in the list, but its delivery history is preserved.

POST /api/v1/ehr/webhooks/{id}/testlink

Queues an ehr.webhook.test event and a pending delivery for the subscription, so you can verify your receiver end to end. Returns 202 Accepted:

{ "eventId": "66f1a2b3c4d5e6f7a8b9c0e2", "deliveryId": "66f1a2b3c4d5e6f7a8b9c0e3" }

Returns 409 Conflict if the subscription is not active (for example, paused).

The event's data names the subscription as subscriptionId, its id as stored, in lower case, whatever case the {id} in the path was written in.

The dashboard's equivalent (sendEhrWebhookTest, and redeliverEhrWebhook) throttles both actions together to once every 10 seconds per subscription; this REST endpoint does not carry that throttle.

4. The event envelopelink

Every webhook delivery POSTs this JSON body. This is what a subscription with includePhi: false (the default) receives for ehr.breakglass.started:

{
  "id": "66f1a2b3c4d5e6f7a8b9c0d5",
  "type": "ehr.breakglass.started",
  "version": 1,
  "orgId": "66f1a2b3c4d5e6f7a8b9c000",
  "occurredAt": "2026-09-18T14:03:11.000Z",
  "aggregate": { "type": "break_glass", "id": "66f1a2b3c4d5e6f7a8b9c0d6" },
  "data": { "userId": "66f1a2b3c4d5e6f7a8b9c0d8", "expiresAt": "2026-09-18T18:03:11.000Z" }
}

A subscription with includePhi: true receives the same envelope plus a top-level "clientId": "66f1a2b3c4d5e6f7a8b9c0d7".

FieldNotes
idThe event's unique ID
typeOne of the event types in section 10
versionEnvelope version, currently always 1
orgIdThe organization the event belongs to
occurredAtISO 8601 timestamp of when the event happened
aggregateThe domain object the event is about: { type, id }
clientIdPresent only when the subscription has includePhi: true and the event names a client: the client, enrollment, encounter, document, treatment plan and break-the-glass events, ehr.consent.created, ehr.consent.revoked and ehr.consent.expiring, and an appointment event when the appointment has exactly one client. A subscription with includePhi: false, the default, never receives clientId, for any event type
dataEvent-specific fields. Unless the subscription has includePhi: true, the fields the event catalog marks as PHI for the event type, its phiFields (section 10), are removed from data: clientId and mrn on the four client events, and survivorClientId on ehr.client.merged, clientId on the enrollment and encounter events, clientId and type on ehr.consent.created and ehr.consent.revoked, clientId on ehr.consent.expiring, and reviewDueAt on the three treatment plan events. No other event type has a PHI field inside data; the document and break-the-glass events carry their only PHI, the client, in the top-level clientId, gated the same way

5. Signature verificationlink

Every delivery carries these headers:

HeaderContents
x-newinstance-signaturesha256=<hex HMAC-SHA256 of the raw request body, using the subscription secret as the key>
x-newinstance-timestampMilliseconds since epoch, as a string, when the delivery was sent
x-newinstance-event-idThe event's id, matching the envelope
x-newinstance-delivery-attemptThe attempt number for this delivery, starting at 1

Verify the signature against the raw request body (before any JSON parsing, since re-serializing can change the bytes) using a constant-time comparison, and check x-newinstance-timestamp against a tolerance window to reject replayed deliveries. The platform does not enforce a tolerance window itself; five minutes is a reasonable choice for a receiver to enforce:

const crypto = require('node:crypto');

const TIMESTAMP_TOLERANCE_MS = 5 * 60 * 1000;

function verifyEhrWebhookSignature(rawBody, signatureHeader, timestampHeader, secret) {
  const expected = `sha256=${crypto.createHmac('sha256', secret).update(rawBody).digest('hex')}`;
  const expectedBuffer = Buffer.from(expected, 'utf8');
  const receivedBuffer = Buffer.from(signatureHeader ?? '', 'utf8');

  if (expectedBuffer.length !== receivedBuffer.length) {
    return false;
  }
  if (!crypto.timingSafeEqual(expectedBuffer, receivedBuffer)) {
    return false;
  }

  const timestamp = Number(timestampHeader);
  if (!Number.isFinite(timestamp) || Math.abs(Date.now() - timestamp) > TIMESTAMP_TOLERANCE_MS) {
    return false;
  }

  return true;
}

// Example use with a raw body string captured before JSON.parse:
// const ok = verifyEhrWebhookSignature(
//   rawBody,
//   req.headers['x-newinstance-signature'],
//   req.headers['x-newinstance-timestamp'],
//   subscriptionSecret,
// );

6. Retries and dead-letteringlink

AttemptTiming
1Sent as soon as the event is dispatched
21 minute after attempt 1 fails
35 minutes after attempt 2 fails
415 minutes after attempt 3 fails
560 minutes after attempt 4 fails
6240 minutes (4 hours) after attempt 5 fails
7720 minutes (12 hours) after attempt 6 fails
8720 minutes (12 hours) after attempt 7 fails

A delivery's status is served only by the GraphQL API, as EhrWebhookDelivery.status, in upper case; no REST route lists deliveries. After attempt 8 fails, the delivery is marked DEAD. Only a delivery reaching DEAD increments the subscription's failure count, not every failed attempt; a subscription that accumulates 100 dead deliveries is automatically paused, and any successful delivery resets the count to zero. While paused, due deliveries are left PENDING and rechecked every 15 minutes rather than attempted, so resuming the subscription delivers the backlog. A subscription that is deleted, or otherwise not active and not paused, fails its pending deliveries immediately instead of retrying them, and so does one whose configuring member has left or no longer reaches every client (section 3). Redeliver a FAILED or DEAD delivery through redeliverEhrWebhook (dashboard) to retry it immediately from attempt 1. A DEAD delivery also raises a work item on the Failed webhook deliveries queue and notifies whoever created the subscription. That queue opens to the permission that manages integrations, which is also what replaying a delivery takes; creating, changing, pausing or deleting the subscription takes that permission and the reach of every client.

Delivery records are deleted 30 days after creation regardless of status. Dispatched events are deleted 7 days after dispatch unless a delivery still PENDING, FAILED or DEAD references them, which is what keeps Redeliver able to find the event to resend.

This is separate from event dispatch itself: each event record also tracks its own status and attempts, capped at 10, for the outbox drain step that fans events out to deliveries. Its status is pending, dispatched, failed or withheld: a failed event never made it to dispatch at all (for example, a Redis outage), and is distinct from a webhook delivery being DEAD; a withheld event was held back from every webhook because a client it names is protected under 42 CFR Part 2 or has no record, or because whether one is protected could not be checked. GET /api/v1/ehr/events (section 7) serves the status as stored, pending, dispatched or failed, and never lists a withheld event. The GraphQL API's ehrEvents serves it upper cased as EhrEventStatus, PENDING, DISPATCHED, FAILED or WITHHELD, with attempts.

7. Replaying eventslink

GET /api/v1/ehr/eventslink

Returns the organization's event log in sequence order, for polling or backfilling missed deliveries. Requires the webhooks permission group, and a key issued by a member who reaches every client (section 1). Response items carry identifiers and status only, never payload or clientId; to recover an event's actual data, use Redeliver (dashboard) against one of its deliveries, not event replay. Events are removed 7 days after being dispatched unless an unresolved delivery still references them (section 6), so a gap longer than that window returns fewer events with no error or gap indicator: nextSeq simply does not advance past the missing range.

An event is left out of items when it names a client who is protected by 42 CFR Part 2 or who has no record in the organization, whether it names the client directly or through the appointment it is about, and when the appointment it is about is marked Part 2 or cannot be found. That includes an event written before its client became protected. nextSeq still moves past an event left out, so the next call does not walk it again, and a gap in seq is expected.

Query parameterRequiredNotes
sinceSeqNoReturn events with a sequence number greater than this value
limitNo1 to 500, default 100

Example request:

GET /api/v1/ehr/events?sinceSeq=1040&limit=100
x-api-key: key_abc123:secret_xyz789

Example response, 200 OK:

{
  "items": [
    {
      "id": "66f1a2b3c4d5e6f7a8b9c0f1",
      "seq": 1041,
      "type": "ehr.staff.updated",
      "occurredAt": "2026-09-18T09:12:00.000Z",
      "aggregate": { "type": "staff", "id": "66f1a2b3c4d5e6f7a8b9c0f2" },
      "status": "dispatched"
    }
  ],
  "nextSeq": 1041
}

Use nextSeq from the response as sinceSeq on the following call.

8. Scheduling and client lookuplink

This section has three authenticated routes and one provider webhook. There are no REST routes for enrollments, coverages, consents, documents, encounters or client import: those exist on the GraphQL API and in the dashboard only.

GET /api/v1/ehr/appointmentslink

Requires the scheduling permission group. Returns appointments that overlap the requested range, oldest first. Instants are ISO 8601 in UTC and each appointment carries the IANA zone it was booked in.

Query parameterRequiredNotes
fromYesRange start, ISO 8601
toYesRange end, ISO 8601
staffIdNoOnly appointments this staff member attends, 24 characters
statusNoOne of scheduled, confirmed, checked_in, in_progress, completed, no_show, cancelled, late_cancel
pageNoFrom 1, default 1
limitNo1 to 100, default 25

No client identifier is accepted as a filter. A query string reaches an access log, and an identifier there would say which client had care looked up, so filter the returned page yourself. Client identifiers are returned only to a key that also carries the clients-read permission group. Every client that appears in the response is written to the access log.

Example request:

GET /api/v1/ehr/appointments?from=2026-03-02T00:00:00Z&to=2026-03-03T00:00:00Z&limit=2
x-api-key: key_abc123:secret_xyz789

Example response, 200 OK:

{
  "items": [
    {
      "id": "65f1a2b3c4d5e6f7a8b9c0d1",
      "typeId": "65f1a2b3c4d5e6f7a8b9c0a1",
      "primaryStaffId": "65f1a2b3c4d5e6f7a8b9c0b1",
      "clientIds": ["65f1a2b3c4d5e6f7a8b9c0c1"],
      "locationId": "65f1a2b3c4d5e6f7a8b9c0e1",
      "roomLocationId": "65f1a2b3c4d5e6f7a8b9c0e2",
      "programId": "65f1a2b3c4d5e6f7a8b9c0f1",
      "start": "2026-03-02T15:00:00.000Z",
      "end": "2026-03-02T15:45:00.000Z",
      "timezone": "America/New_York",
      "status": "scheduled",
      "part2": false
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 2
}

POST /api/v1/ehr/appointmentslink

Requires the scheduling permission group. Books one appointment.

Body fieldRequiredNotes
typeIdYesAppointment type id, 24 characters
clientIdsYes1 to 60 attending clients
primaryStaffIdYesThe staff member who owns the appointment
staffIdsNoUp to 20 additional attending staff
roomLocationIdNoRoom to reserve
locationIdNoSite or virtual location
programIdNoProgram the appointment belongs to
startYesStart instant, ISO 8601 UTC
endYesEnd instant, ISO 8601 UTC
timezoneYesIANA zone the wall time was entered in
allowConflictsNoDefault false. Book anyway where the organization policy only warns about clashes

A clash returns 409 unless the organization's double-booking policy allows it or allowConflicts is true. The default policy is to warn.

Example request:

POST /api/v1/ehr/appointments
x-api-key: key_abc123:secret_xyz789
Content-Type: application/json

{
  "typeId": "65f1a2b3c4d5e6f7a8b9c0a1",
  "clientIds": ["65f1a2b3c4d5e6f7a8b9c0c1"],
  "primaryStaffId": "65f1a2b3c4d5e6f7a8b9c0b1",
  "start": "2026-03-04T15:00:00.000Z",
  "end": "2026-03-04T15:45:00.000Z",
  "timezone": "America/New_York"
}

Example response, 201 Created:

{
  "id": "65f1a2b3c4d5e6f7a8b9c0d2",
  "status": "scheduled",
  "start": "2026-03-04T15:00:00.000Z",
  "end": "2026-03-04T15:45:00.000Z",
  "timezone": "America/New_York"
}

POST /api/v1/ehr/clients/lookuplink

Requires the clients-read permission group. Returns the demographic summary for one client, looked up by medical record number. The number is sent in the request body, never in the path or the query string, because both reach an access log and a medical record number there identifies the client to anyone who reads the log. The read is written to the access log with the API key as the actor.

POST /api/v1/ehr/clients/lookup
x-api-key: key_abc123:secret_xyz789
Content-Type: application/json

{
  "mrn": "MRN-000123"
}

POST /api/v1/ehr/sms/inboundlink

No API key. This route receives message replies from the SMS provider and is authenticated by the provider signature over the full URL and the sorted parameters, computed with the organization's auth token. The signature travels in x-twilio-signature.

The payload is application/x-www-form-urlencoded with To, From, Body and optionally MessageSid.

A reply of C, CONFIRM, YES or Y confirms the next appointment within seven days, except from a number more than one client shares, where it confirms nothing and the reply is recorded as ignored. STOP, STOPALL, UNSUBSCRIBE, CANCEL, END or QUIT removes text message consent for that number immediately, so a reply of CANCEL opts the number out of text messages and cancels no appointment.

9. Part 2 and protected contentlink

The REST API is not capable of returning 42 CFR Part 2 content, and there is no header that makes it capable.

  • Appointments belonging to a Part 2 client are excluded from GET /api/v1/ehr/appointments.
  • POST /api/v1/ehr/appointments refuses a Part 2 client, and it refuses with 404 Client not found rather than with a message that would confirm the client exists.
  • POST /api/v1/ehr/clients/lookup refuses a Part 2 client the same way.
  • Claims and charges belonging to a Part 2 client are excluded from GET /api/v1/ehr/billing/claims and from its total, and the single reads answer 404 for them.
  • No webhook event is sent at all for an entry the ledger records on a Part 2 client's charge, or for a claim written for one. Nothing about them leaves the building on a webhook, and an event that was never sent cannot be inferred from one that was.
  • Events that name a Part 2 client or a client with no record, directly or through the appointment they are about, and events about an appointment that is Part 2 or cannot be found, are left out of GET /api/v1/ehr/events, and nextSeq still moves past them.

The refusal is deliberately indistinguishable from an absent client. A refusal that said "this client is protected" would itself disclose that the client is in a substance use program, which is the fact the rule exists to protect.

Part 2 content is reachable from the dashboard and the GraphQL API, by two routes, each open only to a person who holds ehr.part2.access. The first is the program: a member of the program the record belongs to, or, for a record that names no program, of a Part 2 program the client is enrolled in, opens it with no consent asked. The second is a consent: anyone else opens it only while a signed 42 CFR Part 2 consent the client gave names them, as a person by their staff profile, covering the record's program or carrying a general designation. Such a consent opens nothing unless it names a disclosure purpose the product knows, nothing before its first day, after its last day or on or after its expiry, and nothing once it is revoked. Breaking the glass for the client also opens the record, and the session is kept for review. When one of those records is disclosed it carries the redisclosure notice.

10. Event cataloglink

These are the exact event types EVENT_CATALOG in gql-server/src/services/ehr/shared/outbox.ts catalogues.

phiFields on an event are omitted unless the subscription sets includePhi: true (see section 4). The column below says which events carry a client identifier at all.

Event typeAggregate typeCarries clientId
ehr.config.updatedtenant_settingsNo
ehr.program.createdprogramNo
ehr.program.updatedprogramNo
ehr.location.createdlocationNo
ehr.location.updatedlocationNo
ehr.staff.updatedstaffNo
ehr.form_definition.publishedform_definitionNo
ehr.form_definition.retiredform_definitionNo
ehr.workflow_definition.publishedworkflow_definitionNo
ehr.breakglass.startedbreak_glassOnly when includePhi: true (see section 4)
ehr.breakglass.endedbreak_glassOnly when includePhi: true (see section 4)
ehr.breakglass.reviewedbreak_glassOnly when includePhi: true (see section 4)
ehr.webhook.testwebhook_subscriptionNo
ehr.claim.submittedclaimNo. Never sent for a Part 2 client
ehr.ledger.postedchargeNo. Never sent for a Part 2 client
ehr.client.createdclientYes, with mrn
ehr.client.updatedclientYes, with mrn
ehr.client.status_changedclientYes, with mrn
ehr.client.mergedclientYes, with mrn and survivorClientId
ehr.enrollment.createdenrollmentYes
ehr.enrollment.transitionedenrollmentYes
ehr.consent.createdconsentYes, with type
ehr.consent.revokedconsentYes, with type
ehr.consent.expiringconsentYes
ehr.appointment.scheduledappointmentYes, when the appointment has exactly one client
ehr.appointment.updatedappointmentYes, when the appointment has exactly one client
ehr.appointment.cancelledappointmentYes, when the appointment has exactly one client
ehr.appointment.checked_inappointmentYes, when the appointment has exactly one client
ehr.appointment.completedappointmentYes, when the appointment has exactly one client
ehr.appointment.no_showappointmentYes, when the appointment has exactly one client
ehr.encounter.completedencounterYes
ehr.encounter.voidedencounterYes
ehr.document.signeddocumentYes
ehr.document.cosigneddocumentYes
ehr.document.amendeddocumentYes
ehr.document.voideddocumentYes
ehr.treatment_plan.activatedtreatment_planYes, with reviewDueAt
ehr.treatment_plan.review_duetreatment_planYes, with reviewDueAt
ehr.treatment_plan.closedtreatment_planYes, with reviewDueAt
ehr.group_session.createdgroup_sessionNo
ehr.group_session.attendance_recordedgroup_sessionNo
ehr.group_session.notes_generatedgroup_sessionNo
ehr.import.completedimportNo

ehr.treatment_plan.review_due is sent once for each plan and review date. The background worker's nightly work queue sweep, ehr.workqueue.sweep, sends it for each active plan whose review date has passed since the sweep last ran; its first run tells of nothing that fell due before it. Signing a plan whose review date has already passed sends it at once, with ehr.treatment_plan.activated; signing one whose review is still to come sends nothing until that date passes. It is never sent for a plan about a 42 CFR Part 2 client, a Part 2 program or a Part 2 plan. Its data is the plan, its version and its review date, planId, version and reviewDueAt, the review date left out unless the subscription has includePhi: true.

ehr.consent.expiring is sent once for each consent and expiry date. The background worker's nightly work queue sweep, ehr.workqueue.sweep, sends it for each consent recorded as active whose expiry falls within the next thirty days, its occurredAt thirty days before the expiry, or the moment the consent was recorded if that is later; a consent already that close when it is recorded is told of at the next sweep, and one whose expiry date changes is told of again for the new date. It is never sent for a consent about a 42 CFR Part 2 client, or one whose client cannot be found. Its data is the consent, its client and its expiry, consentId, clientId and expiresAt, the client left out unless the subscription has includePhi: true.

A subscription can list specific event types or * for all of them. POST and PATCH /api/v1/ehr/webhooks accept every event type in this catalog by name in events, ehr.breakglass.ended among them. A type the catalog does not list is refused with 400 "Invalid request payload input", and nothing is written.

11. FHIR R4 facadelink

GET /fhir/r4/metadatalink

Returns the server's FHIR CapabilityStatement. No authentication required. Content type application/fhir+json.

{
  "resourceType": "CapabilityStatement",
  "status": "active",
  "date": "2026-09-18T00:00:00.000Z",
  "kind": "instance",
  "fhirVersion": "4.0.1",
  "format": ["json"],
  "software": { "name": "NewInstance EHR" },
  "rest": [{ "mode": "server" }]
}

Only this capability endpoint is served. The rest[0] entry has no resource key at all: no FHIR resources (Patient, Practitioner, Encounter, and so on) are registered. The resource facade is not built; the program plan (README.md) places it in Phase 4, and SMART on FHIR authentication in Phase 5.

12. Billing and revenue cyclelink

Billing has three authenticated read routes and two webhook events, and no write route: charges, claims, payments, refunds, adjustments and statements are written from the dashboard and the GraphQL API only, where a named person holds the permission and the audit trail records who they were.

Each of the two events names an object and nothing else. There is no amount, no service code, no reason code, no client identifier and no Part 2 flag in either of them, because a webhook is a disclosure to a destination the organization configured and no consent names that destination. What a recipient may do with the event is come back through these routes, where the key is authenticated, the permission is checked, the read is written to the access log and a Part 2 client is absent.

Event typeSent whenCarries
ehr.claim.submittedA claim is written for a payerThe claim id, as aggregate
ehr.ledger.postedMoney posts against a chargeThe charge id as aggregate, and sourceType, one of charge, remittance_line, client_payment, refund, adjustment

GET /api/v1/ehr/billing/claimslink

Requires billing-read, and a creator who holds the permission to view billing amounts. Returns claims newest first.

Query parameterRequiredNotes
statusNoOne of queued, corrected, void
fromNoOnly claims written at or after this instant, ISO 8601
toNoOnly claims written before this instant, ISO 8601
pageNoFrom 1, default 1
limitNo1 to 100, default 25

Each row carries the claim id, the payer id, the control number, the status, the number of charges on it, what it was worth, and the state of its transmission (prepared, synthetic_not_sent, sent, acknowledged or rejected) with the instants it was sent and acknowledged. synthetic_not_sent is a claim handed to the synthetic destination inside the product, which development and testing use and production refuses: it was never sent, no payer has seen it, it carries no instant it was sent, and nothing was disclosed. clientId appears only for a key that also carries clients-read. Payer reason codes are not on this list, only on the single read below.

GET /api/v1/ehr/billing/claims/{id}link

Returns one claim with its service lines and, where the payer denied it, the reason and remark codes, the amount denied, the amount allowed and deniedAt, the date the payer gave for the denial on its remittance, served as that calendar date in the form YYYY-MM-DD, such as 2026-09-11, with no time of day, as the GraphQL API serves a denial's deniedAt. The claim file itself, which carries the member number, the name, the date of birth and the diagnoses, is never returned by REST. A claim for a Part 2 client answers 404 Claim not found.

GET /api/v1/ehr/billing/charges/{id}link

Returns one charge and every ledger entry posted against it, with what each party still owes on it: payerOwedMinor for the insurer, clientOwedMinor for the client, and transferredToClientMinor for what has moved from one to the other. There is no single party blind balance, because once responsibility has moved between the two a single figure names neither of them. entryCount is there because owing nothing is also what a charge nothing ever posted to looks like, and the two are different facts. The charge's own amountMinor is returned beside them, so a charge whose ledger does not agree with it can be seen rather than reconciled silently. Diagnoses are not returned. The three figures are worked out from the entries returned beside them when the charge is read, so they always agree with those entries and are never a figure stored earlier. A charge for a Part 2 client answers 404 Charge not found.