Profile Health API

GraphQL is the canonical surface; a thin REST facade is exposed for partners that don't speak GraphQL. All cross-cutting concerns — auth, rate limits, audit, resilience — are enforced once in the gateway.

Quick Start

You need a partner API key (Bearer pk_live_*). Profile Health system administrators issue keys via the admin UI. Ask your contact to provision one for the sandbox first.

curl -X POST https://api.profilehealth.com/graphql \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.yyyyyyyyyyyy" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ health { status } }"}'

Returns {"data":{"health":{"status":"ok"}}}. The health query needs no scope — use it to verify connectivity before making scope-gated calls. Add an Idempotency-Key header on mutations; it must be ≤128 chars and unique per logical operation.

What you can do

GraphQL is the canonical surface. Every field is scope-gated — your API key carries only the scopes your partner agreement grants. All data access is clinic-scoped: a key can only read or write resources that belong to the clinics in its allowedClinicIds list.

Explore the live schema: GraphQL Voyager — schema.graphql (SDL)

Use caseGraphQL operationsRequired scope(s)
Patient & Intake Queries: patient, patients, patientDossier
Mutations: createIntakeInvitation
patient:read, intake:write
Labs & Documents Queries: labResults, documents
Mutations: uploadDocument
patient:read, document:read, document:write
Clinical Notes (SOAP) Queries: composition, compositions
Mutations: createComposition, updateCompositionSection, signComposition, amendComposition, deleteComposition
note:read, note:write, note:sign, note:amend
Encounters Queries: encounter, encounters
Mutations: createEncounter, updateEncounter, deleteEncounter
encounter:read, encounter:write
Scratch Notes Queries: scratchNote, scratchNotes
Mutations: createScratchNote, updateScratchNote, deleteScratchNote
scratch-note:read, scratch-note:write
AI Synthesis Queries: synthesis
Mutations: requestSynthesis
synthesis:read, synthesis:run
Health Information Exchange (Carequality / KNO2) Queries: hieQuery
Mutations: hieDocuments
hie:query; hie:query + document:write (hieDocuments)

Partner and API-key administration (admin:partners:*) and audit-event queries (admin:audit:read) exist in the schema but require admin scopes restricted to Profile Health administrators. Partners do not receive these scopes.

patientDossier is the one-call aggregate: a single query returns patient demographics, encounters, documents, observations, conditions, medications, allergies, procedures, family history, questionnaire responses, and consents assembled server-side. The return type is a union: PatientDossier when the chart is ready, DossierPending (poll again) when the patient has not yet completed intake or the dossier is still assembling.

Error Codes

Errors carry a code in GraphQL extensions or RFC 7807 title for REST. Always include the x-correlation-id response header in support requests — server logs are searchable by it.

CodeHTTPMeaning
UNAUTHENTICATED401No credentials, malformed bearer, or invalid signature.
FORBIDDEN403Authenticated, but missing required scope or trying to act on a clinic the partner does not own.
NOT_FOUND404Resource does not exist or is not visible to this principal.
METHOD_NOT_ALLOWED405HTTP method not supported on this endpoint (e.g. GET on /graphql). Honor the Allow response header.
CONFLICT409Duplicate resource, or stale precondition (e.g. updating a revoked key).
PAYLOAD_TOO_LARGE413Request body exceeds the size limit. Reduce the payload (e.g. submit a smaller Bundle).
VALIDATION_FAILED422Input failed schema validation. Response includes which field failed.
RATE_LIMITED429Per-principal quota exceeded. Honor Retry-After / RateLimit-Reset headers.
IDEMPOTENCY_KEY_REUSED422Same Idempotency-Key seen with a different payload, or replay detected.
UPSTREAM_TIMEOUT504Upstream (Medplum, Firestore, etc.) responded too slowly.
UPSTREAM_UNAVAILABLE503Upstream circuit breaker open, bulkhead full, or service not configured.
INTERNAL500Unhandled server error. Detail is never sent to clients — quote the correlationId in support requests.
KIT_ALREADY_ACTIVATED409This kit code has already been activated. Do not retry with the same code; have the patient contact support quoting the correlationId.

Sample error responses

Three of the most common error shapes — full reference + every code is in openapi.json (Swagger UI shows them under each endpoint's "Responses" tab).

401 — UNAUTHENTICATED (missing/wrong Bearer)

HTTP/1.1 401 Unauthorized
content-type: application/problem+json
x-correlation-id: 7e8c3a2b-19d4-4f6e-b551-aab1f9c2d3e4

{
  "type": "https://errors.profilehealth.com/unauthenticated",
  "title": "UNAUTHENTICATED",
  "status": 401,
  "detail": "Authentication required to access Mutation.createIntakeInvitation",
  "correlationId": "7e8c3a2b-19d4-4f6e-b551-aab1f9c2d3e4"
}

422 — VALIDATION_FAILED (e.g. missing Idempotency-Key)

HTTP/1.1 422 Unprocessable Entity
content-type: application/problem+json

{
  "type": "https://errors.profilehealth.com/validation-failed",
  "title": "VALIDATION_FAILED",
  "status": 422,
  "detail": "Idempotency-Key header is required for this operation",
  "correlationId": "7e8c3a2b-19d4-4f6e-b551-aab1f9c2d3e4"
}

429 — RATE_LIMITED (honor Retry-After)

HTTP/1.1 429 Too Many Requests
content-type: application/problem+json
ratelimit-limit: 50
ratelimit-remaining: 0
ratelimit-reset: 1735000060
retry-after: 1

{
  "type": "https://errors.profilehealth.com/rate-limited",
  "title": "RATE_LIMITED",
  "status": 429,
  "detail": "Rate limit exceeded for partner par_001 (50 req/s)",
  "correlationId": "7e8c3a2b-19d4-4f6e-b551-aab1f9c2d3e4"
}

Reference

Operations

Architecture

Partners writing integrations should read the Integration Guide, which covers credential provisioning, the error contract, idempotency, rate-limit tiers, and clinic scoping end-to-end.