Tenant-resolved access
Credentials and installation keys select the tenant. A caller-supplied tenant ID never grants access.
Test tenants only · live access disabled
This version requires the matching application and schema deployment. It is available only for approved synthetic and sandbox tenants; real-patient access remains disabled. Create an expiring credential with explicit resource scopes and patient restrictions in your client workspace. Keep it on your backend. Tenant authority comes from the credential; never send a tenant header or tenant identifier as authorization.
/v1/client/2026-10/queryUse a JSON body with resource, optional exact patientId/id, limit (1–100) and cursor. Resources: patients, episodes, intakes, consents, appointments, prescriptions, labs, results, records, documents, payments, fulfillment, packages, communications and renewals. Each requires its own resource:read scope. These are minimum-necessary summaries: no intake answers, raw clinical content, private document links or card data. Clinical rows also require the current environment and tenant capability gates.
const response = await fetch(baseUrl + '/v1/client/2026-10/query', {
method: 'POST',
headers: { Authorization: 'Bearer ' + serverCredential,
'Content-Type': 'application/json' },
body: JSON.stringify({ resource: 'patients', limit: 50 })
})
if (!response.ok) throw new Error('integration_request_failed')
const page = await response.json()
// Store permitted data securely. Never log the retrieval payload.Responses include data, nextCursor and changeCursor. Save the first change cursor before finishing snapshot pagination across resources. Then POST to /v1/client/2026-10/events with that cursor. Apply each page and its checkpoint in one durable consumer transaction. Keep polling to reconcile missed callbacks. Signed cursors bind to the credential and filters; after seven days, 410 requires a fresh snapshot.
POST /v1/client/2026-10/crosswalks with resource, id, externalId requires mappings:write plus read scope. Exact retries are safe; conflicting immutable links return 409. Use only opaque external IDs in bodies.
Subscribe an existing approved sandbox lifecycle endpoint to selected resources. Notifications contain only id, schemaVersion, type, resource, resourceId, occurredAt. Types are resource.changed and resource.removed. Fetch current authorized state; never infer a clinical or payment action from delivery.
Verify HMAC-SHA256 of the exact timestamp, a period and raw body using the endpoint key. Headers are x-ezpharma-timestamp, x-ezpharma-signature: sha256=… and x-ezpharma-event-id. Enforce a 300-second clock window, constant-time comparison, matching signed payload/header IDs and durable event deduplication. Timestamp checking alone does not prevent duplicate effects.
Delivery is at least once and unordered, with eight automatic attempts, exponential minute backoff capped at eight hours, fenced 60-second leases, pause and controlled retry. Immutable coded attempt evidence is preserved. Synthetic callbacks never reach a network. API credentials, scopes, patient restrictions and capability/connection availability are rechecked before dispatch.
Errors: 401 credential denied; 403 scope/capability/live boundary; 404 not found/unauthorized; 409 conflict; 410 resync; 422 malformed request/cursor; 429 shared 120/minute/credential with Retry-After: 60; 503 dependency unavailable. No external chart, prescribing, payment or care-state write is authorized by these read credentials.
Download OpenAPI contract · Notification schema · Signature and transactional sync example
Client acceptance pending
Open Customize intake in your workspace to change bounded typography, colors, spacing, layout, progress, completion text and operational fields, then preview at desktop or 390px. Governed identity/clinical/consent content, products/prices and payment policy require staff review. Drafts preserve the published version; stale editors are rejected and unchanged retries reuse a command ID.
<script src="https://www.ezpharma.app/intake-embed.js"></script>
<div id="care-cart"></div>
<script>
EzPharma.mountCart('#care-cart', { publicKey: 'YOUR_TEST_INSTALLATION_KEY' })
</script>EzPharma.openCart({ publicKey }) opens the same modal flow. A hosted cart uses /intake-frame?publicKey=YOUR_TEST_INSTALLATION_KEY&mode=hosted&cart=true. Only one plan at quantity one is supported. Multi-item/quantity, coupons, taxes and cross-currency pricing are unsupported. Selection/removal uses the existing durable intake; required intake and consent precede the authoritative quote and connected-account hosted Checkout. Browser price/quantity fields never control the charge.
Opaque resume credentials remain in the frame’s sessionStorage; answers remain server-side and in memory. In-progress sessions load their original journey via GET /v1/intake/sessions/:sessionId/journey with a Bearer session token. Publishing a price or design revision does not change those sessions. Frames retain origin approval and the parent challenge. Resume is tab/device scoped; storage restrictions can prevent interruption recovery.
Completion retains the opaque session reference onComplete({ sessionId, status: 'completed' }). No answer, direct patient identity or secret enters host callbacks. Checkout retains existing server/provider idempotency and signed-webhook status; payment never implies clinical approval, prescribing, dispensing or shipment. Keep uncertified live checkout disabled. Copy intake/cart snippets from the approved installation card and test every supported placement before enabling it.
Guided product tour — browser-only orientation with role paths and explicit availability limits. Simulated outcomes are not API verification or partner approval.
Build secure patient intake, receive verified clinical and pharmacy updates, and keep every approved system aligned from visit through delivery.
Credentials and installation keys select the tenant. A caller-supplied tenant ID never grants access.
Session creation and inbound events are designed for idempotent, retryable delivery.
Source events are retained and mapped through explicit identifiers—never patient-name guessing.
Clinical, payment, prescription, dispense, and shipment states do not imply one another.
The API is deployed, but access is provisioned per client and connection. Sandbox validation, credential exchange, domain approval, data agreements, and partner certification may be required before live data or money is enabled.
Every integration starts in an approved sandbox connection. Your onboarding package contains only the credentials and identifiers required for your use case.
Tell us whether you are integrating intake, clinical events, pharmacy events, or client timelines.
Sign one synthetic request, retry it, and confirm the resulting timeline or lifecycle callback.
Approve mappings, failure paths, operators, and data controls before the connection is enabled.
Compute the signature over the exact request bytes. Do not parse and reserialize the body after signing.
timestamp=$(date +%s)
body='{"schema_version":"1.0","event_id":"11111111-1111-4111-8111-111111111111","event_type":"visit.completed","tenant_id":"tenant_from_onboarding","care_episode_id":"episode_demo_123","source":{"organization_id":"organization_from_onboarding","connection_id":"00000000-0000-4000-8000-000000000000","system":"example-ehr","source_event_id":"ehr-event-88421"},"subject":{"patient_id":"synthetic-patient-42"},"occurred_at":"2026-10-22T14:30:00Z","received_at":"2026-10-22T14:30:00Z","data":{"status":"completed"}}'
signature=$(printf '%s' "$timestamp.$body" | openssl dgst -sha256 -hmac "$EZPHARMA_WEBHOOK_SECRET" -hex | sed 's/^.* //')
curl https://www.ezpharma.app/v1/integrations/$EZPHARMA_CONNECTION_ID/events \
-H "Content-Type: application/json" \
-H "X-EzPharma-Timestamp: $timestamp" \
-H "X-EzPharma-Signature: sha256=$signature" \
--data-raw "$body"
import { createHmac } from 'node:crypto'
const timestamp = Math.floor(Date.now() / 1000).toString()
const body = JSON.stringify(event)
const digest = createHmac('sha256', process.env.EZPHARMA_WEBHOOK_SECRET)
.update(`${timestamp}.${body}`)
.digest('hex')
const response = await fetch(
`https://www.ezpharma.app/v1/integrations/${process.env.EZPHARMA_CONNECTION_ID}/events`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
'x-ezpharma-timestamp': timestamp,
'x-ezpharma-signature': `sha256=${digest}`
},
body
}
)
console.log(response.status, await response.json())
import hashlib, hmac, json, os, time, requests
timestamp = str(int(time.time()))
body = json.dumps(event, separators=(",", ":"))
message = f"{timestamp}.{body}".encode()
digest = hmac.new(
os.environ["EZPHARMA_WEBHOOK_SECRET"].encode(),
message,
hashlib.sha256,
).hexdigest()
response = requests.post(
f"https://www.ezpharma.app/v1/integrations/{os.environ['EZPHARMA_CONNECTION_ID']}/events",
headers={
"Content-Type": "application/json",
"X-EzPharma-Timestamp": timestamp,
"X-EzPharma-Signature": f"sha256={digest}",
},
data=body,
)
print(response.status_code, response.json())
A new accepted event returns 202. Replaying the same source event returns 200 with duplicate: true and does not create a second state transition.
Sandbox and live resources are separate even when they share the same API origin. A credential, installation, connection, mapping, or merchant account belongs to exactly one environment.
| Environment | Base URL | Use |
|---|---|---|
| Sandbox | https://www.ezpharma.app | Synthetic patients, test payments, mapping validation, retry and failure testing. |
| Live | https://www.ezpharma.app | Enabled only after the specific client, partner, domains, credentials, and operating controls are certified. |
Environment is carried by the provisioned resource—not a query parameter. Never infer live access from an HTTP success in sandbox.
EzPharma uses the narrowest credential for each surface. The credential resolves tenant access server-side.
| Surface | Credential | Transport |
|---|---|---|
| Intake | Publishable installation key, then opaque session token | Request body or Authorization: Bearer |
| Inbound partner events | Connection-specific HMAC secret | X-EzPharma-Timestamp and X-EzPharma-Signature |
| Client timeline | Tenant-scoped API key | Authorization: Bearer <key> |
| Dashboard APIs | Individual user session plus membership | Secure HTTP-only session cookie |
| Patient portal | Short-lived patient session resolved from emailed-code verification | Secure HTTP-only session cookie |
signed_payload = unix_timestamp + "." + exact_raw_request_body
digest = HMAC_SHA256(connection_secret, signed_payload)
header = "sha256=" + lowercase_hex(digest)Requests outside the configured replay window are rejected. Rotate credentials through your EzPharma onboarding contact; secrets are revealed only through an approved secure exchange.
Do not send a tenant header or tenant query parameter. If a canonical event contains tenant_id, it must match the tenant already resolved by the connection.
Network failures are normal. EzPharma separates “safe to retry” from “accepted exactly once” by operation.
| Operation | Idempotency key | Retry behavior |
|---|---|---|
| Create intake session | Idempotency-Key header, up to 160 characters | Reuse the same key for the same logical creation attempt. |
| Canonical event | source.connection_id + source.source_event_id | Duplicate delivery returns success without a new event. |
| Pharmacy event | Connection + event_id | Duplicate delivery returns success without a new event. |
| Lifecycle callback | X-EzPharma-Event-Id | Consumers must persist and deduplicate this ID. |
| Patient care message | commandId in the JSON body | Exact retries return the original message; conflicting reuse is rejected. |
Retry 429 and 5xx responses with bounded exponential backoff and jitter. Do not automatically retry validation, authentication, or mapping errors without changing the request or configuration.
A successful payment is not a clinical approval. A signed prescription is not a filled dispense. A shipping label is not a delivery.
Integrations should render and reconcile each domain separately. Unknown external statuses are rejected for explicit mapping rather than silently coerced.
Errors are JSON objects with a stable machine-readable error code. Some validation responses also include safe details.
{
"error": "external_mapping_required",
"domain": "prescription",
"externalEventId": "partner-event-123"
}| Status | Meaning | Action |
|---|---|---|
| 400 | Malformed JSON or request. | Correct the payload; do not retry unchanged. |
| 401 | Missing, invalid, inactive, or expired credential/signature. | Check credential and clock; rotate only through the approved path. |
| 403 | Credential is valid but not allowed for the resource or adapter. | Confirm the provisioned connection and domain. |
| 404 | Resource does not exist within the credential's tenant. | Check the tenant-scoped identifier. |
| 409 | Required external identifier mapping is missing or ambiguous. | Add or correct the mapping; never guess from demographics. |
| 422 | Payload shape or status is unsupported. | Fix the contract or approve a new explicit mapping. |
| 429 | Rate limit reached. | Honor Retry-After and back off. |
| 5xx | Temporary server failure. | Retry idempotently with exponential backoff. |
/v1/intake/installations/{publicKey}Browser-safeReturns the approved public bootstrap for an active installation: brand, published journey, modes, and safe configuration.
| Parameter | Type | Description |
|---|---|---|
| publicKey required | string · path | Publishable key issued for one sandbox or live website installation. |
curl https://www.ezpharma.app/v1/intake/installations/pk_sandbox_exampleA public key identifies an installation but is not a general API credential. Session creation still enforces installation state, allowed origin, request limits, environment, and live approval.
/v1/intake/sessionsOrigin checkedCreates a version-pinned intake session for an approved installation.
| Field | Type | Description |
|---|---|---|
| publicKey required | string | Installation publishable key. |
| installationMode | string | inline, modal, hosted, or headless. Defaults to inline. |
| source | string | Safe source label for attribution. Do not include patient data. |
| externalReference | string | Optional client correlation value approved for this flow. |
curl https://www.ezpharma.app/v1/intake/sessions \
-X POST \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: intake-attempt-018fa' \
-H 'X-EzPharma-Parent-Origin: https://client.example' \
-d '{
"publicKey": "pk_sandbox_example",
"installationMode": "inline",
"source": "client-site"
}'The 201 response contains session, including the opaque token needed for subsequent session operations. Keep the token out of URLs, analytics, and logs.
/v1/intake/sessions/{sessionId}Send the opaque token as Authorization: Bearer <session_token> to retrieve the version-pinned session. An invalid token and an unknown session both return 404 session_not_found.
/v1/intake/sessions/{sessionId}/answersValidates and saves the current answer set against the session's immutable journey version.
| Field | Type | Description |
|---|---|---|
| token required | string | Opaque token returned at session creation. |
| answers required | object | Keys and values defined by the pinned journey. Send only requested information. |
| currentStep | integer | Zero-based current step. Defaults to 0. |
curl https://www.ezpharma.app/v1/intake/sessions/session_123/answers \
-X PATCH \
-H 'Content-Type: application/json' \
-d '{
"token": "session_token_from_create",
"currentStep": 1,
"answers": {
"synthetic_example_field": "example value"
}
}'Clinical answers belong only in the encrypted request body and approved durable store. Never copy them into query strings, URLs, browser analytics, payment metadata, lifecycle metadata, or application logs.
/v1/intake/sessions/{sessionId}/quoteCreates the authoritative, immutable quote for the selected product configuration.
/v1/intake/sessions/{sessionId}/checkoutPer-client enablementCreates a provider-hosted checkout using a tenant-owned merchant connection. EzPharma does not receive raw card data.
curl https://www.ezpharma.app/v1/intake/sessions/session_123/quote \
-X POST -H 'Content-Type: application/json' \
-d '{"token":"session_token_from_create"}'
curl https://www.ezpharma.app/v1/intake/sessions/session_123/checkout \
-X POST -H 'Content-Type: application/json' \
-d '{
"token":"session_token_from_create",
"merchantConnectionId":"merchant_connection_from_bootstrap"
}'Checkout creation does not mark payment successful. The payment state changes only after the provider's signed webhook is verified. Payment state does not change clinical state.
/v1/intake/sessions/{sessionId}/completeFinalizes a valid session and returns safe completion content plus patient-status access when configured.
curl https://www.ezpharma.app/v1/intake/sessions/session_123/complete \
-X POST \
-H 'Content-Type: application/json' \
-d '{"token":"session_token_from_create"}'Store only the minimum result needed by the approved user experience. Treat patient-status access as a secret and do not put it into analytics or lifecycle metadata.
/v1/patient/portal/sessionPatient session requiredReturns { authenticated, portal } using the HTTP-only patient session. Invalid, expired and revoked sessions return authenticated: false.
The server resolves account and tenant; caller-supplied identities grant no access. Only completed, explicitly linked intakes are included. Responses use no-store and no-referrer. Do not copy this response into browser storage, logs, analytics or partner lifecycle metadata.
| Projection | Meaning |
|---|---|
| portal.care.available / synthetic | Clinical capability availability and explicit synthetic-practice label. Unavailable care returns empty dashboard arrays. |
| appointments | Order-bound ID, status, modality and ISO start/end. Display with an explicit timezone. Internal joining links and notes are excluded; request instructions or changes through secure messaging. |
| fulfillment | Independent order and item state, each package's status, reshipOf reference, carrier/tracking text, last recorded update, estimated date and support flag. Missing movement never proves delivery. |
| renewals | Enrollment status/due date, latest cycle status/evaluation time and allowlisted gate key/status pairs. Missing evidence is unavailable. Clinical approval remains independent of payment. |
Released prescription content includes separate signingStatus and transmissionStatus. Unsigned cancelled drafts and unordered laboratory drafts stay hidden. Pharmacy acknowledgement and dispensing remain separate episode evidence. There are no patient-authorized schedule or renewal lifecycle commands: use the existing secure message route to request care-team action. Reads are safe to retry; message commands require an unchanged commandId and payload after uncertain responses.
Real care and actual partners require separate client and partner acceptance. Synthetic records and simulator movement do not establish certification or physical delivery.
/v1/patient/portal/orders/{orderId}/messagesClinical enablement requiredAppends a patient-authored message to the care episode linked to a completed portal order.
| Field | Type | Description |
|---|---|---|
| commandId required | string | Patient-generated idempotency key for this send attempt. |
| text required | string | Secure message text, at most 8,000 characters. |
The endpoint accepts only the portal's HTTP-only patient session. The server resolves the tenant, patient account, completed intake, chart, and care episode; request bodies cannot select those identities. Exact retries return the original message, while conflicting command reuse returns 409 care_message_conflict.
Do not place message text in URLs, analytics, lifecycle metadata, or logs. Clinician-authored messages and lab results appear in the portal only after explicit patient release.
/v1/patient/portal/orders/{orderId}/pharmacy-selectionClinical enablement requiredRecords a patient-authored pharmacy preference for the care episode linked to a completed portal order.
| Field | Type | Description |
|---|---|---|
| commandId required | string | Patient-generated idempotency key. |
| pharmacyKey required | string | Stable preference or pharmacy-directory identifier; it does not establish an eligibility match. |
| displayName required | string | Patient-facing pharmacy name. |
| jurisdiction required | string | Two-letter jurisdiction matching the server-resolved review case. |
The server resolves tenant, patient account, and care episode from the HTTP-only portal session. A patient preference is always stored as review_required; it cannot establish pharmacy eligibility or authorize transmission. The native portal may generate an opaque preference reference for a named pharmacy/location; the care team must separately verify an approved match. GET /v1/patient/portal/session includes up to 100 of this account's patient-authored choices in portal.clinical.pharmacyChoices (id, orderId, displayName, jurisdiction, eligibilityStatus, createdAt), only for linked orders when clinical gates permit. Patient API responses use no-store and no-referrer.
/v1/integrations/{connectionId}/eventsCertified connectionsIngests a signed EzPharma canonical event, preserves the original request, and projects an eligible state transition.
| Field | Type | Description |
|---|---|---|
| schema_version required | string | Currently 1.0. |
| event_id required | UUID | Canonical event identifier. |
| event_type required | string | Namespaced event such as visit.completed or shipment.in_transit. |
| tenant_id required | string | Must match the tenant resolved from the connection; it cannot grant access. |
| care_episode_id required | string | Tenant-owned care episode. |
| source required | object | Provisioned organization and connection IDs, system, and stable source event ID. |
| subject required | object | Internal patient ID and applicable resource IDs. |
| occurred_at required | date-time | When the source says the action occurred. |
| received_at required | date-time | Accepted for schema compatibility; EzPharma records authoritative receipt time. |
| data required | object | Minimum normalized fields needed for the event. |
{
"duplicate": false,
"projected": true
}Out-of-order and terminal-state conflicts are retained as event evidence even when they cannot update the current projection. Corrections are new events; history is never rewritten.
/v1/integrations/{connectionId}/fhir/eventsClinical connectionsAccepts a signed FHIR R4 Appointment or Encounter, resolves explicit external links, and translates a supported status into a canonical event.
{
"resourceType": "Encounter",
"id": "encounter-demo-1042",
"status": "finished",
"meta": {
"versionId": "1",
"lastUpdated": "2026-10-22T14:30:00Z"
},
"subject": { "reference": "Patient/synthetic-patient-42" },
"appointment": [{ "reference": "Appointment/demo-appointment-9" }],
"period": { "end": "2026-10-22T14:30:00Z" }
}Use Content-Type: application/fhir+json and the standard EzPharma HMAC headers. Clinical or approved technology connections may use this adapter. Missing or ambiguous links return 409 external_mapping_required.
/v1/integrations/{connectionId}/pharmacy/eventsPharmacy & fulfillmentAccepts a signed prescription, dispense, or shipment envelope and maps approved partner statuses to independent canonical resources.
{
"event_id": "partner-event-123",
"event_type": "shipment.status",
"occurred_at": "2026-10-22T18:10:00Z",
"patient_id": "synthetic-patient-42",
"prescription_id": "partner-rx-15",
"order_id": "partner-order-81",
"shipment_id": "partner-shipment-9",
"status": "in_transit",
"tracking": {
"number": "SYNTHETIC-TRACKING-123",
"carrier": "example-carrier",
"url": "https://carrier.example/track/SYNTHETIC-TRACKING-123"
}
}| event_type | Required resource | Example statuses |
|---|---|---|
| prescription.status | prescription_id | signed, sent, received, clarification_required |
| dispense.status | order_id | under_review, preparing, filled, partial |
| shipment.status | shipment_id | created, picked_up, in_transit, delivered |
The patient and domain resource must already have tenant-and-connection-scoped links. EzPharma never joins an event using a name, date of birth, medication name, or other probabilistic demographic match.
/v1/care-episodes/{careEpisodeId}/timelineClient API keyReturns the authenticated tenant's canonical events for one care episode.
curl https://www.ezpharma.app/v1/care-episodes/episode_demo_123/timeline \
-H "Authorization: Bearer $EZPHARMA_API_KEY"{
"careEpisodeId": "episode_demo_123",
"events": [
{
"eventType": "visit.completed",
"occurredAt": "2026-10-22T14:30:00Z",
"receivedAt": "2026-10-22T14:30:04Z",
"sourceSystem": "example-ehr"
}
]
}A care episode outside the API key's tenant returns 404, the same as a missing episode. This prevents cross-tenant resource discovery.
EzPharma can deliver approved lifecycle events to a tenant endpoint. Configure the endpoint in the client workspace and keep its secret server-side.
| Header | Value | Purpose |
|---|---|---|
| X-EzPharma-Timestamp | Unix seconds | Reject stale deliveries and build the signature input. |
| X-EzPharma-Signature | sha256=<hex> | HMAC-SHA256 of the timestamp, a literal period, and the exact body. |
| X-EzPharma-Event-Id | Opaque event ID | Persist and deduplicate before applying business logic. |
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verifyEzPharmaWebhook(rawBody, headers, secret, now = Date.now()) {
const timestamp = headers['x-ezpharma-timestamp']
const signature = headers['x-ezpharma-signature']
if (typeof timestamp !== 'string' || !/^\d{1,12}$/.test(timestamp) ||
Math.abs(now / 1000 - Number(timestamp)) > 300 ||
typeof signature !== 'string' || !/^sha256=[a-f0-9]{64}$/.test(signature)) return false
const expected = createHmac('sha256', secret)
.update(timestamp + '.' + rawBody).digest()
const provided = Buffer.from(signature.slice(7), 'hex')
return provided.length === expected.length && timingSafeEqual(provided, expected)
}Timestamp validation alone does not prevent duplicate effects: persist and deduplicate the event ID in durable storage. Return a 2xx only after the event is durably accepted. Process asynchronously where possible. Failed deliveries are retried and can be inspected or retried by an authorized operator.
Lifecycle events exclude clinical answers unless a later, explicitly reviewed contract requires them. Do not place secrets or patient data in response bodies or logs.
The CRM framework includes a disabled provider catalog and a synthetic-only lab. HubSpot, Salesforce, Dynamics/Dataverse, Zoho CRM and Pipedrive remain in scope; no actual vendor connection, OAuth app or patient export is enabled. Protocol fixture tests do not establish vendor-account acceptance.
Client owners/admins with current membership and recent MFA can use the lab from the client workspace in an authorized synthetic workspace. It maps bounded operational intake-event metadata to durable simulators, with immutable versions, explicit backfill, safe retry, conflict quarantine and protected recovery evidence. Source clinical/payment records and consent remain unchanged.
Native session routes use /api/portal/workspaces/{tenantId}/crm: GET connection health; POST /connections with a provider to create a simulator; GET /connections/{id} for fields/versions; POST its /preview, /mappings, /state, /backfill, /retry or /reconcile for authorized management. POST /run dispatches up to 20 simulated jobs. These routes do not accept client API bearer credentials or caller-selected operating modes.
Live/sandbox workspaces receive only the disabled catalog; simulator operations accept only synthetic tenants. Mapping inputs allow known source/destination fields, a unique external ID, bounded transforms and null handling. Errors are coded: 403 authority/synthetic boundary, 404 missing connection, 409 conflict/state/schema/reconciliation, 422 invalid input, 503 service failure. Responses are private and uncached. The server-only OAuth/credential kernel and association request prototypes are fixture-tested. Real provider brokers, HTTP callback routes, change feeds, runtime wiring and actual-account reconciliation remain unfinished; vendor delivery is deferred until demand.
The client workspace links to a native provider/pharmacy contract simulator. Authorized owners/admins with recent MFA can publish immutable synthetic identifier mappings, queue versioned exercises, inspect independent projections and reconcile durable acknowledgements. Live/sandbox workspaces receive disabled readiness information; simulator operations require a synthetic tenant. No clinical signature, patient data export, actual prescription, dispensing or shipment occurs.
Download the onboarding packet and versioned contract schema. The packet includes supported operations, per-operation scenarios and resource targets, authority boundaries, transport options and outstanding partner inputs. Actual partner accounts, specifications, catalogs, privacy/clinical acceptance and certification remain separate requirements. Controlled substances are excluded.
Native session API: /api/portal/workspaces/{tenantId}/partner-readiness. GET the catalog; POST /connections with domain; GET /connections/{id} for safe evidence. POST its /mappings with commandId, mapping, version, /state with action, /jobs with commandId, operation, resource, scenario, /run with an empty object, or /recovery with jobId, action. Actions are activate/pause/revoke/reconnect and reconcile/retry/skip respectively. No caller token, mode or endpoint override is accepted.
Unknown/stale/out-of-order evidence is quarantined. Timeouts require a durable receipt reread, never blind retransmission. Repeated commands reuse immutable receipts; changed retries conflict. Projections stay separate by operation and package identity. Replies are no-store/no-referrer; errors are coded 403 authority, 404 missing record, 409 state/replay/reconciliation, 422 invalid fields and 503 unavailable. The signed simulator acknowledgement verifier is internal; no public partner callback route is added. Existing reviewed partner ingress and actual credentials are required for future adapters.
Healthcare integrations fail when authorization, identity matching, and operational states are treated as shortcuts. EzPharma makes those boundaries explicit.
Installation keys, user memberships, partner connections, and client API keys resolve tenant ownership. Hostnames and request parameters do not.
Clinical access requires both the runtime switch and a current tenant grant, in addition to individual clinical authority. Synthetic tenants can use simulator connections only; sandbox tenants cannot use production connections. A live tenant may retain explicitly configured sandbox connections for testing, but cannot use simulator evidence as live certification. Requests cannot grant capabilities or override connection classification. Incompatible connections fail closed, including on worker retries. Synthetic simulator connections require separate owner provisioning and cannot certify a real partner. Clinical traffic remains gated independently.
Simulator transports use server-selected connections for prescription, laboratory, pharmacy, communications, payment, fulfillment and carrier exercises. They contain no live endpoint or processor credential. Normal synthetic checkout can resolve inline through its local merchant adapter without moving money; real processor payments still use their signed provider evidence. Accepted simulator delivery never signs a prescription, approves care or certifies a partner.
The internal /simulators workspace requires a current synthetic operator membership, verified email, MFA and recent TOTP step-up. Its bounded evidence and patient-code inbox use no-store and no-referrer; unknown fields, tenant selectors and endpoint/credential overrides are rejected. Patient codes retain native expiry, one-time use and account scope. Simulator migration, provisioning, release and enabled acceptance are separate stages.
Authorized clinical users can submit an existing lab order through POST /api/clinical/review-cases/:caseId/lab-orders/:labOrderId/transmit with {commandId}. Normal case authority, MFA, runtime and tenant gates apply. Delivery evidence does not establish collection, a result or patient release.
The native patient session scopes GET /v1/patient/portal/communication-preferences and POST /v1/patient/portal/communication-preferences. POST accepts {commandId, optionalEmailEnabled}; it returns {preferences: {optionalEmailEnabled}}, 401 without patient authority, 422 for invalid input, or 409 for conflicting command reuse. No tenant or recipient selector authorizes access. Responses use no-store and no-referrer.
Optional mail requires explicit consent. Opt-out links use an opaque fragment consumed by the preference page and submitted as {token} to POST /v1/communications/unsubscribe. The response is always generic {ok: true} for handled requests and reveals no account. It cannot opt in or expose patient records. Signed provider complaints/bounces suppress delivery; retries recheck suppression. Transactional classification stays independent. Availability requires the matching release and migration 041; client/sender acceptance remains separate.
External resources are linked within one tenant and one connection. Missing or ambiguous links stop processing.
Source and canonical events are retained. Corrections append; they do not erase history.
Payment providers, URLs, callbacks, logs, and audit metadata receive only approved routing and operational fields.
The current HTTP contract is under /v1. Additive response fields may appear without a version change, so clients should ignore fields they do not recognize.
| Change | Policy |
|---|---|
| Additive field or event type | May be introduced within v1. New event types and status mappings still require connection-level approval before use. |
| Breaking request or response change | Introduced under a new version with a documented migration and overlap period. |
| Capability enablement | Provisioned and certified independently of the API version. Implementation does not imply enablement. |
Email hello@ezpharma.app with your organization, integration type, and intended sandbox workflow. Do not send credentials or patient information by email.
GET /health/ready returns a coarse ready/unavailable status (200/503), with no patient, tenant, queue or dependency detail. It checks dependency/queue posture and freshness of the protected readiness job. It grants no application access.
GET /api/platform/contact returns the configured platform account-support email only. Patient support remains tenant-resolved and the operational incident recipient stays private.
Patient support and branded email Reply-To come from the authorized client’s support setting. Reserved support_email and client_name template bindings cannot be overridden by event variables. Missing client support blocks patient email processing; platform incident contacts are separate.