# Huli Developers — llms-full.txt # Generated: 2026-09-23T17:01:06.001Z # This file concatenates the cleaned source of every documentation page. # Use it to provide the full docs corpus as a single context window. ======================================================================== # Huli Developers # URL: https://developers.huli.ai/ # Developer documentation for the Huli Public API (SMART on FHIR R4 and R5) and the huli CLI. Run your first FHIR request in the browser in under a minute — no key required. :::::LandingHero headline="The FHIR API for LATAM healthcare" subhead="Read and write clinical data over standards-compliant SMART on FHIR, modeled for the names, identifiers, and catalogs LATAM clinics actually use. First request in the browser, no key required." primary-label="Start building" primary-href="/v1/start" ::::: ======================================================================== # AI Agents # URL: https://developers.huli.ai/v1/agents # Downloadable, model-agnostic artifacts for wiring the Huli Public API into an LLM agent — a ready-to-use system prompt and OpenAI/Anthropic tool definitions. # AI Agents Two downloadable artifacts help you connect an LLM agent to the Huli Public API without hand-authoring boilerplate. Both are regenerated from the API contract on every docs build, so they stay accurate as the API evolves. ## System prompt A model-agnostic system prompt that primes any LLM (Claude, GPT, Gemini, …) with what it needs to call the Huli Public API correctly: the SMART on FHIR R4 model, authentication, the `system/.` scope grammar, the available FHIR resources and their interactions, the `OperationOutcome` error shape, and cursor pagination. Drop it into your agent's system message verbatim. **Download:** [`/huli-system-prompt.md`](/huli-system-prompt.md) ## OpenAPI toolset Tool / function definitions derived directly from the Huli Public API OpenAPI specification — one entry per API operation. The JSON object exposes two top-level keys so you can paste the matching block into your provider's request: - `openai` — an array of `{ type: "function", function: { name, description, parameters } }` objects for the OpenAI / Azure OpenAI `tools` parameter. - `anthropic` — an array of `{ name, description, input_schema }` objects for the Anthropic Messages API `tools` parameter. Each tool's parameter schema lists the operation's path and query parameters (with types and which are required); operations with a request body expose a single `body` object property naming the FHIR resource. The schemas are intentionally shallow — they give the model the operation names and argument shapes, not a full FHIR expansion. Pair them with the [API reference](/v1/api) and the [system prompt](#ai-agents.system-prompt) above for field-level detail. **Download:** [`/huli-openapi-toolset.json`](/huli-openapi-toolset.json) ## See also - [`/llms.txt`](/llms.txt) — a pointer index of every documentation page, for retrieval-augmented agents. - [`/llms-full.txt`](/llms-full.txt) — the entire docs corpus concatenated into one file. ======================================================================== # API Reference # URL: https://developers.huli.ai/v1/api # The Huli Public API is a standards-based FHIR REST surface — the safest bet for an EHR, lab, or billing integration. Browse resources, auth, and scopes. # API Reference **The Huli API is a standards-based FHIR REST surface** — the safest bet for an EHR, lab, or billing integration. Read and write real clinical data through the same FHIR resources your existing tools already speak, with one OAuth flow and one scope model across both FHIR R4 and R5. ## Why FHIR first FHIR is the data model the healthcare integrator community already runs on. Adopt it once and every downstream system — EHR, HIE, lab, billing, and your own AI agent — understands your data without a per-vendor adapter. Every resource here maps a standard FHIR shape to the underlying HuliPractice record; a couple of endpoints (authentication, capability discovery) are plain REST and clearly marked. Both **FHIR R4** (the stable default) and **FHIR R5** are served side by side — same token, same scopes, same pagination. Switch releases with the **R4 / R5** control in the sidebar, or from the switch on any resource page. ## Explore the API ======================================================================== # Authentication # URL: https://developers.huli.ai/v1/api/authentication # OAuth 2.0 token endpoint for SMART Backend Services. # Authentication OAuth 2.0 token endpoint for SMART Backend Services. > This reference is generated from the canonical Huli Public API OpenAPI specification — do not edit it directly. ## Endpoints ### Request Access Token OAuth 2.0 token endpoint implementing the SMART Backend Services `client_credentials` grant with `private_key_jwt` client authentication. The client must submit a signed JWT assertion (JWS, RS384) containing: - `iss` and `sub`: your `client_id` (API key prefix) - `aud`: this token endpoint URL - `exp`: expiration (max 5 minutes from now) - `iat`: issued-at timestamp - `jti`: unique token ID (for replay prevention) The server validates the assertion signature against the client's registered JWKS endpoint, checks claim validity, and issues a short-lived access token (5-minute TTL). Rate limited to 20 requests per minute per source IP. #### Request Body #### Response — 200 ```json { "access_token": "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9...", "token_type": "bearer", "expires_in": 300, "scope": "system/Patient.rs system/Appointment.rs" } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ | jq -r .access_token) curl -X POST https://api.huli.ai/auth/token \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @token.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/auth/token`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.post( f"https://api.huli.ai/auth/token", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class createTokenExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/auth/token")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("POST", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func createTokenExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("POST", "https://api.huli.ai/auth/token", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Introspect a bearer credential Returns non-sensitive metadata about the bearer credential in the `Authorization` header — in particular whether it is a **sandbox** or a **production** key. Used by the interactive playground to refuse production credentials before running any request from a browser (sandbox keys only). Carries no PHI. Rate limited per source IP. #### Response — 200 ```json { "mode": "sandbox", "org_id": "0193b7c8-d4e7-7000-8000-000000009001", "key_prefix": "htk_ab12", "expires_on": "2026-07-28T00:00:00Z", "volume_cap": 10000, "scopes": [ "system/Patient.cru" ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ | jq -r .access_token) curl -X GET https://api.huli.ai/auth/introspect \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/auth/introspect`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/auth/introspect", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class introspectTokenExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/auth/introspect")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func introspectTokenExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/auth/introspect", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Revoke Access or Refresh Token OAuth 2.0 token revocation endpoint ([RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009)). Revokes an access token or refresh token previously issued by `/auth/token` or the interactive Authorization Code + PKCE flow. Returns 200 even when the token is already revoked or unknown, as required by the RFC. #### Request Body #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ | jq -r .access_token) curl -X POST https://api.huli.ai/auth/revoke \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @revoke.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/auth/revoke`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.post( f"https://api.huli.ai/auth/revoke", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class revokeTokenExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/auth/revoke")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("POST", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func revokeTokenExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("POST", "https://api.huli.ai/auth/revoke", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: --- ### OAuth Authorization Endpoint (Interactive) Interactive OAuth 2.1 Authorization Code + PKCE entry point for the user-facing consent flow. Redirects the browser to the Huli consent UI; on consent (or denial) the server redirects back to the registered `redirect_uri` with the appropriate authorization code or `error` query parameter. Requires `code_challenge` (S256). Public clients use this flow alongside `client_credentials` for M2M; both surfaces share the same access-token format. #### Query Parameters #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ | jq -r .access_token) curl -X GET https://api.huli.ai/authorize \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/authorize`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/authorize", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class authorizeExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/authorize")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func authorizeExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/authorize", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: --- ### Consent Request Info Returns the metadata the consent UI needs to render the prompt for an in-flight authorization request (client name, requested scopes, redirect_uri). Requires a session cookie established by the `/authorize` redirect. #### Query Parameters #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ | jq -r .access_token) curl -X GET https://api.huli.ai/authorize/consent_info \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/authorize/consent_info`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/authorize/consent_info", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getConsentInfoExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/authorize/consent_info")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getConsentInfoExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/authorize/consent_info", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: --- ### Grant Consent Records the operator's consent for an in-flight authorization request and returns the redirect URL (`redirect_to`) the frontend should navigate to. Requires the `integrations.manage` permission (Owner / Admin only). #### Request Body #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ | jq -r .access_token) curl -X POST https://api.huli.ai/authorize/consent \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @consent.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/authorize/consent`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.post( f"https://api.huli.ai/authorize/consent", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class grantConsentExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/authorize/consent")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("POST", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func grantConsentExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("POST", "https://api.huli.ai/authorize/consent", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: --- ### Deny Consent Records the operator's denial of an in-flight authorization request and returns the redirect URL with an OAuth `error=access_denied` parameter. #### Request Body #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ | jq -r .access_token) curl -X POST https://api.huli.ai/authorize/deny \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @deny.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/authorize/deny`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.post( f"https://api.huli.ai/authorize/deny", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class denyConsentExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/authorize/deny")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("POST", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func denyConsentExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("POST", "https://api.huli.ai/authorize/deny", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: --- ======================================================================== # Discovery # URL: https://developers.huli.ai/v1/api/discovery # Public endpoints for SMART configuration and FHIR capability discovery. # Discovery Public endpoints for SMART configuration and FHIR capability discovery. > This reference is generated from the canonical Huli Public API OpenAPI specification — do not edit it directly. ## Endpoints ### SMART Configuration (root alias) Defensive root alias of the canonical `/fhir/.well-known/smart-configuration`. SMART Backend Services clients should derive discovery from the issuer (`https://api.huli.ai/fhir`), i.e. `/fhir/.well-known/smart-configuration`. This root path serves the **identical** document so a client that probes the host root instead of the issuer path still resolves (rather than receiving an error). The returned `issuer` is still `…/fhir`. #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ | jq -r .access_token) curl -X GET https://api.huli.ai/.well-known/smart-configuration \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/.well-known/smart-configuration`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/.well-known/smart-configuration", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getSmartConfigurationRootAliasExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/.well-known/smart-configuration")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getSmartConfigurationRootAliasExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/.well-known/smart-configuration", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: --- ### SMART Configuration Returns the [SMART on FHIR discovery document](https://hl7.org/fhir/smart-app-launch/conformance.html) describing the server's authorization capabilities, supported scopes, and token endpoint URL. The discovery document is issuer-rooted under `/fhir`: the full URL is `https://api.huli.ai/fhir/.well-known/smart-configuration` and it advertises a `jwks_uri` of `https://api.huli.ai/fhir/.well-known/jwks.json`. #### Response — 200 ```json { "issuer": "https://api.huli.ai/fhir", "jwks_uri": "https://api.huli.ai/fhir/.well-known/jwks.json", "authorization_endpoint": "", "token_endpoint": "https://api.huli.ai/auth/token", "token_endpoint_auth_methods_supported": [ "private_key_jwt", "none" ], "token_endpoint_auth_signing_alg_values_supported": [ "RS384" ], "grant_types_supported": [ "client_credentials", "authorization_code", "refresh_token" ], "scopes_supported": [ "system/Patient.rs", "system/Patient.cru", "system/Appointment.rs", "system/Appointment.cru", "system/Encounter.rs", "system/Encounter.cru", "system/Observation.rs", "system/Observation.cru", "system/MedicationRequest.rs", "system/MedicationRequest.cru", "system/ServiceRequest.rs", "system/ServiceRequest.cru", "system/Composition.rs", "system/Composition.cru", "system/DocumentReference.rs", "system/DocumentReference.cru", "system/Practitioner.rs", "system/Organization.rs", "system/Subscription.rs", "system/Subscription.crud", "user/Patient.rs", "user/Patient.cru", "user/Appointment.rs", "user/Appointment.cru", "user/Encounter.rs", "user/Encounter.cru", "user/Observation.rs", "user/Observation.cru", "user/MedicationRequest.rs", "user/MedicationRequest.cru", "user/ServiceRequest.rs", "user/ServiceRequest.cru", "user/Composition.rs", "user/Composition.cru", "user/DocumentReference.rs", "user/DocumentReference.cru", "user/Practitioner.rs", "user/Organization.rs" ], "capabilities": [ "client-confidential-asymmetric" ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/.well-known/smart-configuration \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/.well-known/smart-configuration`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/.well-known/smart-configuration", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getSmartConfigurationExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/.well-known/smart-configuration")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getSmartConfigurationExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/.well-known/smart-configuration", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: --- ### JSON Web Key Set Returns the server's [JSON Web Key Set](https://datatracker.ietf.org/doc/html/rfc7517) — the active signing key plus any overlap-window key during rotation. Partners fetch this to verify access-token signatures out-of-band without round-tripping to the server. Advertised as `jwks_uri` in the SMART configuration document. #### Response — 200 ```json { "keys": [ { "kty": "RSA", "use": "sig", "alg": "RS384", "kid": "2026-05-01", "n": "0vx7agoeb...", "e": "AQAB" } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/.well-known/jwks.json \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/.well-known/jwks.json`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/.well-known/jwks.json", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getJwksExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/.well-known/jwks.json")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getJwksExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/.well-known/jwks.json", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: --- ### Capability Statement Returns the FHIR R4 [CapabilityStatement](https://hl7.org/fhir/R4/capabilitystatement.html) describing this server's supported resources, operations, and search parameters. #### Response — 200 ```json { "resourceType": "CapabilityStatement", "status": "active", "date": "2026-05-01", "kind": "instance", "fhirVersion": "4.0.1", "format": [ "application/fhir+json" ], "rest": [ { "mode": "server", "resource": [ { "type": "Patient", "interaction": [ { "code": "read" }, { "code": "search-type" }, { "code": "create" }, { "code": "update" } ] }, { "type": "Appointment", "interaction": [ { "code": "read" }, { "code": "search-type" }, { "code": "create" }, { "code": "update" } ] }, { "type": "Encounter", "interaction": [ { "code": "read" }, { "code": "search-type" }, { "code": "create" }, { "code": "update" } ] }, { "type": "Observation", "interaction": [ { "code": "read" }, { "code": "search-type" }, { "code": "create" }, { "code": "update" } ] }, { "type": "Practitioner", "interaction": [ { "code": "read" } ] }, { "type": "Organization", "interaction": [ { "code": "read" } ] } ] } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/metadata \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/metadata`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/metadata", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getMetadataExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/metadata")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getMetadataExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/metadata", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: --- ======================================================================== # FHIR Implementation Guide # URL: https://developers.huli.ai/v1/api/fhir-ig # The Huli FHIR Implementation Guide — canonical profiles, extensions, and value sets for FHIR R4 and R5. # FHIR Implementation Guide The Huli FHIR Implementation Guide (IG) is the canonical set of profiles, extensions, and value sets the API validates against — one guide per release. It documents the exact resource shapes, cardinalities, and terminology bindings your integration must conform to. - **FHIR R4** — [developers.huli.ai/fhir/r4/](https://developers.huli.ai/fhir/r4/) — the stable default. - **FHIR R5** — [developers.huli.ai/fhir/r5](https://developers.huli.ai/fhir/r5) — publishing soon. The R5 Implementation Guide is still being deployed; its link goes live when R5 ships. R4 is the stable default today. For a resource-by-resource overview inside these docs, see [FHIR resources](/v1/api/resources). ======================================================================== # Choosing R4 vs R5 # URL: https://developers.huli.ai/v1/api/fhir-versions # When to target FHIR R4 vs R5 on the Huli Public API — both are supported; pick by wire shape and integration maturity. # Choosing R4 vs R5 The Huli Public API serves **both** FHIR releases side by side. Pick the one that matches your integration — you do not have to migrate, and both stay supported. Use the **R4 / R5 switch** in the API sidebar (or the switch on any resource page) to browse either release. | | FHIR R4 | FHIR R5 | |---|---------|---------| | Base path | `/fhir/R4` | `/fhir/R5` | | CapabilityStatement | `GET /fhir/R4/metadata` | `GET /fhir/R5/metadata` | | `fhirVersion` | `4.0.1` | `5.0.0` | | Resource reference | [R4 resources](/v1/api/resources) | [R5 resources](/v1/api/resources) | | Maturity | Stable default | Newer wire shapes | ## Use R4 when R4 is the **stable default** and the right choice for the vast majority of integrations: - You already integrate against `/fhir/R4` — there's no reason to change. - Your client library, middleware, or partner system speaks R4 (the most widely deployed FHIR release). - You want the surface with the most third-party tooling and examples. ## Use R5 when R5 advertises the newer R5 wire shapes and one capability R4 cannot express: - **Appointment recurrence.** Only R5 supports recurring series via the `recurrenceTemplate` element (POST with a `recurrenceTemplate` creates a series; series edits select scope with the `_recurrenceScope` query parameter — `this` | `this-and-following` | `all`). R4 has no equivalent. See the [R5 Appointment reference](/v1/api/r5/appointment). - **Encounter redesign.** R5 reshapes Encounter: `class` becomes a list (`class[]`), the period field is `actualPeriod`, and `subjectStatus` replaces the R4 patient-status modelling. See the [R5 Encounter reference](/v1/api/r5/encounter). - **R5 Observation shape.** R5 Observation follows the R5 element model. See the [R5 Observation reference](/v1/api/r5/observation). ## What's the same across both Authentication and access control are **identical** — there is one auth surface, not two: - Same [SMART Backend Services authentication](/v1/auth) and `POST /auth/token` flow. - Same [scopes](/v1/scopes): a `system/Patient.rs` token works against both `/fhir/R4` and `/fhir/R5`. The release is chosen by the request path, not the scope. - Same [cursor-based pagination](/v1/concepts/pagination) (`_count`, `next`). - The same resources are served on **both** releases: Patient, Appointment, Encounter, Observation, MedicationRequest, ServiceRequest, Composition, DocumentReference, Practitioner, Organization, HealthcareService, PractitionerRole, Schedule, Slot. Only Subscription, Location, Device, CodeSystem, ValueSet remain R4-only. Because the auth and scope model is shared, you can call both releases with the same access token — switch by changing `/fhir/R4` to `/fhir/R5` in the request path. ======================================================================== # Appointment (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/appointment # A booking of a healthcare event between patient(s) and practitioner(s) for a specific date/time. Maps to HuliPractice ap # Appointment (FHIR R4) A booking of a healthcare event between patient(s) and practitioner(s) for a specific date/time. Maps to HuliPractice appointment slots with start/end times, status, and participant references. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [Appointment — HL7 FHIR R4](https://hl7.org/fhir/R4/appointment.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R4/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R4/{Resource}/{id}`) ## Scopes Scopes are shared across FHIR releases — the same `system/Appointment.*` scope grants Appointment access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-appointment-rs) - — see [scope reference](/v1/scopes#system-appointment-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `patient` | reference | Resource reference — supply the UUID of the referenced resource. | | `practitioner` | reference | Resource reference — supply the UUID of the referenced resource. | | `date` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `status` | token | Exact match. For identifiers use `system|value` format. | | `appointment-type` | token | Exact match. For identifiers use `system|value` format. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_cursor` | string | Case-insensitive partial match. | ## Endpoints ### Read Appointment Retrieve a single Appointment resource by its ID. **Required scope:** #### Response — 200 ```json { "resourceType": "Appointment", "id": "770e8400-e29b-41d4-a716-446655440010", "status": "booked", "start": "2026-06-01T09:00:00Z", "end": "2026-06-01T09:30:00Z", "participant": [ { "actor": { "reference": "Patient/550e8400-e29b-41d4-a716-446655440001", "display": "Maria Garcia" }, "status": "accepted" }, { "actor": { "reference": "Practitioner/880e8400-e29b-41d4-a716-446655440020", "display": "Dr. Rodriguez" }, "status": "accepted" } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Appointment.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Appointment/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Appointment/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Appointment/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getAppointmentExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Appointment/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getAppointmentExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Appointment/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Update Appointment Update an existing Appointment. The target `status` drives the operation: - **`status: cancelled`** → cancels the appointment. Requires the `appointments.delete` permission and a `cancelationReason` that resolves to one of the organization's active cancellation reasons (matched by the coding `code` or a SNOMED coding). The cancel is **blocked (`409`)** when a clinical encounter is linked to the appointment, and rejected (`422`) when the reason is missing or unresolvable. `comment` maps to the cancellation note. - **`status: entered-in-error`** → marks the appointment as a data-entry error (a correction, not a cancellation). Requires the `appointments.edit` permission; no cancellation reason is needed. - **any other change** (reschedule / field edit) → re-runs the double-booking / availability / calendar-scope checks; an overlapping slot is rejected with `409`. Requires `appointments.edit`. A field edit carries the full appointment shape (`start`, `end`, participants, `serviceType`). Supports optimistic concurrency: supply the `If-Match` header with the `ETag` from the last read to make the update conditional. A stale validator is rejected with a `409` version conflict (distinct from the double-booking `409`, though both carry HPB-00103). The response carries `ETag` + `Last-Modified` for the new version. **Required scope:** #### Request Body #### Request Example ```json { "resourceType": "Appointment", "status": "cancelled", "cancelationReason": { "coding": [ { "system": "http://snomed.info/sct", "code": "185332005" } ], "text": "Patient request" }, "comment": "Patient called to cancel" } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Appointment.cru" \ | jq -r .access_token) curl -X PUT https://api.huli.ai/fhir/R4/Appointment/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @resource.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Appointment/${id}`, { method: "PUT", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.put( f"https://api.huli.ai/fhir/R4/Appointment/{resource_id}", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class updateAppointmentExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Appointment/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("PUT", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func updateAppointmentExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("PUT", "https://api.huli.ai/fhir/R4/Appointment/"+resourceID+"", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00103](/v1/errors#hpb-00103) | 409 | Version conflict | | [HPB-00101](/v1/errors#hpb-00101) | 422 | Unprocessable entity | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Search Appointments Search for Appointment resources using FHIR search parameters. **Required scope:** #### Query Parameters #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Appointment.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Appointment \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Appointment`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Appointment", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchAppointmentsExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Appointment")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchAppointmentsExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Appointment", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Create Appointment Create a new Appointment resource. The server assigns the resource ID and always books the appointment in the `booked` state. The create runs the full scheduling rule set, so the request body must name the resources an appointment requires: - **`serviceType`** (required) — `serviceType[0].coding[0]` must reference a Huli organization service via the `https://fhir.huli.ai/r4/CodeSystem/org-service` code system, with the service UUID as the `code`. The service drives appointment type, specialty, booking policy, and the per-service resource requirements. A missing or unresolvable `serviceType` is rejected (`400`) — it is a structural-validation failure (all of which return `400`; `422` is reserved for business-rule rejections). - **At least one Practitioner participant** and **one Location (room) participant** — appointments are unschedulable without them. Equipment is supplied as `Device` participants. The server runs double-booking / availability / calendar-scope checks: a time slot that overlaps an existing booking for any participant is rejected with `409`. Telehealth services auto-provision a telemedicine session. Requires the `appointments.create` permission in addition to the `system/Appointment.cru` scope. **Required scope:** #### Request Body #### Request Example ```json { "resourceType": "Appointment", "status": "booked", "start": "2026-06-15T14:00:00Z", "end": "2026-06-15T14:30:00Z", "serviceType": [ { "coding": [ { "system": "https://fhir.huli.ai/r4/CodeSystem/org-service", "code": "990e8400-e29b-41d4-a716-446655440030" } ] } ], "participant": [ { "actor": { "reference": "Patient/550e8400-e29b-41d4-a716-446655440001" }, "status": "accepted" }, { "actor": { "reference": "Practitioner/880e8400-e29b-41d4-a716-446655440020" }, "status": "accepted" }, { "actor": { "reference": "Location/aa0e8400-e29b-41d4-a716-446655440040" }, "status": "accepted" } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Appointment.cru" \ | jq -r .access_token) curl -X POST https://api.huli.ai/fhir/R4/Appointment \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @appointment.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Appointment`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.post( f"https://api.huli.ai/fhir/R4/Appointment", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class createAppointmentExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Appointment")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("POST", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func createAppointmentExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("POST", "https://api.huli.ai/fhir/R4/Appointment", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00103](/v1/errors#hpb-00103) | 409 | Version conflict | | [HPB-00101](/v1/errors#hpb-00101) | 422 | Unprocessable entity | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # CodeSystem (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/codesystem # FHIR R4 CodeSystem resource. # CodeSystem (FHIR R4) FHIR R4 CodeSystem resource. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [CodeSystem — HL7 FHIR R4](https://hl7.org/fhir/R4/codesystem.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) Terminology resource. Scope is enforced per CodeSystem id by the handler (e.g. `address-source` → `system/Patient.rs`, `org-service` → `system/Appointment.rs`). ## Scopes Scopes are shared across FHIR releases — the same `system/CodeSystem.*` scope grants CodeSystem access on both `/fhir/R4` and `/fhir/R5`. ## Endpoints ### Read CodeSystem Retrieve a static Huli CodeSystem by id. Scope is enforced per-id: - `address-source` requires `system/Patient.rs` - `org-service` requires `system/Appointment.rs` **Required scope:** #### Response — 200 ```json { "resourceType": "CodeSystem", "url": "https://huli.io/fhir/CodeSystem/address-source", "status": "active", "content": "complete", "concept": [ { "code": "mx-normativo-nom024", "display": "NOM-024 normative address source" } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Patient.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/CodeSystem/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/CodeSystem/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/CodeSystem/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getCodeSystemExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/CodeSystem/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getCodeSystemExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/CodeSystem/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # Composition (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/composition # FHIR R4 Composition resource. # Composition (FHIR R4) FHIR R4 Composition resource. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [Composition — HL7 FHIR R4](https://hl7.org/fhir/R4/composition.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R4/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R4/{Resource}/{id}`) ## Scopes Scopes are shared across FHIR releases — the same `system/Composition.*` scope grants Composition access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-composition-rs) - — see [scope reference](/v1/scopes#system-composition-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `patient` | reference | Resource reference — supply the UUID of the referenced resource. | | `type` | token | Exact match. For identifiers use `system|value` format. | | `date` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_cursor` | string | Case-insensitive partial match. | ## Endpoints ### Read Composition Retrieve a single Composition (the clinical-note projection of an encounter) by its ID. The response carries a weak ETag derived from the encounter's last-modified time. **Required scope:** #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Composition.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Composition/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Composition/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Composition/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getCompositionExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Composition/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getCompositionExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Composition/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Update Composition Update a Composition's clinical narrative and document status. Supports optimistic concurrency via `If-Match` against the weak ETag returned on read (a stale ETag yields 409 HPB-00103). Set `status: entered-in-error` to void the note. The underlying visit envelope (class, period) is preserved. **Required scope:** #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Composition.u" \ | jq -r .access_token) curl -X PUT https://api.huli.ai/fhir/R4/Composition/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @resource.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Composition/${id}`, { method: "PUT", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.put( f"https://api.huli.ai/fhir/R4/Composition/{resource_id}", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class updateCompositionExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Composition/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("PUT", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func updateCompositionExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("PUT", "https://api.huli.ai/fhir/R4/Composition/"+resourceID+"", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00103](/v1/errors#hpb-00103) | 409 | Version conflict | | [HPB-00101](/v1/errors#hpb-00101) | 422 | Unprocessable entity | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Search Compositions Search Compositions by `patient`, `date`, and `type`. Paginated with `_count` and `_cursor`. **Required scope:** #### Query Parameters #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Composition.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Composition \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Composition`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Composition", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchCompositionsExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Composition")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchCompositionsExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Composition", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Create Composition Create a Composition. Because a Composition is a projection of an encounter, this creates a new encounter carrying the clinical narrative (subject + author are required). The server assigns the resource ID. **Required scope:** #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Composition.c" \ | jq -r .access_token) curl -X POST https://api.huli.ai/fhir/R4/Composition \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @composition.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Composition`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.post( f"https://api.huli.ai/fhir/R4/Composition", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class createCompositionExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Composition")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("POST", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func createCompositionExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("POST", "https://api.huli.ai/fhir/R4/Composition", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00101](/v1/errors#hpb-00101) | 422 | Unprocessable entity | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # Device (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/device # FHIR R4 Device resource. # Device (FHIR R4) FHIR R4 Device resource. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [Device — HL7 FHIR R4](https://hl7.org/fhir/R4/device.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) Authorized by the SMART scope `system/Appointment.rs`. This resource inherits an existing scope — no per-resource `system/Device.*` scope is minted; requesting one is rejected. ## Scopes Scopes are shared across FHIR releases — the same `system/Device.*` scope grants Device access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-appointment-rs) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_offset` | number | Integer. For `_count`: default 20, max 100. | ## Endpoints ### Read Device Retrieve a single Device resource by its ID. A Device is a Huli schedulable resource of type `equipment`. This is a read-only discovery resource, gated by `system/Appointment.rs` because it supports appointment booking. **Required scope:** #### Response — 200 ```json { "resourceType": "Device", "id": "330e8400-e29b-41d4-a716-446655440120", "status": "active", "deviceName": [ { "name": "Ultrasonido Sala 2", "type": "user-friendly-name" } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Appointment.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Device/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Device/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Device/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getDeviceExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Device/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getDeviceExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Device/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Search Device Search Device resources (bookable equipment). Results are returned as a FHIR searchset Bundle. This is a read-only discovery resource, gated by `system/Appointment.rs` because it supports appointment booking. **Required scope:** #### Query Parameters #### Response — 200 ```json { "resourceType": "Bundle", "type": "searchset", "total": 1, "entry": [ { "fullUrl": "Device/330e8400-e29b-41d4-a716-446655440120", "resource": { "resourceType": "Device", "id": "330e8400-e29b-41d4-a716-446655440120", "status": "active", "deviceName": [ { "name": "Ultrasonido Sala 2", "type": "user-friendly-name" } ] }, "search": { "mode": "match" } } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Appointment.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Device \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Device`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Device", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchDeviceExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Device")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchDeviceExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Device", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # DocumentReference (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/documentreference # FHIR R4 DocumentReference resource. # DocumentReference (FHIR R4) FHIR R4 DocumentReference resource. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [DocumentReference — HL7 FHIR R4](https://hl7.org/fhir/R4/documentreference.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R4/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R4/{Resource}/{id}`) - **$upload** — operation ## Scopes Scopes are shared across FHIR releases — the same `system/DocumentReference.*` scope grants DocumentReference access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-documentreference-rs) - — see [scope reference](/v1/scopes#system-documentreference-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `patient` | reference | Resource reference — supply the UUID of the referenced resource. | | `type` | token | Exact match. For identifiers use `system|value` format. | | `category` | token | Exact match. For identifiers use `system|value` format. | | `date` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_cursor` | string | Case-insensitive partial match. | ## Endpoints ### Read DocumentReference Retrieve a single DocumentReference. The binary is referenced via a 30-minute signed URL in content[0].attachment.url (never inlined). **Required scope:** #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/DocumentReference.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/DocumentReference/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/DocumentReference/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/DocumentReference/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getDocumentReferenceExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/DocumentReference/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getDocumentReferenceExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/DocumentReference/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Update DocumentReference metadata Update mutable metadata (category, description) of a DocumentReference. The binary content is immutable on this surface. Set `status: entered-in-error` to soft-delete the document. **Required scope:** #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/DocumentReference.u" \ | jq -r .access_token) curl -X PUT https://api.huli.ai/fhir/R4/DocumentReference/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @resource.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/DocumentReference/${id}`, { method: "PUT", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.put( f"https://api.huli.ai/fhir/R4/DocumentReference/{resource_id}", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class updateDocumentReferenceExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/DocumentReference/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("PUT", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func updateDocumentReferenceExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("PUT", "https://api.huli.ai/fhir/R4/DocumentReference/"+resourceID+"", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Search DocumentReferences Search DocumentReferences for a patient (the `patient` parameter is required). Filter by `category`, `type`, and `date`. Paginated with `_count` and `_cursor`. Search results do not carry a signed URL — read the individual resource for the binary. **Required scope:** #### Query Parameters #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/DocumentReference.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/DocumentReference \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/DocumentReference`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/DocumentReference", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchDocumentReferencesExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/DocumentReference")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchDocumentReferencesExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/DocumentReference", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Create DocumentReference (inline upload) Create a DocumentReference by inlining the binary as base64 in content[0].attachment.data (with a filename in attachment.title). Equivalent to the $upload operation's inline mode. Enforces the 25 MB ceiling and magic-byte content validation (PDF/JPEG/PNG/WEBP/DICOM). **Required scope:** #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/DocumentReference.c" \ | jq -r .access_token) curl -X POST https://api.huli.ai/fhir/R4/DocumentReference \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @documentreference.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/DocumentReference`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.post( f"https://api.huli.ai/fhir/R4/DocumentReference", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class createDocumentReferenceExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/DocumentReference")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("POST", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func createDocumentReferenceExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("POST", "https://api.huli.ai/fhir/R4/DocumentReference", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Upload a document ($upload) Upload a document binary in one of two modes: (1) multipart/form-data with a `file` part plus a `subject` (Patient reference or UUID) and optional `encounter` field; or (2) a JSON DocumentReference with content[0].attachment.data base64 (identical to POST /DocumentReference). Enforces the 25 MB ceiling, magic-byte content validation, and SHA-256 dedup. **Required scope:** #### Request Body #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/DocumentReference.c" \ | jq -r .access_token) curl -X POST https://api.huli.ai/fhir/R4/DocumentReference/$upload \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @$upload.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/DocumentReference/$upload`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.post( f"https://api.huli.ai/fhir/R4/DocumentReference/$upload", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class uploadDocumentReferenceExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/DocumentReference/$upload")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("POST", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func uploadDocumentReferenceExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("POST", "https://api.huli.ai/fhir/R4/DocumentReference/$upload", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # Encounter (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/encounter # An interaction between a patient and healthcare provider for the purpose of providing healthcare service. Maps to HuliPr # Encounter (FHIR R4) An interaction between a patient and healthcare provider for the purpose of providing healthcare service. Maps to HuliPractice clinical encounters (consultations). > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [Encounter — HL7 FHIR R4](https://hl7.org/fhir/R4/encounter.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R4/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R4/{Resource}/{id}`) ## Scopes Scopes are shared across FHIR releases — the same `system/Encounter.*` scope grants Encounter access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-encounter-rs) - — see [scope reference](/v1/scopes#system-encounter-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `patient` | reference | Resource reference — supply the UUID of the referenced resource. | | `practitioner` | reference | Resource reference — supply the UUID of the referenced resource. | | `date` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `status` | token | Exact match. For identifiers use `system|value` format. | | `class` | token | Exact match. For identifiers use `system|value` format. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_cursor` | string | Case-insensitive partial match. | ## Endpoints ### Read Encounter Retrieve a single Encounter resource by its ID. **Required scope:** #### Response — 200 ```json { "resourceType": "Encounter", "id": "990e8400-e29b-41d4-a716-446655440030", "status": "finished", "class": { "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "AMB", "display": "ambulatory" }, "subject": { "reference": "Patient/550e8400-e29b-41d4-a716-446655440001", "display": "Maria Garcia" }, "participant": [ { "individual": { "reference": "Practitioner/880e8400-e29b-41d4-a716-446655440020", "display": "Dr. Rodriguez" } } ], "period": { "start": "2026-05-20T10:00:00Z", "end": "2026-05-20T10:45:00Z" } } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Encounter.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Encounter/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Encounter/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Encounter/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getEncounterExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Encounter/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getEncounterExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Encounter/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Update Encounter Update an existing Encounter resource. **Required scope:** #### Request Body #### Request Example ```json { "resourceType": "Encounter", "id": "990e8400-e29b-41d4-a716-446655440030", "status": "finished", "class": { "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "AMB", "display": "ambulatory" }, "subject": { "reference": "Patient/550e8400-e29b-41d4-a716-446655440001" }, "participant": [ { "individual": { "reference": "Practitioner/880e8400-e29b-41d4-a716-446655440020" } } ], "period": { "start": "2026-05-20T10:00:00Z", "end": "2026-05-20T10:45:00Z" } } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Encounter.cru" \ | jq -r .access_token) curl -X PUT https://api.huli.ai/fhir/R4/Encounter/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @resource.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Encounter/${id}`, { method: "PUT", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.put( f"https://api.huli.ai/fhir/R4/Encounter/{resource_id}", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class updateEncounterExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Encounter/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("PUT", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func updateEncounterExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("PUT", "https://api.huli.ai/fhir/R4/Encounter/"+resourceID+"", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00101](/v1/errors#hpb-00101) | 422 | Unprocessable entity | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Search Encounters Search for Encounter resources using FHIR search parameters. **Required scope:** #### Query Parameters #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Encounter.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Encounter \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Encounter`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Encounter", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchEncountersExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Encounter")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchEncountersExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Encounter", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Create Encounter Create a new Encounter resource. The server assigns the resource ID. **Required scope:** #### Request Body #### Request Example ```json { "resourceType": "Encounter", "status": "planned", "class": { "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "AMB", "display": "ambulatory" }, "subject": { "reference": "Patient/550e8400-e29b-41d4-a716-446655440001" }, "participant": [ { "individual": { "reference": "Practitioner/880e8400-e29b-41d4-a716-446655440020" } } ], "period": { "start": "2026-06-10T08:00:00Z" } } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Encounter.cru" \ | jq -r .access_token) curl -X POST https://api.huli.ai/fhir/R4/Encounter \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @encounter.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Encounter`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.post( f"https://api.huli.ai/fhir/R4/Encounter", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class createEncounterExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Encounter")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("POST", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func createEncounterExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("POST", "https://api.huli.ai/fhir/R4/Encounter", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00101](/v1/errors#hpb-00101) | 422 | Unprocessable entity | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # HealthcareService (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/healthcareservice # FHIR R4 HealthcareService resource. # HealthcareService (FHIR R4) FHIR R4 HealthcareService resource. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [HealthcareService — HL7 FHIR R4](https://hl7.org/fhir/R4/healthcareservice.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) Authorized by the SMART scope `system/Appointment.rs`. This resource inherits an existing scope — no per-resource `system/HealthcareService.*` scope is minted; requesting one is rejected. ## Scopes Scopes are shared across FHIR releases — the same `system/HealthcareService.*` scope grants HealthcareService access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-appointment-rs) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_offset` | number | Integer. For `_count`: default 20, max 100. | ## Endpoints ### Read HealthcareService Retrieve a single HealthcareService resource by its ID. This is a read-only discovery resource, gated by `system/Appointment.rs` because it supports appointment booking. **Required scope:** #### Response — 200 ```json { "resourceType": "HealthcareService", "id": "ee0e8400-e29b-41d4-a716-446655440080", "active": true, "name": "Consulta General", "type": [ { "coding": [ { "system": "http://snomed.info/sct", "code": "11429006", "display": "Consultation" } ] } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Appointment.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/HealthcareService/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/HealthcareService/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/HealthcareService/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getHealthcareServiceExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/HealthcareService/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getHealthcareServiceExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/HealthcareService/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Search HealthcareService Search HealthcareService resources. Results are returned as a FHIR searchset Bundle. This is a read-only discovery resource, gated by `system/Appointment.rs` because it supports appointment booking. **Required scope:** #### Query Parameters #### Response — 200 ```json { "resourceType": "Bundle", "type": "searchset", "total": 1, "entry": [ { "fullUrl": "HealthcareService/ee0e8400-e29b-41d4-a716-446655440080", "resource": { "resourceType": "HealthcareService", "id": "ee0e8400-e29b-41d4-a716-446655440080", "active": true, "name": "Consulta General" }, "search": { "mode": "match" } } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Appointment.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/HealthcareService \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/HealthcareService`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/HealthcareService", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchHealthcareServiceExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/HealthcareService")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchHealthcareServiceExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/HealthcareService", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # Location (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/location # FHIR R4 Location resource. # Location (FHIR R4) FHIR R4 Location resource. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [Location — HL7 FHIR R4](https://hl7.org/fhir/R4/location.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) Authorized by the SMART scope `system/Appointment.rs`. This resource inherits an existing scope — no per-resource `system/Location.*` scope is minted; requesting one is rejected. ## Scopes Scopes are shared across FHIR releases — the same `system/Location.*` scope grants Location access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-appointment-rs) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_offset` | number | Integer. For `_count`: default 20, max 100. | ## Endpoints ### Read Location Retrieve a single Location resource by its ID. This is a read-only discovery resource. Gated by `system/Appointment.rs` because it supports appointment booking. **Required scope:** #### Response — 200 ```json { "resourceType": "Location", "id": "dd0e8400-e29b-41d4-a716-446655440070", "status": "active", "name": "Consultorio 1", "address": { "line": [ "Av. Insurgentes Sur 1000" ], "city": "Ciudad de Mexico", "country": "MX" } } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Appointment.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Location/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Location/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Location/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getLocationExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Location/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getLocationExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Location/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Search Location Search Location resources. Results are returned as a FHIR searchset Bundle. This is a read-only discovery resource, gated by `system/Appointment.rs` because it supports appointment booking. **Required scope:** #### Query Parameters #### Response — 200 ```json { "resourceType": "Bundle", "type": "searchset", "total": 1, "entry": [ { "fullUrl": "Location/dd0e8400-e29b-41d4-a716-446655440070", "resource": { "resourceType": "Location", "id": "dd0e8400-e29b-41d4-a716-446655440070", "status": "active", "name": "Consultorio 1" }, "search": { "mode": "match" } } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Appointment.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Location \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Location`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Location", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchLocationExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Location")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchLocationExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Location", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # MedicationRequest (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/medicationrequest # FHIR R4 MedicationRequest resource. # MedicationRequest (FHIR R4) FHIR R4 MedicationRequest resource. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [MedicationRequest — HL7 FHIR R4](https://hl7.org/fhir/R4/medicationrequest.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R4/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R4/{Resource}/{id}`) ## Scopes Scopes are shared across FHIR releases — the same `system/MedicationRequest.*` scope grants MedicationRequest access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-medicationrequest-rs) - — see [scope reference](/v1/scopes#system-medicationrequest-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `patient` | reference | Resource reference — supply the UUID of the referenced resource. | | `encounter` | reference | Resource reference — supply the UUID of the referenced resource. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_cursor` | string | Case-insensitive partial match. | ## Endpoints ### Read MedicationRequest Retrieve a single MedicationRequest resource by its ID. **Required scope:** #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/MedicationRequest.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/MedicationRequest/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/MedicationRequest/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/MedicationRequest/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getMedicationRequestExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/MedicationRequest/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getMedicationRequestExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/MedicationRequest/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Update MedicationRequest Update an existing MedicationRequest. The update is a read-then-merge: the FHIR-owned fields (status, medication code/system/display, dosage, dispense, reason, notes) overlay the stored row, while app-only fields not represented in FHIR (prescription warnings, validity, sort order, and the draft-prescription grouping) are preserved. A MedicationRequest whose parent prescription is already signed or cancelled cannot be modified (`409 Conflict`). The `subject` is immutable — a body subject that differs from the stored patient is rejected (`422`). Supports optimistic concurrency: supply the `If-Match` header with the `ETag` from the last read to make the update conditional. A stale validator is rejected with a `409` version conflict — the same HPB-00103 shape as the signed-prescription conflict; the `VersionConflict` response documents the version-conflict cause. The response carries `ETag` + `Last-Modified` for the new version. **Required scope:** #### Request Body #### Request Example ```json { "resourceType": "MedicationRequest", "status": "active", "intent": "order", "medicationCodeableConcept": { "text": "Amoxicillin 500mg" }, "subject": { "reference": "Patient/550e8400-e29b-41d4-a716-446655440001" }, "dosageInstruction": [ { "text": "1 tablet every 12 hours" } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/MedicationRequest.cru" \ | jq -r .access_token) curl -X PUT https://api.huli.ai/fhir/R4/MedicationRequest/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @resource.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/MedicationRequest/${id}`, { method: "PUT", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.put( f"https://api.huli.ai/fhir/R4/MedicationRequest/{resource_id}", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class updateMedicationRequestExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/MedicationRequest/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("PUT", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func updateMedicationRequestExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("PUT", "https://api.huli.ai/fhir/R4/MedicationRequest/"+resourceID+"", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00103](/v1/errors#hpb-00103) | 409 | Version conflict | | [HPB-00101](/v1/errors#hpb-00101) | 422 | Unprocessable entity | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Search MedicationRequests Search for MedicationRequest resources. Requires a `patient` or `encounter` parameter. Paginated with `_count` and a numeric offset `_cursor`. **Required scope:** #### Query Parameters #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/MedicationRequest.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/MedicationRequest \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/MedicationRequest`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/MedicationRequest", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchMedicationRequestsExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/MedicationRequest")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchMedicationRequestsExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/MedicationRequest", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Create MedicationRequest Create a new MedicationRequest. The server assigns the resource ID and auto-links the medication to a draft prescription. **Required scope:** #### Request Body #### Request Example ```json { "resourceType": "MedicationRequest", "status": "active", "intent": "order", "medicationCodeableConcept": { "text": "Amoxicillin 500mg" }, "subject": { "reference": "Patient/550e8400-e29b-41d4-a716-446655440001" }, "dosageInstruction": [ { "text": "1 tablet every 8 hours" } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/MedicationRequest.c" \ | jq -r .access_token) curl -X POST https://api.huli.ai/fhir/R4/MedicationRequest \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @medicationrequest.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/MedicationRequest`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.post( f"https://api.huli.ai/fhir/R4/MedicationRequest", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class createMedicationRequestExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/MedicationRequest")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("POST", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func createMedicationRequestExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("POST", "https://api.huli.ai/fhir/R4/MedicationRequest", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00101](/v1/errors#hpb-00101) | 422 | Unprocessable entity | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # Observation (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/observation # Measurements and simple assertions made about a patient, device, or other subject. Used for vital signs (heart rate, tem # Observation (FHIR R4) Measurements and simple assertions made about a patient, device, or other subject. Used for vital signs (heart rate, temperature, blood pressure), laboratory results, and clinical exam findings. LOINC codes are validated on write. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [Observation — HL7 FHIR R4](https://hl7.org/fhir/R4/observation.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R4/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R4/{Resource}/{id}`) ## Scopes Scopes are shared across FHIR releases — the same `system/Observation.*` scope grants Observation access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-observation-rs) - — see [scope reference](/v1/scopes#system-observation-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `patient` | reference | Resource reference — supply the UUID of the referenced resource. | | `encounter` | reference | Resource reference — supply the UUID of the referenced resource. | | `code` | token | Exact match. For identifiers use `system|value` format. | | `date` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `status` | token | Exact match. For identifiers use `system|value` format. | | `category` | token | Exact match. For identifiers use `system|value` format. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_cursor` | string | Case-insensitive partial match. | ## Endpoints ### Read Observation Retrieve a single Observation resource by its ID. **Required scope:** #### Response — 200 ```json { "resourceType": "Observation", "id": "aa0e8400-e29b-41d4-a716-446655440040", "status": "final", "category": [ { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "vital-signs", "display": "Vital Signs" } ] } ], "code": { "coding": [ { "system": "http://loinc.org", "code": "8867-4", "display": "Heart rate" } ] }, "subject": { "reference": "Patient/550e8400-e29b-41d4-a716-446655440001" }, "encounter": { "reference": "Encounter/990e8400-e29b-41d4-a716-446655440030" }, "effectiveDateTime": "2026-05-20T10:15:00Z", "valueQuantity": { "value": 72, "unit": "/min", "system": "http://unitsofmeasure.org", "code": "/min" } } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Observation.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Observation/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Observation/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Observation/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getObservationExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Observation/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getObservationExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Observation/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Update Observation Update an existing Observation resource. **Required scope:** #### Request Body #### Request Example ```json { "resourceType": "Observation", "id": "aa0e8400-e29b-41d4-a716-446655440040", "status": "amended", "category": [ { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "vital-signs", "display": "Vital Signs" } ] } ], "code": { "coding": [ { "system": "http://loinc.org", "code": "8867-4", "display": "Heart rate" } ] }, "subject": { "reference": "Patient/550e8400-e29b-41d4-a716-446655440001" }, "encounter": { "reference": "Encounter/990e8400-e29b-41d4-a716-446655440030" }, "effectiveDateTime": "2026-05-20T10:15:00Z", "valueQuantity": { "value": 74, "unit": "/min", "system": "http://unitsofmeasure.org", "code": "/min" } } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Observation.cru" \ | jq -r .access_token) curl -X PUT https://api.huli.ai/fhir/R4/Observation/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @resource.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Observation/${id}`, { method: "PUT", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.put( f"https://api.huli.ai/fhir/R4/Observation/{resource_id}", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class updateObservationExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Observation/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("PUT", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func updateObservationExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("PUT", "https://api.huli.ai/fhir/R4/Observation/"+resourceID+"", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00101](/v1/errors#hpb-00101) | 422 | Unprocessable entity | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Search Observations Search for Observation resources using FHIR search parameters. **Required scope:** #### Query Parameters #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Observation.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Observation \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Observation`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Observation", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchObservationsExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Observation")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchObservationsExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Observation", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Create Observation Create a new Observation resource. The server assigns the resource ID. LOINC codes are validated. **Required scope:** #### Request Body #### Request Example ```json { "resourceType": "Observation", "status": "final", "category": [ { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "vital-signs", "display": "Vital Signs" } ] } ], "code": { "coding": [ { "system": "http://loinc.org", "code": "8310-5", "display": "Body temperature" } ] }, "subject": { "reference": "Patient/550e8400-e29b-41d4-a716-446655440001" }, "encounter": { "reference": "Encounter/990e8400-e29b-41d4-a716-446655440030" }, "effectiveDateTime": "2026-05-20T10:15:00Z", "valueQuantity": { "value": 36.6, "unit": "Cel", "system": "http://unitsofmeasure.org", "code": "Cel" } } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Observation.cru" \ | jq -r .access_token) curl -X POST https://api.huli.ai/fhir/R4/Observation \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @observation.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Observation`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.post( f"https://api.huli.ai/fhir/R4/Observation", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class createObservationExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Observation")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("POST", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func createObservationExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("POST", "https://api.huli.ai/fhir/R4/Observation", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00101](/v1/errors#hpb-00101) | 422 | Unprocessable entity | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # Organization (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/organization # A formally or informally recognised grouping of people or organisations formed for healthcare purposes. Read-only in the # Organization (FHIR R4) A formally or informally recognised grouping of people or organisations formed for healthcare purposes. Read-only in the public API — represents the healthcare organisation that owns the data. All resources are scoped to a single organisation per API key. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [Organization — HL7 FHIR R4](https://hl7.org/fhir/R4/organization.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) ## Scopes Scopes are shared across FHIR releases — the same `system/Organization.*` scope grants Organization access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-organization-rs) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | ## Endpoints ### Search Organization Search Organization resources. A SMART client sees only its own authenticated organization, so the searchset Bundle carries at most that one organization. An `_id` that does not match the caller's organization yields an empty Bundle. **Required scope:** #### Query Parameters #### Response — 200 ```json { "resourceType": "Bundle", "type": "searchset", "total": 1, "entry": [ { "fullUrl": "Organization/bb0e8400-e29b-41d4-a716-446655440050", "resource": { "resourceType": "Organization", "id": "bb0e8400-e29b-41d4-a716-446655440050", "active": true, "name": "Centro Medico Huli" }, "search": { "mode": "match" } } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Organization.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Organization \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Organization`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Organization", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchOrganizationExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Organization")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchOrganizationExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Organization", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Read Organization Retrieve a single Organization resource by its ID. This is a read-only resource. **Required scope:** #### Response — 200 ```json { "resourceType": "Organization", "id": "bb0e8400-e29b-41d4-a716-446655440050", "active": true, "name": "Centro Medico Huli", "type": [ { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/organization-type", "code": "prov", "display": "Healthcare Provider" } ] } ], "telecom": [ { "system": "phone", "value": "+52-555-0300" } ], "address": [ { "line": [ "Av. Insurgentes Sur 1000" ], "city": "Ciudad de Mexico", "state": "CDMX", "postalCode": "03100", "country": "MX" } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Organization.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Organization/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Organization/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Organization/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getOrganizationExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Organization/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getOrganizationExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Organization/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # Patient (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/patient # Demographic and administrative information about an individual receiving healthcare services. Maps to HuliPractice patie # Patient (FHIR R4) Demographic and administrative information about an individual receiving healthcare services. Maps to HuliPractice patient records including name, birth date, gender, contact information, and identifiers such as CURP. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [Patient — HL7 FHIR R4](https://hl7.org/fhir/R4/patient.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R4/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R4/{Resource}/{id}`) - **$everything** — operation ## Scopes Scopes are shared across FHIR releases — the same `system/Patient.*` scope grants Patient access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-patient-rs) - — see [scope reference](/v1/scopes#system-patient-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `identifier` | token | Exact match. For identifiers use `system|value` format. | | `name` | string | Case-insensitive partial match. | | `gender` | token | Exact match. For identifiers use `system|value` format. | | `birthdate` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `active` | token | Exact match. For identifiers use `system|value` format. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_cursor` | string | Case-insensitive partial match. | ## Endpoints ### Read Patient Retrieve a single Patient resource by its ID. **Required scope:** #### Response — 200 ```json { "resourceType": "Patient", "id": "550e8400-e29b-41d4-a716-446655440001", "meta": { "lastUpdated": "2026-01-15T10:30:00Z" }, "active": true, "name": [ { "use": "official", "family": "Garcia", "given": [ "Maria" ] } ], "gender": "female", "birthDate": "1990-03-15", "telecom": [ { "system": "phone", "value": "+52-555-0100", "use": "mobile" }, { "system": "email", "value": "maria.garcia@example.com" } ], "address": [ { "use": "home", "line": [ "Av. Reforma 222" ], "city": "Ciudad de Mexico", "state": "CDMX", "postalCode": "06600", "country": "MX" } ], "identifier": [ { "system": "https://huli.ai/identifiers/curp", "value": "GARM900315MDFRRL09" } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Patient.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Patient/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Patient/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Patient/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getPatientExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Patient/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getPatientExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Patient/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Update Patient Update an existing Patient resource. The request body must include `resourceType: "Patient"`. Deceased patients cannot be updated (returns 422). **Required scope:** #### Request Body #### Request Example ```json { "resourceType": "Patient", "id": "550e8400-e29b-41d4-a716-446655440001", "active": true, "name": [ { "use": "official", "family": "Garcia Lopez", "given": [ "Maria" ] } ], "gender": "female", "birthDate": "1990-03-15", "telecom": [ { "system": "phone", "value": "+52-555-0101", "use": "mobile" } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Patient.cru" \ | jq -r .access_token) curl -X PUT https://api.huli.ai/fhir/R4/Patient/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @resource.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Patient/${id}`, { method: "PUT", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.put( f"https://api.huli.ai/fhir/R4/Patient/{resource_id}", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class updatePatientExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Patient/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("PUT", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func updatePatientExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("PUT", "https://api.huli.ai/fhir/R4/Patient/"+resourceID+"", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00101](/v1/errors#hpb-00101) | 422 | Unprocessable entity | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Patient $everything Aggregate a patient's clinical record across Patient, Encounter, Observation, Composition, DocumentReference, MedicationRequest, and ServiceRequest into a single searchset Bundle. Each additional resource type is included only when the access token's SMART scopes grant read on it; withheld types are listed in an information OperationOutcome and signalled via a scope-filtered `meta.tag`. Optional `start`/`end` bound the date range; `_count` caps each per-type page; `_type` restricts the included resource types. Baseline scope: system/Patient.r. **Required scope:** #### Query Parameters #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Patient.r" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Patient/${RESOURCE_ID}/$everything \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Patient/${id}/$everything`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Patient/{resource_id}/$everything", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class patientEverythingExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Patient/" + resourceId + "/$everything")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func patientEverythingExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Patient/"+resourceID+"/$everything", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Search Patients Search for Patient resources using FHIR search parameters. Results are returned as a FHIR Bundle with cursor-based pagination. **Required scope:** #### Query Parameters #### Response — 200 ```json { "resourceType": "Bundle", "type": "searchset", "total": 2, "link": [ { "relation": "self", "url": "https://api.huli.ai/fhir/R4/Patient?name=Garcia&_count=20" }, { "relation": "next", "url": "https://api.huli.ai/fhir/R4/Patient?name=Garcia&_count=20&_cursor=eyJ0IjoiMjAyNi0wMS0xNVQxMDozMDowMFoiLCJpIjoiNTUwZTg0MDAtZTI5Yi00MWQ0LWE3MTYtNDQ2NjU1NDQwMDAxIn0" } ], "entry": [ { "fullUrl": "Patient/550e8400-e29b-41d4-a716-446655440001", "resource": { "resourceType": "Patient", "id": "550e8400-e29b-41d4-a716-446655440001", "active": true, "name": [ { "use": "official", "family": "Garcia", "given": [ "Maria" ] } ], "gender": "female", "birthDate": "1990-03-15" }, "search": { "mode": "match" } } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Patient.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Patient \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Patient`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Patient", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchPatientsExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Patient")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchPatientsExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Patient", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Create Patient Create a new Patient resource. The server assigns the resource ID; do not include `id` in the request body. **Required scope:** #### Request Body #### Request Example ```json { "resourceType": "Patient", "active": true, "name": [ { "use": "official", "family": "Hernandez", "given": [ "Carlos" ] } ], "gender": "male", "birthDate": "1985-07-20", "telecom": [ { "system": "phone", "value": "+52-555-0200", "use": "mobile" }, { "system": "email", "value": "carlos.hernandez@example.com" } ], "address": [ { "use": "home", "line": [ "Calle Madero 50" ], "city": "Guadalajara", "state": "Jalisco", "postalCode": "44100", "country": "MX" } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Patient.cru" \ | jq -r .access_token) curl -X POST https://api.huli.ai/fhir/R4/Patient \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @patient.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Patient`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.post( f"https://api.huli.ai/fhir/R4/Patient", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class createPatientExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Patient")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("POST", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func createPatientExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("POST", "https://api.huli.ai/fhir/R4/Patient", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00101](/v1/errors#hpb-00101) | 422 | Unprocessable entity | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # Practitioner (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/practitioner # A person who is directly or indirectly involved in providing healthcare. Read-only in the public API — practitioner reco # Practitioner (FHIR R4) A person who is directly or indirectly involved in providing healthcare. Read-only in the public API — practitioner records are managed through HuliPractice. Referenced by Appointment and Encounter resources. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [Practitioner — HL7 FHIR R4](https://hl7.org/fhir/R4/practitioner.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) ## Scopes Scopes are shared across FHIR releases — the same `system/Practitioner.*` scope grants Practitioner access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-practitioner-rs) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `name` | string | Case-insensitive partial match. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_offset` | number | Integer. For `_count`: default 20, max 100. | ## Endpoints ### Read Practitioner Retrieve a single Practitioner resource by its ID. This is a read-only resource. **Required scope:** #### Response — 200 ```json { "resourceType": "Practitioner", "id": "880e8400-e29b-41d4-a716-446655440020", "active": true, "name": [ { "use": "official", "family": "Rodriguez", "given": [ "Elena" ], "prefix": [ "Dr." ] } ], "telecom": [ { "system": "email", "value": "elena.rodriguez@clinic.example.com" } ], "qualification": [ { "code": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/v2-0360", "code": "MD", "display": "Doctor of Medicine" } ] } } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Practitioner.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Practitioner/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Practitioner/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Practitioner/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getPractitionerExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Practitioner/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getPractitionerExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Practitioner/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Search Practitioner Search Practitioner resources. Results are returned as a FHIR searchset Bundle. This is a read-only discovery resource, gated by `system/Practitioner.rs`. **Required scope:** #### Query Parameters #### Response — 200 ```json { "resourceType": "Bundle", "type": "searchset", "total": 1, "entry": [ { "fullUrl": "Practitioner/880e8400-e29b-41d4-a716-446655440020", "resource": { "resourceType": "Practitioner", "id": "880e8400-e29b-41d4-a716-446655440020", "active": true, "name": [ { "use": "official", "text": "Dra. Elena Rodriguez" } ] }, "search": { "mode": "match" } } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Practitioner.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Practitioner \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Practitioner`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Practitioner", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchPractitionerExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Practitioner")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchPractitionerExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Practitioner", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # PractitionerRole (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/practitionerrole # FHIR R4 PractitionerRole resource. # PractitionerRole (FHIR R4) FHIR R4 PractitionerRole resource. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [PractitionerRole — HL7 FHIR R4](https://hl7.org/fhir/R4/practitionerrole.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) Authorized by the SMART scope `system/Practitioner.rs`. This resource inherits an existing scope — no per-resource `system/PractitionerRole.*` scope is minted; requesting one is rejected. ## Scopes Scopes are shared across FHIR releases — the same `system/PractitionerRole.*` scope grants PractitionerRole access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-practitioner-rs) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `practitioner` | reference | Resource reference — supply the UUID of the referenced resource. | | `location` | reference | Resource reference — supply the UUID of the referenced resource. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_offset` | number | Integer. For `_count`: default 20, max 100. | ## Endpoints ### Read PractitionerRole Retrieve a single PractitionerRole resource by its ID. This is a read-only discovery resource, gated by `system/Practitioner.rs`. **Required scope:** #### Response — 200 ```json { "resourceType": "PractitionerRole", "id": "ff0e8400-e29b-41d4-a716-446655440090", "active": true, "practitioner": { "reference": "Practitioner/880e8400-e29b-41d4-a716-446655440020" }, "location": [ { "reference": "Location/dd0e8400-e29b-41d4-a716-446655440070" } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Practitioner.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/PractitionerRole/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/PractitionerRole/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/PractitionerRole/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getPractitionerRoleExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/PractitionerRole/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getPractitionerRoleExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/PractitionerRole/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Search PractitionerRole Search PractitionerRole resources. Results are returned as a FHIR searchset Bundle. This is a read-only discovery resource, gated by `system/Practitioner.rs`. **Required scope:** #### Query Parameters #### Response — 200 ```json { "resourceType": "Bundle", "type": "searchset", "total": 1, "entry": [ { "fullUrl": "PractitionerRole/ff0e8400-e29b-41d4-a716-446655440090", "resource": { "resourceType": "PractitionerRole", "id": "ff0e8400-e29b-41d4-a716-446655440090", "active": true, "practitioner": { "reference": "Practitioner/880e8400-e29b-41d4-a716-446655440020" } }, "search": { "mode": "match" } } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Practitioner.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/PractitionerRole \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/PractitionerRole`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/PractitionerRole", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchPractitionerRoleExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/PractitionerRole")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchPractitionerRoleExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/PractitionerRole", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # Schedule (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/schedule # FHIR R4 Schedule resource. # Schedule (FHIR R4) FHIR R4 Schedule resource. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [Schedule — HL7 FHIR R4](https://hl7.org/fhir/R4/schedule.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) Authorized by the SMART scope `system/Appointment.rs`. This resource inherits an existing scope — no per-resource `system/Schedule.*` scope is minted; requesting one is rejected. ## Scopes Scopes are shared across FHIR releases — the same `system/Schedule.*` scope grants Schedule access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-appointment-rs) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `actor` | reference | Resource reference — supply the UUID of the referenced resource. | | `service-type` | token | Exact match. For identifiers use `system|value` format. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_offset` | number | Integer. For `_count`: default 20, max 100. | ## Endpoints ### Read Schedule Retrieve a single Schedule resource by its ID. This is a read-only discovery resource, gated by `system/Appointment.rs` because it supports appointment booking. **Required scope:** #### Response — 200 ```json { "resourceType": "Schedule", "id": "110e8400-e29b-41d4-a716-446655440100", "active": true, "actor": [ { "reference": "PractitionerRole/ff0e8400-e29b-41d4-a716-446655440090" } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Appointment.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Schedule/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Schedule/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Schedule/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getScheduleExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Schedule/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getScheduleExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Schedule/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Search Schedule Search Schedule resources. Results are returned as a FHIR searchset Bundle. This is a read-only discovery resource, gated by `system/Appointment.rs` because it supports appointment booking. Exactly one of `_id`, `actor`, or `service-type` is required; supplying more than one is rejected. **Required scope:** #### Query Parameters #### Response — 200 ```json { "resourceType": "Bundle", "type": "searchset", "total": 1, "entry": [ { "fullUrl": "Schedule/110e8400-e29b-41d4-a716-446655440100", "resource": { "resourceType": "Schedule", "id": "110e8400-e29b-41d4-a716-446655440100", "active": true, "actor": [ { "reference": "PractitionerRole/ff0e8400-e29b-41d4-a716-446655440090" } ] }, "search": { "mode": "match" } } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Appointment.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Schedule \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Schedule`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Schedule", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchScheduleExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Schedule")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchScheduleExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Schedule", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # ServiceRequest (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/servicerequest # FHIR R4 ServiceRequest resource. # ServiceRequest (FHIR R4) FHIR R4 ServiceRequest resource. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [ServiceRequest — HL7 FHIR R4](https://hl7.org/fhir/R4/servicerequest.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R4/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R4/{Resource}/{id}`) ## Scopes Scopes are shared across FHIR releases — the same `system/ServiceRequest.*` scope grants ServiceRequest access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-servicerequest-rs) - — see [scope reference](/v1/scopes#system-servicerequest-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `patient` | reference | Resource reference — supply the UUID of the referenced resource. | | `encounter` | reference | Resource reference — supply the UUID of the referenced resource. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_cursor` | string | Case-insensitive partial match. | ## Endpoints ### Read ServiceRequest Retrieve a single ServiceRequest resource by its ID. **Required scope:** #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/ServiceRequest.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/ServiceRequest/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/ServiceRequest/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/ServiceRequest/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getServiceRequestExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/ServiceRequest/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getServiceRequestExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/ServiceRequest/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Update ServiceRequest Update an existing ServiceRequest as a full-resource replace with read-then-merge semantics: the FHIR-owned fields (priority, `code` + `category` — the order's primary study — `reasonCode`, `note`) overlay the stored draft order, while app-only fields not represented in FHIR (the requisition grouping, the requesting practitioner, and per-study clinical order data such as specimen requirements or body site) are preserved. `reasonCode` and `note` follow faithful replace semantics (absent means removed); a body that omits `priority` preserves the stored value — priority is a clinical triage decision, never silently reset to `routine`. The update is draft-gated: an order whose backing clinical document is already signed or cancelled cannot be modified (`409`, HPB-00135). An order holding more than one study is read-only on this surface — a replace would drop clinician-authored studies (`409`, HPB-00136), and a body carrying `orderDetail` is rejected as on create. `status` must be `active` (lifecycle transitions are not exposed here); `subject` and `encounter` are immutable (`422`). Supports optimistic concurrency: supply the `If-Match` header with the `ETag` from the last read to make the update conditional. A stale validator is rejected with a `409` version conflict (HPB-00103), re-checked against the locked row inside the write transaction. The response carries the `ETag` of the new version. **Required scope:** #### Request Body #### Request Example ```json { "resourceType": "ServiceRequest", "status": "active", "intent": "order", "priority": "urgent", "category": [ { "coding": [ { "code": "laboratory" } ] } ], "code": { "text": "Urinalysis (EGO)" }, "subject": { "reference": "Patient/550e8400-e29b-41d4-a716-446655440001" } } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/ServiceRequest.cru" \ | jq -r .access_token) curl -X PUT https://api.huli.ai/fhir/R4/ServiceRequest/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @resource.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/ServiceRequest/${id}`, { method: "PUT", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.put( f"https://api.huli.ai/fhir/R4/ServiceRequest/{resource_id}", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class updateServiceRequestExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/ServiceRequest/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("PUT", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func updateServiceRequestExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("PUT", "https://api.huli.ai/fhir/R4/ServiceRequest/"+resourceID+"", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00103](/v1/errors#hpb-00103) | 409 | Version conflict | | [HPB-00101](/v1/errors#hpb-00101) | 422 | Unprocessable entity | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Search ServiceRequests Search for ServiceRequest resources. Requires a `patient` or `encounter` parameter. Paginated with `_count` and a numeric offset `_cursor`. **Required scope:** #### Query Parameters #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/ServiceRequest.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/ServiceRequest \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/ServiceRequest`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/ServiceRequest", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchServiceRequestsExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/ServiceRequest")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchServiceRequestsExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/ServiceRequest", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Create ServiceRequest Create a new ServiceRequest (a single-item service order). The server assigns the resource ID and stamps the requesting practitioner from the authenticated session. **Required scope:** #### Request Body #### Request Example ```json { "resourceType": "ServiceRequest", "status": "active", "intent": "order", "priority": "routine", "category": [ { "coding": [ { "code": "laboratory" } ] } ], "code": { "text": "Complete blood count (CBC)" }, "subject": { "reference": "Patient/550e8400-e29b-41d4-a716-446655440001" } } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/ServiceRequest.c" \ | jq -r .access_token) curl -X POST https://api.huli.ai/fhir/R4/ServiceRequest \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @servicerequest.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/ServiceRequest`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.post( f"https://api.huli.ai/fhir/R4/ServiceRequest", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class createServiceRequestExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/ServiceRequest")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("POST", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func createServiceRequestExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("POST", "https://api.huli.ai/fhir/R4/ServiceRequest", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00101](/v1/errors#hpb-00101) | 422 | Unprocessable entity | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # Slot (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/slot # FHIR R4 Slot resource. # Slot (FHIR R4) FHIR R4 Slot resource. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [Slot — HL7 FHIR R4](https://hl7.org/fhir/R4/slot.html) ## Supported Interactions - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) Authorized by the SMART scope `system/Appointment.rs`. This resource inherits an existing scope — no per-resource `system/Slot.*` scope is minted; requesting one is rejected. ## Scopes Scopes are shared across FHIR releases — the same `system/Slot.*` scope grants Slot access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-appointment-rs) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `schedule` | reference | Resource reference — supply the UUID of the referenced resource. | | `actor` | reference | Resource reference — supply the UUID of the referenced resource. | | `start` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `end` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_offset` | number | Integer. For `_count`: default 20, max 100. | ## Endpoints ### Search Slot Search Slot resources (available appointment slots). Results are returned as a FHIR searchset Bundle. This is a read-only discovery resource, gated by `system/Appointment.rs` because it supports appointment booking. Either `schedule` OR `actor` is required. The `start`/`end` window is expressed as date / RFC 3339 timestamps and is capped at a 31-day span. **Required scope:** #### Query Parameters #### Response — 200 ```json { "resourceType": "Bundle", "type": "searchset", "total": 1, "entry": [ { "fullUrl": "Slot/220e8400-e29b-41d4-a716-446655440110", "resource": { "resourceType": "Slot", "id": "220e8400-e29b-41d4-a716-446655440110", "schedule": { "reference": "Schedule/110e8400-e29b-41d4-a716-446655440100" }, "status": "free", "start": "2026-06-15T10:00:00Z", "end": "2026-06-15T10:30:00Z" }, "search": { "mode": "match" } } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Appointment.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Slot \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Slot`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Slot", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchSlotExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Slot")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchSlotExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Slot", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # Subscription (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/subscription # FHIR R4 Subscription resource. # Subscription (FHIR R4) FHIR R4 Subscription resource. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [Subscription — HL7 FHIR R4](https://hl7.org/fhir/R4/subscription.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R4/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R4/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R4/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R4/{Resource}/{id}`) - **delete** — delete - **$replay** — operation - **$stats** — operation - **$deliveries** — operation ## Scopes Scopes are shared across FHIR releases — the same `system/Subscription.*` scope grants Subscription access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-subscription-rs) - — see [scope reference](/v1/scopes#system-subscription-crud) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `status` | token | Exact match. For identifiers use `system|value` format. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_offset` | number | Integer. For `_count`: default 20, max 100. | ## Endpoints ### Read Subscription Retrieve a single outbound-webhook Subscription by its ID. The response NEVER includes the signing secret — the secret is returned exactly once, on create. **Required scope:** #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Subscription.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Subscription/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Subscription/${id}`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Subscription/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getSubscriptionExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Subscription/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getSubscriptionExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Subscription/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Update Subscription Update an existing outbound-webhook Subscription. The mutable fields are `status`, `criteria`, and the `channel` target/payload/headers. The signing secret is PRESERVED (never rotated or returned here). The endpoint is re-validated (https-only, SSRF-safe) on every update. `channel.type` must remain `rest-hook` and `channel.payload` must remain `application/fhir+json`. **Required scope:** #### Request Body #### Request Example ```json { "resourceType": "Subscription", "status": "active", "reason": "Notify our EHR mirror of encounter changes", "criteria": "Encounter", "channel": { "type": "rest-hook", "endpoint": "https://example.org/webhooks/huli", "payload": "application/fhir+json", "header": [ "X-Source-System: acme-ehr" ] } } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Subscription.crud" \ | jq -r .access_token) curl -X PUT https://api.huli.ai/fhir/R4/Subscription/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @resource.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Subscription/${id}`, { method: "PUT", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.put( f"https://api.huli.ai/fhir/R4/Subscription/{resource_id}", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class updateSubscriptionExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Subscription/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("PUT", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func updateSubscriptionExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("PUT", "https://api.huli.ai/fhir/R4/Subscription/"+resourceID+"", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Delete Subscription Soft-delete an outbound-webhook Subscription. The subscription is deactivated (no further deliveries fan out to it). Returns `204` on success. **Required scope:** #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Subscription.crud" \ | jq -r .access_token) curl -X DELETE https://api.huli.ai/fhir/R4/Subscription/${RESOURCE_ID} \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Subscription/${id}`, { method: "DELETE", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.delete( f"https://api.huli.ai/fhir/R4/Subscription/{resource_id}", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class deleteSubscriptionExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Subscription/" + resourceId)) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("DELETE", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func deleteSubscriptionExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("DELETE", "https://api.huli.ai/fhir/R4/Subscription/"+resourceID+"", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Replay Subscription events Re-scan the organization's event history for this subscription's criteria (resource type) over a time window and re-enqueue each matching event — for outage recovery / backfill. NOTE: the scan is by criteria + window, so it includes events that predate the subscription and events already delivered; dedupe on `X-Huli-Event-Id`. An event that already has an in-flight replay for this subscription is skipped, so repeated calls over the same window don't duplicate. Each newly matched event creates a fresh delivery carrying the SAME `X-Huli-Event-Id` as the original (so receivers dedup by event) plus `X-Huli-Replay: true`. The window is clamped to the 30-day retention horizon (`from` floored at `now-30d`, `to` capped at `now`) and a single call re-enqueues at most 500 events (for a larger backlog, call again with the same window — in-flight replays are skipped, so each call advances to older events — until `truncated` is false). Only an **active** subscription may replay. The `from`/`to` window is supplied as a FHIR `Parameters` body (or as `from`/`to` RFC3339 query params); `from` is required, `to` defaults to now. **Required scope:** #### Request Example ```json { "resourceType": "Parameters", "parameter": [ { "name": "from", "valueInstant": "2026-06-01T00:00:00Z" }, { "name": "to", "valueInstant": "2026-06-02T00:00:00Z" } ] } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Subscription.crud" \ | jq -r .access_token) curl -X POST https://api.huli.ai/fhir/R4/Subscription/${RESOURCE_ID}/$replay \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @$replay.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Subscription/${id}/$replay`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.post( f"https://api.huli.ai/fhir/R4/Subscription/{resource_id}/$replay", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class replaySubscriptionExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Subscription/" + resourceId + "/$replay")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("POST", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func replaySubscriptionExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("POST", "https://api.huli.ai/fhir/R4/Subscription/"+resourceID+"/$replay", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00101](/v1/errors#hpb-00101) | 422 | Unprocessable entity | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Subscription delivery metrics Per-subscription delivery observability. Returns a FHIR `Parameters` resource with the delivered / failed / pending / dead counts, the dead-letter depth, total attempts, the terminal success rate, and the enqueue-to-delivery latency p50/p95 (seconds). Never exposes payloads, endpoints, or signing secrets. **Required scope:** #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Subscription.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Subscription/${RESOURCE_ID}/$stats \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Subscription/${id}/$stats`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Subscription/{resource_id}/$stats", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getSubscriptionStatsExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Subscription/" + resourceId + "/$stats")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getSubscriptionStatsExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Subscription/"+resourceID+"/$stats", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Subscription delivery-attempt trail The delivery-level audit trail for one subscription: the recent delivery attempts (newest first, paged via `_count`, optionally filtered by `status` = pending | delivered | failed | dead), returned as a FHIR `Parameters` resource with one `delivery` group per attempt. Each group carries only sanitized metadata — delivery status, attempt count, last HTTP status code, queued/delivered timestamps, the replay flag, and the source event's type — never the payload, endpoint, signing secret, or error text. **Required scope:** #### Query Parameters #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Subscription.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Subscription/${RESOURCE_ID}/$deliveries \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Subscription/${id}/$deliveries`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Subscription/{resource_id}/$deliveries", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class getSubscriptionDeliveriesExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String resourceId = "RESOURCE_ID"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Subscription/" + resourceId + "/$deliveries")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func getSubscriptionDeliveriesExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Subscription/"+resourceID+"/$deliveries", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00102](/v1/errors#hpb-00102) | 404 | Resource not found | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Search Subscriptions Search outbound-webhook Subscriptions for the authenticated organization. Supports `_id` and `status` filters plus offset pagination. Responses NEVER include the signing secret. **Required scope:** #### Query Parameters #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Subscription.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/Subscription \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Subscription`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/Subscription", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class searchSubscriptionsExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Subscription")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func searchSubscriptionsExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/Subscription", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ### Create Subscription Create an outbound-webhook Subscription. The server assigns the resource ID and generates an HMAC signing secret, which is returned EXACTLY ONCE in the `201` response body as a `valueString` extension (`https://huli.io/fhir/StructureDefinition/subscription-signing-secret`) with a `Cache-Control: no-store` response header. The secret is never returned again — store it securely. Validation (all fail-closed, `400`): - `channel.type` must be `rest-hook`. - `channel.endpoint` must be an `https` URL with no embedded credentials (SSRF-checked). - `channel.payload`, when set, must be `application/fhir+json`. - `channel.header` entries must be `Name: value` (no CR/LF, no duplicates). - `criteria` must be a bare resource type — one of `Patient`, `Appointment`, `Encounter`, `Observation`, `MedicationRequest`, `ServiceRequest`. Query-parameter filtering is not supported in v1 (fan-out matches on resource type only); a `criteria` carrying a query (e.g. `Encounter?status=finished`) is rejected. - `reason` is required. The `201` body also carries a non-blocking warning `OperationOutcome` (in `contained`) reminding the integrator of their BAA / PHI-handling responsibility for the configured endpoint. **Required scope:** #### Request Body #### Request Example ```json { "resourceType": "Subscription", "status": "requested", "reason": "Notify our EHR mirror of encounter changes", "criteria": "Encounter", "channel": { "type": "rest-hook", "endpoint": "https://example.org/webhooks/huli", "payload": "application/fhir+json", "header": [ "X-Source-System: acme-ehr" ] } } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Subscription.crud" \ | jq -r .access_token) curl -X POST https://api.huli.ai/fhir/R4/Subscription \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/fhir+json" \ -d @subscription.json ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/Subscription`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/fhir+json", }, body: JSON.stringify(payload), } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/fhir+json"} resp = requests.post( f"https://api.huli.ai/fhir/R4/Subscription", headers=headers, json=payload, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class createSubscriptionExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); String payload = "{}"; // your serialized FHIR resource HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Subscription")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .header("Content-Type", "application/fhir+json") .method("POST", HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func createSubscriptionExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("POST", "https://api.huli.ai/fhir/R4/Subscription", body) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/fhir+json") // set req.Body to your serialized resource resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # ValueSet (FHIR R4) # URL: https://developers.huli.ai/v1/api/r4/valueset # FHIR R4 ValueSet resource. # ValueSet (FHIR R4) FHIR R4 ValueSet resource. > This reference is generated from the canonical Huli Public API OpenAPI specification and the server's FHIR R4 CapabilityStatement — do not edit it directly. ## FHIR R4 Specification Official spec: [ValueSet — HL7 FHIR R4](https://hl7.org/fhir/R4/valueset.html) ## Supported Interactions - **$expand** — operation Terminology operation. Scope is enforced per ValueSet `url` by the handler (cancellation-reason → `system/Appointment.rs`, observation-loinc → `system/Observation.rs`, mx-* → `system/Patient.rs`). ## Scopes Scopes are shared across FHIR releases — the same `system/ValueSet.*` scope grants ValueSet access on both `/fhir/R4` and `/fhir/R5`. ## Endpoints ### Expand a ValueSet Expand one of the Huli terminology ValueSets into its codes (FHIR `$expand` operation). The `url` parameter selects the catalog. Scope is enforced per-url: - `.../ValueSet/cancellation-reason` requires `system/Appointment.rs` - `.../ValueSet/observation-loinc` requires `system/Observation.rs` - `.../ValueSet/mx-country`, `.../ValueSet/mx-municipality`, `.../ValueSet/mx-locality` require `system/Patient.rs` `mx-municipality` requires a `state` parameter; `mx-locality` requires both `state` and `municipality`. The optional `filter` is a case/accent-insensitive name/code prefix where the backing catalog supports it. **Required scope:** #### Query Parameters #### Response — 200 ```json { "resourceType": "ValueSet", "url": "https://fhir.huli.ai/r4/ValueSet/cancellation-reason", "status": "active", "expansion": { "total": 1, "contains": [ { "system": "https://fhir.huli.ai/r4/CodeSystem/cancellation-reason", "code": "patient-no-show", "display": "El paciente no se presentó" } ] } } ``` #### Code Samples :::CodeGroup ```bash {label="cURL"} TOKEN=$(curl -s -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Appointment.rs" \ | jq -r .access_token) curl -X GET https://api.huli.ai/fhir/R4/ValueSet/$expand \ -H "Authorization: Bearer ${TOKEN}" ``` ```typescript {label="TypeScript"} const token = process.env.HULI_ACCESS_TOKEN ?? ""; const response = await fetch( `https://api.huli.ai/fhir/R4/ValueSet/$expand`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, }, } ); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); const data: unknown = await response.json(); ``` ```python {label="Python"} import os import requests token = os.environ["HULI_ACCESS_TOKEN"] headers = {"Authorization": f"Bearer {token}"} resp = requests.get( f"https://api.huli.ai/fhir/R4/ValueSet/$expand", headers=headers, ) resp.raise_for_status() print(resp.json()) ``` ```java {label="Java"} import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class expandValueSetExample { public static void main(String[] args) throws Exception { String token = System.getenv("HULI_ACCESS_TOKEN"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/ValueSet/$expand")) .header("Authorization", "Bearer " + token) .header("Accept", "application/fhir+json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("status: " + response.statusCode()); System.out.println(response.body()); } } ``` ```go {label="Go"} import ( "fmt" "net/http" "os" ) func expandValueSetExample() { token := os.Getenv("HULI_ACCESS_TOKEN") req, _ := http.NewRequest("GET", "https://api.huli.ai/fhir/R4/ValueSet/$expand", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("error:", err) return } defer resp.Body.Close() fmt.Println("status:", resp.Status) } ``` ::: #### Errors | Code | Status | Description | |------|--------|-------------| | [HPB-00101](/v1/errors#hpb-00101) | 400 | Validation error | | [HPB-00106](/v1/errors#hpb-00106) | 401 | Authentication failed | | [HPB-00104](/v1/errors#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](/v1/errors#hpb-00105) | 429 | Rate limit exceeded | --- ======================================================================== # Appointment (FHIR R5) # URL: https://developers.huli.ai/v1/api/r5/appointment # A booking of a healthcare event between patient(s) and practitioner(s) for a specific date/time. Maps to HuliPractice ap # Appointment (FHIR R5) A booking of a healthcare event between patient(s) and practitioner(s) for a specific date/time. Maps to HuliPractice appointment slots with start/end times, status, and participant references. > This page is auto-generated from the server's FHIR R5 CapabilityStatement (`/fhir/R5/metadata`). ## FHIR R5 Specification Official spec: [Appointment — HL7 FHIR R5](https://hl7.org/fhir/R5/appointment.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R5/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R5/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R5/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R5/{Resource}/{id}`) Supports recurring appointments via the R5 `recurrenceTemplate` element: POST with a recurrenceTemplate creates a series. Series edits select scope with the `_recurrenceScope` query parameter (`this` | `this-and-following` | `all`) on the PUT; cancelling a series is a PUT with status=cancelled plus `_recurrenceScope`. GET/search echo the recurrenceTemplate and a `recurrence-pattern-id` extension for occurrences that belong to a series. ## Scopes Scopes are shared across FHIR releases — the same `system/Appointment.*` scope grants Appointment access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-appointment-rs) - — see [scope reference](/v1/scopes#system-appointment-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `patient` | reference | Resource reference — supply the UUID of the referenced resource. | | `practitioner` | reference | Resource reference — supply the UUID of the referenced resource. | | `date` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `status` | token | Exact match. For identifiers use `system|value` format. | | `appointment-type` | token | Exact match. For identifiers use `system|value` format. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_cursor` | string | Case-insensitive partial match. | ## Endpoints Operations, request/response examples, and code samples mirror the R4 surface — call them at `/fhir/R5` with the same token and scopes. See the [Appointment R4 reference](/v1/api/r4/appointment). ======================================================================== # Composition (FHIR R5) # URL: https://developers.huli.ai/v1/api/r5/composition # FHIR R5 Composition resource. # Composition (FHIR R5) FHIR R5 Composition resource. > This page is auto-generated from the server's FHIR R5 CapabilityStatement (`/fhir/R5/metadata`). ## FHIR R5 Specification Official spec: [Composition — HL7 FHIR R5](https://hl7.org/fhir/R5/composition.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R5/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R5/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R5/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R5/{Resource}/{id}`) ## Scopes Scopes are shared across FHIR releases — the same `system/Composition.*` scope grants Composition access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-composition-rs) - — see [scope reference](/v1/scopes#system-composition-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `patient` | reference | Resource reference — supply the UUID of the referenced resource. | | `type` | token | Exact match. For identifiers use `system|value` format. | | `date` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_cursor` | string | Case-insensitive partial match. | ## Endpoints Operations, request/response examples, and code samples mirror the R4 surface — call them at `/fhir/R5` with the same token and scopes. See the [Composition R4 reference](/v1/api/r4/composition). ======================================================================== # DocumentReference (FHIR R5) # URL: https://developers.huli.ai/v1/api/r5/documentreference # FHIR R5 DocumentReference resource. # DocumentReference (FHIR R5) FHIR R5 DocumentReference resource. > This page is auto-generated from the server's FHIR R5 CapabilityStatement (`/fhir/R5/metadata`). ## FHIR R5 Specification Official spec: [DocumentReference — HL7 FHIR R5](https://hl7.org/fhir/R5/documentreference.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R5/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R5/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R5/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R5/{Resource}/{id}`) - **$upload** — operation ## Scopes Scopes are shared across FHIR releases — the same `system/DocumentReference.*` scope grants DocumentReference access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-documentreference-rs) - — see [scope reference](/v1/scopes#system-documentreference-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `patient` | reference | Resource reference — supply the UUID of the referenced resource. | | `type` | token | Exact match. For identifiers use `system|value` format. | | `category` | token | Exact match. For identifiers use `system|value` format. | | `date` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_offset` | number | Integer. For `_count`: default 20, max 100. | ## Endpoints Operations, request/response examples, and code samples mirror the R4 surface — call them at `/fhir/R5` with the same token and scopes. See the [DocumentReference R4 reference](/v1/api/r4/documentreference). ======================================================================== # Encounter (FHIR R5) # URL: https://developers.huli.ai/v1/api/r5/encounter # An interaction between a patient and healthcare provider for the purpose of providing healthcare service. Maps to HuliPr # Encounter (FHIR R5) An interaction between a patient and healthcare provider for the purpose of providing healthcare service. Maps to HuliPractice clinical encounters (consultations). > This page is auto-generated from the server's FHIR R5 CapabilityStatement (`/fhir/R5/metadata`). ## FHIR R5 Specification Official spec: [Encounter — HL7 FHIR R5](https://hl7.org/fhir/R5/encounter.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R5/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R5/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R5/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R5/{Resource}/{id}`) ## Scopes Scopes are shared across FHIR releases — the same `system/Encounter.*` scope grants Encounter access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-encounter-rs) - — see [scope reference](/v1/scopes#system-encounter-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `patient` | reference | Resource reference — supply the UUID of the referenced resource. | | `practitioner` | reference | Resource reference — supply the UUID of the referenced resource. | | `date` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `status` | token | Exact match. For identifiers use `system|value` format. | | `class` | token | Exact match. For identifiers use `system|value` format. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_cursor` | string | Case-insensitive partial match. | ## Endpoints Operations, request/response examples, and code samples mirror the R4 surface — call them at `/fhir/R5` with the same token and scopes. See the [Encounter R4 reference](/v1/api/r4/encounter). ======================================================================== # HealthcareService (FHIR R5) # URL: https://developers.huli.ai/v1/api/r5/healthcareservice # FHIR R5 HealthcareService resource. # HealthcareService (FHIR R5) FHIR R5 HealthcareService resource. > This page is auto-generated from the server's FHIR R5 CapabilityStatement (`/fhir/R5/metadata`). ## FHIR R5 Specification Official spec: [HealthcareService — HL7 FHIR R5](https://hl7.org/fhir/R5/healthcareservice.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R5/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R5/{Resource}?...`) Authorized by the SMART scope `system/Appointment.rs`. This resource inherits an existing scope — no per-resource `system/HealthcareService.*` scope is minted; requesting one is rejected. ## Scopes Scopes are shared across FHIR releases — the same `system/HealthcareService.*` scope grants HealthcareService access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-appointment-rs) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_offset` | number | Integer. For `_count`: default 20, max 100. | ## Endpoints Operations, request/response examples, and code samples mirror the R4 surface — call them at `/fhir/R5` with the same token and scopes. See the [HealthcareService R4 reference](/v1/api/r4/healthcareservice). ======================================================================== # MedicationRequest (FHIR R5) # URL: https://developers.huli.ai/v1/api/r5/medicationrequest # FHIR R5 MedicationRequest resource. # MedicationRequest (FHIR R5) FHIR R5 MedicationRequest resource. > This page is auto-generated from the server's FHIR R5 CapabilityStatement (`/fhir/R5/metadata`). ## FHIR R5 Specification Official spec: [MedicationRequest — HL7 FHIR R5](https://hl7.org/fhir/R5/medicationrequest.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R5/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R5/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R5/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R5/{Resource}/{id}`) ## Scopes Scopes are shared across FHIR releases — the same `system/MedicationRequest.*` scope grants MedicationRequest access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-medicationrequest-rs) - — see [scope reference](/v1/scopes#system-medicationrequest-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `patient` | reference | Resource reference — supply the UUID of the referenced resource. | | `encounter` | reference | Resource reference — supply the UUID of the referenced resource. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_cursor` | string | Case-insensitive partial match. | ## Endpoints Operations, request/response examples, and code samples mirror the R4 surface — call them at `/fhir/R5` with the same token and scopes. See the [MedicationRequest R4 reference](/v1/api/r4/medicationrequest). ======================================================================== # Observation (FHIR R5) # URL: https://developers.huli.ai/v1/api/r5/observation # Measurements and simple assertions made about a patient, device, or other subject. Used for vital signs (heart rate, tem # Observation (FHIR R5) Measurements and simple assertions made about a patient, device, or other subject. Used for vital signs (heart rate, temperature, blood pressure), laboratory results, and clinical exam findings. LOINC codes are validated on write. > This page is auto-generated from the server's FHIR R5 CapabilityStatement (`/fhir/R5/metadata`). ## FHIR R5 Specification Official spec: [Observation — HL7 FHIR R5](https://hl7.org/fhir/R5/observation.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R5/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R5/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R5/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R5/{Resource}/{id}`) ## Scopes Scopes are shared across FHIR releases — the same `system/Observation.*` scope grants Observation access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-observation-rs) - — see [scope reference](/v1/scopes#system-observation-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `patient` | reference | Resource reference — supply the UUID of the referenced resource. | | `encounter` | reference | Resource reference — supply the UUID of the referenced resource. | | `code` | token | Exact match. For identifiers use `system|value` format. | | `date` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `status` | token | Exact match. For identifiers use `system|value` format. | | `category` | token | Exact match. For identifiers use `system|value` format. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_cursor` | string | Case-insensitive partial match. | ## Endpoints Operations, request/response examples, and code samples mirror the R4 surface — call them at `/fhir/R5` with the same token and scopes. See the [Observation R4 reference](/v1/api/r4/observation). ======================================================================== # Organization (FHIR R5) # URL: https://developers.huli.ai/v1/api/r5/organization # A formally or informally recognised grouping of people or organisations formed for healthcare purposes. Read-only in the # Organization (FHIR R5) A formally or informally recognised grouping of people or organisations formed for healthcare purposes. Read-only in the public API — represents the healthcare organisation that owns the data. All resources are scoped to a single organisation per API key. > This page is auto-generated from the server's FHIR R5 CapabilityStatement (`/fhir/R5/metadata`). ## FHIR R5 Specification Official spec: [Organization — HL7 FHIR R5](https://hl7.org/fhir/R5/organization.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R5/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R5/{Resource}?...`) ## Scopes Scopes are shared across FHIR releases — the same `system/Organization.*` scope grants Organization access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-organization-rs) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | ## Endpoints Operations, request/response examples, and code samples mirror the R4 surface — call them at `/fhir/R5` with the same token and scopes. See the [Organization R4 reference](/v1/api/r4/organization). ======================================================================== # Patient (FHIR R5) # URL: https://developers.huli.ai/v1/api/r5/patient # Demographic and administrative information about an individual receiving healthcare services. Maps to HuliPractice patie # Patient (FHIR R5) Demographic and administrative information about an individual receiving healthcare services. Maps to HuliPractice patient records including name, birth date, gender, contact information, and identifiers such as CURP. > This page is auto-generated from the server's FHIR R5 CapabilityStatement (`/fhir/R5/metadata`). ## FHIR R5 Specification Official spec: [Patient — HL7 FHIR R5](https://hl7.org/fhir/R5/patient.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R5/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R5/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R5/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R5/{Resource}/{id}`) - **$everything** — operation ## Scopes Scopes are shared across FHIR releases — the same `system/Patient.*` scope grants Patient access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-patient-rs) - — see [scope reference](/v1/scopes#system-patient-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `identifier` | token | Exact match. For identifiers use `system|value` format. | | `name` | string | Case-insensitive partial match. | | `gender` | token | Exact match. For identifiers use `system|value` format. | | `birthdate` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `active` | token | Exact match. For identifiers use `system|value` format. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_cursor` | string | Case-insensitive partial match. | ## Endpoints Operations, request/response examples, and code samples mirror the R4 surface — call them at `/fhir/R5` with the same token and scopes. See the [Patient R4 reference](/v1/api/r4/patient). ======================================================================== # Practitioner (FHIR R5) # URL: https://developers.huli.ai/v1/api/r5/practitioner # A person who is directly or indirectly involved in providing healthcare. Read-only in the public API — practitioner reco # Practitioner (FHIR R5) A person who is directly or indirectly involved in providing healthcare. Read-only in the public API — practitioner records are managed through HuliPractice. Referenced by Appointment and Encounter resources. > This page is auto-generated from the server's FHIR R5 CapabilityStatement (`/fhir/R5/metadata`). ## FHIR R5 Specification Official spec: [Practitioner — HL7 FHIR R5](https://hl7.org/fhir/R5/practitioner.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R5/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R5/{Resource}?...`) ## Scopes Scopes are shared across FHIR releases — the same `system/Practitioner.*` scope grants Practitioner access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-practitioner-rs) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `name` | string | Case-insensitive partial match. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_offset` | number | Integer. For `_count`: default 20, max 100. | ## Endpoints Operations, request/response examples, and code samples mirror the R4 surface — call them at `/fhir/R5` with the same token and scopes. See the [Practitioner R4 reference](/v1/api/r4/practitioner). ======================================================================== # PractitionerRole (FHIR R5) # URL: https://developers.huli.ai/v1/api/r5/practitionerrole # FHIR R5 PractitionerRole resource. # PractitionerRole (FHIR R5) FHIR R5 PractitionerRole resource. > This page is auto-generated from the server's FHIR R5 CapabilityStatement (`/fhir/R5/metadata`). ## FHIR R5 Specification Official spec: [PractitionerRole — HL7 FHIR R5](https://hl7.org/fhir/R5/practitionerrole.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R5/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R5/{Resource}?...`) Authorized by the SMART scope `system/Practitioner.rs`. This resource inherits an existing scope — no per-resource `system/PractitionerRole.*` scope is minted; requesting one is rejected. ## Scopes Scopes are shared across FHIR releases — the same `system/PractitionerRole.*` scope grants PractitionerRole access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-practitioner-rs) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `practitioner` | reference | Resource reference — supply the UUID of the referenced resource. | | `location` | reference | Resource reference — supply the UUID of the referenced resource. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_offset` | number | Integer. For `_count`: default 20, max 100. | ## Endpoints Operations, request/response examples, and code samples mirror the R4 surface — call them at `/fhir/R5` with the same token and scopes. See the [PractitionerRole R4 reference](/v1/api/r4/practitionerrole). ======================================================================== # Schedule (FHIR R5) # URL: https://developers.huli.ai/v1/api/r5/schedule # FHIR R5 Schedule resource. # Schedule (FHIR R5) FHIR R5 Schedule resource. > This page is auto-generated from the server's FHIR R5 CapabilityStatement (`/fhir/R5/metadata`). ## FHIR R5 Specification Official spec: [Schedule — HL7 FHIR R5](https://hl7.org/fhir/R5/schedule.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R5/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R5/{Resource}?...`) Authorized by the SMART scope `system/Appointment.rs`. This resource inherits an existing scope — no per-resource `system/Schedule.*` scope is minted; requesting one is rejected. ## Scopes Scopes are shared across FHIR releases — the same `system/Schedule.*` scope grants Schedule access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-appointment-rs) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `_id` | token | Exact match. For identifiers use `system|value` format. | | `actor` | reference | Resource reference — supply the UUID of the referenced resource. | | `service-type` | token | Exact match. For identifiers use `system|value` format. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_offset` | number | Integer. For `_count`: default 20, max 100. | ## Endpoints Operations, request/response examples, and code samples mirror the R4 surface — call them at `/fhir/R5` with the same token and scopes. See the [Schedule R4 reference](/v1/api/r4/schedule). ======================================================================== # ServiceRequest (FHIR R5) # URL: https://developers.huli.ai/v1/api/r5/servicerequest # FHIR R5 ServiceRequest resource. # ServiceRequest (FHIR R5) FHIR R5 ServiceRequest resource. > This page is auto-generated from the server's FHIR R5 CapabilityStatement (`/fhir/R5/metadata`). ## FHIR R5 Specification Official spec: [ServiceRequest — HL7 FHIR R5](https://hl7.org/fhir/R5/servicerequest.html) ## Supported Interactions - **read** — Read a single resource by ID (`GET /fhir/R5/{Resource}/{id}`) - **search-type** — Search resources with query parameters (`GET /fhir/R5/{Resource}?...`) - **create** — Create a new resource (`POST /fhir/R5/{Resource}`) - **update** — Update an existing resource (`PUT /fhir/R5/{Resource}/{id}`) ## Scopes Scopes are shared across FHIR releases — the same `system/ServiceRequest.*` scope grants ServiceRequest access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-servicerequest-rs) - — see [scope reference](/v1/scopes#system-servicerequest-cru) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `patient` | reference | Resource reference — supply the UUID of the referenced resource. | | `encounter` | reference | Resource reference — supply the UUID of the referenced resource. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_cursor` | string | Case-insensitive partial match. | ## Endpoints Operations, request/response examples, and code samples mirror the R4 surface — call them at `/fhir/R5` with the same token and scopes. See the [ServiceRequest R4 reference](/v1/api/r4/servicerequest). ======================================================================== # Slot (FHIR R5) # URL: https://developers.huli.ai/v1/api/r5/slot # FHIR R5 Slot resource. # Slot (FHIR R5) FHIR R5 Slot resource. > This page is auto-generated from the server's FHIR R5 CapabilityStatement (`/fhir/R5/metadata`). ## FHIR R5 Specification Official spec: [Slot — HL7 FHIR R5](https://hl7.org/fhir/R5/slot.html) ## Supported Interactions - **search-type** — Search resources with query parameters (`GET /fhir/R5/{Resource}?...`) Authorized by the SMART scope `system/Appointment.rs`. This resource inherits an existing scope — no per-resource `system/Slot.*` scope is minted; requesting one is rejected. ## Scopes Scopes are shared across FHIR releases — the same `system/Slot.*` scope grants Slot access on both `/fhir/R4` and `/fhir/R5`. - — see [scope reference](/v1/scopes#system-appointment-rs) ## Search Parameters | Parameter | Type | Notes | |-----------|------|-------| | `schedule` | reference | Resource reference — supply the UUID of the referenced resource. | | `actor` | reference | Resource reference — supply the UUID of the referenced resource. | | `start` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `end` | date | Supports FHIR date prefixes: `eq`, `ne`, `gt`, `ge`, `lt`, `le`. Format: `[prefix]YYYY-MM-DD`. | | `_count` | number | Integer. For `_count`: default 20, max 100. | | `_offset` | number | Integer. For `_count`: default 20, max 100. | ## Endpoints Operations, request/response examples, and code samples mirror the R4 surface — call them at `/fhir/R5` with the same token and scopes. See the [Slot R4 reference](/v1/api/r4/slot). ======================================================================== # FHIR resources # URL: https://developers.huli.ai/v1/api/resources # Every FHIR resource on the Huli Public API — both R4 and R5 — with its interactions, scopes, and per-version reference page. # FHIR resources The Huli Public API serves **both** [FHIR R4](https://hl7.org/fhir/R4/) (the stable default) and [FHIR R5](https://hl7.org/fhir/R5/). Auth, scopes, and pagination are identical across releases — only the wire shapes and the base path (`/fhir/R4` vs `/fhir/R5`) differ. Not sure which to target? See [Choosing R4 vs R5](/v1/api/fhir-versions). ## Implementation Guide Huli publishes a FHIR Implementation Guide with the profiles, extensions, and value sets for both R4 and R5 — see the [FHIR Implementation Guide](/v1/api/fhir-ig). ## FHIR R4 resources The stable default surface. | Resource | Interactions | Scopes | |----------|-------------|--------| | [Patient](/v1/api/r4/patient) | `read`, `search-type`, `create`, `update`, `$everything` | `system/Patient.rs`, `system/Patient.cru` | | [Appointment](/v1/api/r4/appointment) | `read`, `search-type`, `create`, `update` | `system/Appointment.rs`, `system/Appointment.cru` | | [Encounter](/v1/api/r4/encounter) | `read`, `search-type`, `create`, `update` | `system/Encounter.rs`, `system/Encounter.cru` | | [Observation](/v1/api/r4/observation) | `read`, `search-type`, `create`, `update` | `system/Observation.rs`, `system/Observation.cru` | | [MedicationRequest](/v1/api/r4/medicationrequest) | `read`, `search-type`, `create`, `update` | `system/MedicationRequest.rs`, `system/MedicationRequest.cru` | | [ServiceRequest](/v1/api/r4/servicerequest) | `read`, `search-type`, `create`, `update` | `system/ServiceRequest.rs`, `system/ServiceRequest.cru` | | [Composition](/v1/api/r4/composition) | `read`, `search-type`, `create`, `update` | `system/Composition.rs`, `system/Composition.cru` | | [DocumentReference](/v1/api/r4/documentreference) | `read`, `search-type`, `create`, `update`, `$upload` | `system/DocumentReference.rs`, `system/DocumentReference.cru` | | [Practitioner](/v1/api/r4/practitioner) | `read`, `search-type` | `system/Practitioner.rs` | | [Organization](/v1/api/r4/organization) | `read`, `search-type` | `system/Organization.rs` | | [Subscription](/v1/api/r4/subscription) | `read`, `search-type`, `create`, `update`, `delete`, `$replay`, `$stats`, `$deliveries` | `system/Subscription.rs`, `system/Subscription.crud` | | [Location](/v1/api/r4/location) | `read`, `search-type` | `system/Appointment.rs` | | [HealthcareService](/v1/api/r4/healthcareservice) | `read`, `search-type` | `system/Appointment.rs` | | [PractitionerRole](/v1/api/r4/practitionerrole) | `read`, `search-type` | `system/Practitioner.rs` | | [Schedule](/v1/api/r4/schedule) | `read`, `search-type` | `system/Appointment.rs` | | [Slot](/v1/api/r4/slot) | `search-type` | `system/Appointment.rs` | | [Device](/v1/api/r4/device) | `read`, `search-type` | `system/Appointment.rs` | | [CodeSystem](/v1/api/r4/codesystem) | `read` | | | [ValueSet](/v1/api/r4/valueset) | `$expand` | | ## FHIR R5 resources The newer wire shapes — same token, same scopes, base path `/fhir/R5`. | Resource | Interactions | Scopes | |----------|-------------|--------| | [Patient](/v1/api/r5/patient) | `read`, `search-type`, `create`, `update`, `$everything` | `system/Patient.rs`, `system/Patient.cru` | | [Appointment](/v1/api/r5/appointment) | `read`, `search-type`, `create`, `update` | `system/Appointment.rs`, `system/Appointment.cru` | | [Encounter](/v1/api/r5/encounter) | `read`, `search-type`, `create`, `update` | `system/Encounter.rs`, `system/Encounter.cru` | | [Observation](/v1/api/r5/observation) | `read`, `search-type`, `create`, `update` | `system/Observation.rs`, `system/Observation.cru` | | [MedicationRequest](/v1/api/r5/medicationrequest) | `read`, `search-type`, `create`, `update` | `system/MedicationRequest.rs`, `system/MedicationRequest.cru` | | [ServiceRequest](/v1/api/r5/servicerequest) | `read`, `search-type`, `create`, `update` | `system/ServiceRequest.rs`, `system/ServiceRequest.cru` | | [Composition](/v1/api/r5/composition) | `read`, `search-type`, `create`, `update` | `system/Composition.rs`, `system/Composition.cru` | | [DocumentReference](/v1/api/r5/documentreference) | `read`, `search-type`, `create`, `update`, `$upload` | `system/DocumentReference.rs`, `system/DocumentReference.cru` | | [Practitioner](/v1/api/r5/practitioner) | `read`, `search-type` | `system/Practitioner.rs` | | [Organization](/v1/api/r5/organization) | `read`, `search-type` | `system/Organization.rs` | | [HealthcareService](/v1/api/r5/healthcareservice) | `read`, `search-type` | `system/Appointment.rs` | | [PractitionerRole](/v1/api/r5/practitionerrole) | `read`, `search-type` | `system/Practitioner.rs` | | [Schedule](/v1/api/r5/schedule) | `read`, `search-type` | `system/Appointment.rs` | | [Slot](/v1/api/r5/slot) | `search-type` | `system/Appointment.rs` | > Subscription, Location, Device, CodeSystem, ValueSet are served on `/fhir/R4` only (forwards-compatible — not duplicated onto R5); find them in the R4 table above. ## Authentication All resource requests require a Bearer token from [`POST /auth/token`](/v1/api/authentication). See [Authentication](/v1/auth) for the full SMART Backend Services flow, and [Scopes](/v1/scopes) for the access model. ======================================================================== # Auth # URL: https://developers.huli.ai/v1/auth # Three authentication modes for the Huli Public API — admin-managed bearer tokens, SMART backend services (M2M), and interactive OAuth with PKCE. Pick the right one for your integration. # Auth The Huli Public API supports three authentication modes. All three are available in v1. Pick based on your integration type. ## Decision guide | Mode | When to use | | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Admin bearer token](/v1/auth/bearer) | Server-to-server integrations where a human admin provisions the key once. Simplest path to a working integration. No JWT signing required. | | [SMART backend services](/v1/auth/jwks) | Automated M2M integrations that need short-lived tokens, key rotation without human involvement, or auditable client identity. Requires RS384 key pair and a reachable JWKS endpoint. | | [Interactive OAuth (PKCE)](/v1/auth/oauth) | User-facing applications where each end user authenticates individually — patient portals, clinician apps, data access requests tied to a specific practitioner identity. | If you are not sure, start with the admin bearer token. It works for the majority of back-office integrations and can be replaced later without changing the request format — both modes set `Authorization: Bearer `. ## Token endpoint All three modes issue tokens via: Base URL: `https://api.huli.ai` (not under `/fhir`). ## Scopes Scopes are requested at token issuance and enforced on every resource operation. The scope format is `system/.` where perms are combinations of `r` (read), `s` (search), `c` (create), `u` (update). See the [Scope reference](/v1/scopes) for the complete table. Cross-links from the individual auth mode pages point to specific scope entries. ## Token lifetime | Mode | Token TTL | | ---------------------- | ------------------------------------------------------------------------------- | | Admin bearer | Does not expire (revoke from Practice Settings) | | SMART backend services | 5 minutes — must be refreshed using `client_credentials` again | | Interactive OAuth | Configurable per app registration (default: 1h access token, 30d refresh token) | ## SMART discovery The discovery document for SMART configuration is at: ``` GET /fhir/.well-known/smart-configuration ``` Full URL: `https://api.huli.ai/fhir/.well-known/smart-configuration` It returns the token endpoint, JWKS URI, scopes supported, and grant types. Any SMART-compliant client library can use this to configure itself automatically. ## What's in each section - **[Bearer tokens](/v1/auth/bearer)** — provisioning, rotation, and usage. Copy-paste path. - **[JWKS / SMART backend services](/v1/auth/jwks)** — key pair generation, JWKS endpoint setup, client assertion format, token exchange, replay protection. - **[Interactive OAuth](/v1/auth/oauth)** — app registration, authorization code flow, PKCE, redirect URIs, and consent screen behavior. ======================================================================== # Bearer Tokens # URL: https://developers.huli.ai/v1/auth/bearer # Admin-managed bearer tokens for server-to-server integrations — provisioning, usage, and rotation. # Bearer Tokens Admin-managed bearer tokens are the fastest path to a working integration. A human admin creates the key once in Practice Settings; the token is used directly in the `Authorization` header with no signing, no token exchange, and no expiry. ## Provisioning 1. Log in to HuliPractice as an admin. 2. Navigate to **Settings → Integrations → API Keys**. 3. Click **New API key**. 4. Select the scopes your integration needs (see [Scopes](/v1/scopes)). 5. Copy the token. It is shown once. The token is a random 256-bit value encoded as a hex string. It does not encode any claims — the server resolves the associated organization and scopes by looking up the hash. ## Usage Set the token in the `Authorization` header on every request: ```bash curl https://api.huli.ai/fhir/R4/Patient?_count=1 \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Accept: application/fhir+json" ``` No additional headers are required. The server resolves the organization from the token. ## Storing the token - Store in a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, etc.) or an environment variable. - Do not commit to source control. - Do not include in client-side bundles — bearer tokens are server-side credentials only. - Do not log the `Authorization` header. Redact it in request traces. ## Rotation Tokens do not expire on a schedule. Rotate them: - After any suspected compromise. - As part of a regular security rotation policy (recommended: every 90 days). - When the admin who provisioned the key leaves the organization. Rotation is atomic: in Practice Settings, click **Rotate** on the key. The new token is issued and the old one is revoked immediately. There is no grace period for simultaneous use of old and new tokens — update your deployment before rotating. For zero-downtime rotation: 1. Create a **new** API key with the same scopes (do not rotate the existing one yet). 2. Deploy the new key to your infrastructure. 3. Verify requests are succeeding with the new key. 4. Return to Practice Settings and revoke the old key. ## Revocation Revoked tokens return `401` with `HPB-00106`. Revoke from Practice Settings or by contacting your organization admin. Revoked tokens cannot be re-enabled — create a new key. ## Scope binding The scopes associated with a bearer token are fixed at provisioning time. A request operation that requires a scope not on the token returns `403` with `HPB-00104`. To add scopes, create a new key with the required scopes. You cannot add scopes to an existing key after provisioning. ## Limitations Bearer tokens are not suitable for: - **User-facing applications** — the token represents the organization, not an individual user. Use [interactive OAuth](/v1/auth/oauth) for per-user authentication. - **Short-lived credentials** — if your security policy requires tokens to expire automatically, use [SMART backend services](/v1/auth/jwks) instead (5-minute TTL). - **Multi-organization integrations** — each bearer token is scoped to one organization. You need one token per organization. ======================================================================== # SMART Backend Services # URL: https://developers.huli.ai/v1/auth/jwks # M2M authentication via client_credentials + private_key_jwt signed with RS384 — key generation, JWKS endpoint setup, client assertion format, and token exchange. # SMART Backend Services SMART backend services authentication issues short-lived access tokens (5-minute TTL) using the `client_credentials` grant with a `private_key_jwt` client assertion. The client signs a JWT with its private key; the server verifies the signature against the client's JWKS endpoint. This mode corresponds to the [HL7 SMART Backend Services specification](https://hl7.org/fhir/uv/bulkdata/authorization/). ## Prerequisites - A registered API key in Practice Settings with a JWKS URI set. - An RS384 (RSA 2048+ bit) key pair. The public key is served from your JWKS endpoint. - A reachable JWKS endpoint (HTTPS, public, no auth required). Huli fetches it to verify the client assertion signature. ## Key generation Generate an RS384 key pair: ```bash # Generate private key (2048-bit minimum; 4096-bit recommended) openssl genrsa -out private.pem 4096 # Extract public key openssl rsa -in private.pem -pubout -out public.pem # Convert to JWK format (install node-jose-tools or similar) npx node-jose-tools key-to-jwk --input private.pem --use sig --alg RS384 ``` Serve the public JWK at your JWKS endpoint. The endpoint must return: ```json { "keys": [ { "kty": "RSA", "use": "sig", "alg": "RS384", "kid": "huli-key-2026-06-01", "n": "", "e": "AQAB" } ] } ``` The `kid` (key ID) must match the `kid` in your client assertion header (see below). ## Registering the JWKS URI In Practice Settings → Integrations → API Keys, set the **JWKS URI** field to the URL of your JWKS endpoint (e.g., `https://keys.example.com/.well-known/jwks.json`). Huli caches JWKS responses for 55 seconds and re-fetches at most once per 60 seconds per URI. Plan key rotations to overlap with the cache TTL. ## Client assertion format Build a JWT signed with your private key: ```json { "alg": "RS384", "kid": "huli-key-2026-06-01" } ``` ```json { "iss": "", "sub": "", "aud": "https://api.huli.ai/fhir", "iat": 1748808600, "exp": 1748808900, "jti": "unique-token-id-e8f2a1c9" } ``` Rules: - `iss` and `sub` must both equal your `client_id` (the API key identifier visible in Practice Settings). - `aud` must be exactly `https://api.huli.ai/fhir`. - `exp` must be within 5 minutes of `iat`. Assertions with longer lifetimes are rejected. - `jti` must be unique per assertion. Huli records used JTI values to prevent replay attacks. Re-using a JTI within the assertion TTL returns `401` with `HPB-00106`. ## Token exchange ```bash CLIENT_ASSERTION=$(python3 - <<'EOF' import jwt, time, uuid from cryptography.hazmat.primitives.serialization import load_pem_private_key private_key = load_pem_private_key(open("private.pem", "rb").read(), password=None) now = int(time.time()) payload = { "iss": "your-client-id", "sub": "your-client-id", "aud": "https://api.huli.ai/fhir", "iat": now, "exp": now + 270, "jti": str(uuid.uuid4()), } print(jwt.encode(payload, private_key, algorithm="RS384", headers={"kid": "huli-key-2026-06-01"})) EOF ) curl -X POST https://api.huli.ai/auth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Patient.rs system/Appointment.rs" ``` Successful response: ```json { "access_token": "eyJhbGciOiJSUzM4NCJ9...", "token_type": "Bearer", "expires_in": 300, "scope": "system/Patient.rs system/Appointment.rs" } ``` `expires_in` is always 300 seconds (5 minutes). Schedule your client to request a new token before the current one expires. A common pattern is to refresh at 240 seconds (80% of TTL) to avoid clock skew issues. ## Using the access token ```bash curl https://api.huli.ai/fhir/R4/Patient?_count=1 \ -H "Authorization: Bearer eyJhbGciOiJSUzM4NCJ9..." \ -H "Accept: application/fhir+json" ``` ## Key rotation To rotate keys without downtime: 1. Generate a new key pair with a new `kid`. 2. Add the new public key to your JWKS endpoint (serve both old and new). 3. Begin signing new assertions with the new `kid`. 4. After 60 seconds (one JWKS cache TTL), Huli will have fetched the updated JWKS. 5. Remove the old key from your JWKS endpoint. ## Security notes - Keep the private key out of environment variables in production. Use a KMS or HSM. - SSRF: Huli validates JWKS URIs against a deny-list (private IP ranges, metadata endpoints). Your JWKS URI must be a publicly routable HTTPS address. - Circuit breaker: 5 consecutive authentication failures from one client trigger a 60-second block. This protects against credential stuffing. Failing fast and waiting is better than retrying immediately. ======================================================================== # Interactive OAuth # URL: https://developers.huli.ai/v1/auth/oauth # Authorization code flow with PKCE for user-facing applications — app registration, redirect URIs, consent screen, token lifecycle. # Interactive OAuth Interactive OAuth uses the Authorization Code flow with PKCE ([RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)). Use this mode for applications where each end user authenticates individually — patient portals, clinician apps, and integrations where the access token must represent a specific user identity rather than the organization. ## App registration Contact your Huli organization admin to register your application. You will need to provide: - **Redirect URIs** — the exact URIs your application will use for the OAuth callback. Wildcards are not permitted. Include all environments (development, staging, production). - **Display name** — shown on the consent screen. - **Requested scopes** — the scopes your app will request (see [Scopes](/v1/scopes)). You receive a `client_id`. Public clients (SPAs, mobile apps, the CLI) do not receive a `client_secret` — PKCE replaces the secret. ## Authorization endpoint ``` https://app.huli.ai/oauth/authorize ``` Build the authorization URL: ```bash # Generate PKCE verifier and challenge CODE_VERIFIER=$(openssl rand -base64 48 | tr -d '=+/' | head -c 64) CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '=') AUTH_URL="https://app.huli.ai/oauth/authorize\ ?response_type=code\ &client_id=your-client-id\ &redirect_uri=https%3A%2F%2Fyourapp.example.com%2Fcallback\ &scope=system%2FPatient.rs%20system%2FAppointment.rs\ &state=$(openssl rand -hex 16)\ &code_challenge=${CODE_CHALLENGE}\ &code_challenge_method=S256" echo "$AUTH_URL" ``` Open the URL in a browser. The user authenticates to HuliPractice and sees the consent screen listing the requested scopes. After approval, the browser redirects to your `redirect_uri` with a `code` query parameter. ## Token exchange ```bash curl -X POST https://api.huli.ai/auth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=" \ -d "redirect_uri=https://yourapp.example.com/callback" \ -d "client_id=your-client-id" \ -d "code_verifier=${CODE_VERIFIER}" ``` Successful response: ```json { "access_token": "eyJhbGciOiJSUzM4NCJ9...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...", "scope": "system/Patient.rs system/Appointment.rs" } ``` Default TTLs: access token 1h, refresh token 30 days. ## Refreshing access tokens ```bash curl -X POST https://api.huli.ai/auth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "refresh_token=dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..." \ -d "client_id=your-client-id" ``` Refresh tokens are single-use (rotation). Each refresh issues a new access token and a new refresh token. Store the new refresh token immediately — the old one is invalidated. ## PKCE requirements PKCE is **required** for all public clients. Requests to the authorization endpoint without `code_challenge` and `code_challenge_method=S256` return an error. Never use `code_challenge_method=plain`. The `S256` method is enforced. ## State parameter Always pass `state`. Verify the `state` returned in the redirect matches what you sent. This prevents CSRF attacks on the redirect. ## Consent screen The consent screen at `app.huli.ai/oauth/authorize` shows: - Your application name (set at registration). - The organization the user is authenticated to. - The scopes being requested (in plain language). The user must actively click **Authorize** — there is no auto-approval. ## Token identity Interactive OAuth tokens carry the identity of the authenticated user. The access token encodes the user's identity, their `organization_id`, and the granted scopes. Resource operations are audited against the user, not just the application. This is the primary distinction from admin bearer tokens and SMART backend services, which represent the application/key rather than an individual. ## CLI usage Interactive OAuth is a supported v1 auth mode on the API. The `huli` CLI does not include an interactive-login subcommand for it — for CLI access use SMART backend services (`huli auth setup`) or an admin bearer token. See the [CLI reference](/v1/cli) for the full `huli auth` reference. ======================================================================== # Changelog # URL: https://developers.huli.ai/v1/changelog # Release history for the Huli Public API — dated, versioned, and flagged for breaking changes. # Changelog ## Huli Public API v1.0 — General Availability **Breaking:** no. This is the initial public release. ### What ships **FHIR R4 resources** - `Patient` — read, search, create, update. Identifiers: CURP, RFC, NSS, INE. Two-surname support via the `segundo-apellido` extension. Gender mapping: FHIR `male/female/other` → Huli `M/F/I`. - `Appointment` — read, search, create, update. Status transitions: `booked`, `cancelled`, `fulfilled`. - `Encounter` — read, search, create, update. Class: ambulatory, emergency, inpatient. - `Observation` — read, search, create, update. Categories: `vital-signs`, `laboratory`, `exam`. LOINC required on write. UCUM validated. - `Practitioner` — read, search (read-only). - `Organization` — read, search (read-only). **Authentication** - Admin-managed bearer tokens via Practice Settings. - SMART backend services (`client_credentials` + `private_key_jwt`, RS384). - Interactive OAuth Authorization Code + PKCE via `app.huli.ai/oauth/authorize`. - SMART discovery at `/fhir/.well-known/smart-configuration`. **Rate limiting and audit** - Per-key and per-organization request ceilings. `429` with `Retry-After` on limit exceeded. - Every authenticated request writes an audit record with 7-year retention (NOM-024 compliance requirement). **`huli` CLI v1.0** - `huli auth login` (interactive OAuth PKCE), `huli auth setup` (bearer), `huli auth status`, `huli auth token`. - `huli fhir patient|appointment|encounter|observation|practitioner|organization` subcommands. - `huli api get|post|put` for raw HTTP access. - `huli config get|set|list`. **Scope system** - `system/.` format. Permissions: `r` (read), `s` (search), `c` (create), `u` (update). - Scopes: `system/Patient.rs`, `system/Patient.cru`, `system/Appointment.rs`, `system/Appointment.cru`, `system/Encounter.rs`, `system/Encounter.cru`, `system/Observation.rs`, `system/Observation.cru`, `system/Practitioner.rs`, `system/Organization.rs`. ### Error codes | Code | HTTP | Meaning | | ----------- | ---- | --------------------- | | `HPB-00101` | 400 | Validation error | | `HPB-00102` | 404 | Resource not found | | `HPB-00103` | 409 | Version conflict | | `HPB-00104` | 403 | Insufficient scope | | `HPB-00105` | 429 | Rate limit exceeded | | `HPB-00106` | 401 | Authentication failed | | `HPB-00107` | 401 | Token expired | ### Known limitations at GA - FHIR `$export` (bulk data access) is not available in v1. - Webhook subscriptions are not available in v1. - The `_include` and `_revinclude` FHIR search parameters are not supported. ======================================================================== # CLI # URL: https://developers.huli.ai/v1/cli # The huli command-line interface — install, authenticate, and work against the Public FHIR R4 + R5 API. # Huli CLI The `huli` CLI wraps the Public FHIR API (both R4 and R5) — authentication, pagination, and output formatting from the command line. ## Install ```bash brew install hulilabs/tools/huli-cli ``` The installed binary is named `huli`. Confirm it runs: ```bash huli version ``` ## Command groups ``` huli [command] Available Commands: fhir FHIR resource operations (patient, appointment, encounter, observation, …) auth Authentication and token management config Manage CLI configuration and profiles version Print the CLI version Flags: --help Help for any command --profile Config profile to use (default: "default") --output Output format: json | table | yaml (default: "table") --fhir-version FHIR release to target: r4 | r5 (default: "r4") ``` ## FHIR version (R4 / R5) Every `huli fhir` command works against both FHIR releases. Select the release with the global `--fhir-version` flag (default `r4`); the CLI routes requests to `/fhir/R4` or `/fhir/R5` accordingly: ```bash # Default — FHIR R4 huli fhir patient get # Target FHIR R5 (e.g. for recurring appointments) huli fhir appointment get --fhir-version r5 ``` Auth, scopes, and pagination are identical across releases. See [Choosing R4 vs R5](/v1/api/fhir-versions) for which to pick. Authenticate next — see [Auth](/v1/auth) for bearer tokens, SMART Backend Services, and interactive OAuth. ======================================================================== # Concepts # URL: https://developers.huli.ai/v1/concepts # Mental models for working with the Huli Public API — organizations, pagination, rate limiting, and audit. # Concepts Three concepts underpin every request to the Huli Public API: the organization boundary (which organization owns the data), cursor-based pagination (how sets are navigated), and the rate-limit and audit system (what the API enforces and records on every authenticated call). ## In this section - **[Organizations](/v1/concepts/organizations)** — `organization_id` is the security boundary. Every resource belongs to exactly one organization. Cross-organization reads return `403`. - **[Pagination](/v1/concepts/pagination)** — All list endpoints use cursor-based pagination via `_count` and `_cursor`. `total` is advisory; do not use it as a loop terminator. - **[Rate Limiting](/v1/concepts/rate-limiting)** — Per-key and per-organization request ceilings. Every authenticated request writes an audit record with a 7-year retention obligation. - **[Webhooks](/v1/concepts/webhooks)** — Outbound notifications via the FHIR R4 `Subscription` resource. HMAC-signed, id-level, at-least-once deliveries with retries, auto-pause, and `$replay` for outage recovery. ## FHIR conformance The public surface is FHIR R4. The normative contract is the live CapabilityStatement at [`/fhir/R4/metadata`](https://api.huli.ai/fhir/R4/metadata). All resources and operations advertised there are what the API actually supports — no divergence between docs and the statement. Extensions and custom identifiers are documented in the individual resource pages under the [API reference](/v1/api). ======================================================================== # Organizations # URL: https://developers.huli.ai/v1/concepts/organizations # How the organization boundary works in the Huli Public API — every resource belongs to one organization, and every API key is scoped to one organization. # Organizations Every resource in the Huli Public API — Patient, Appointment, Encounter, Observation, Practitioner, Organization — belongs to exactly one organization. The organization is the security boundary. ## How it works When you authenticate (via any of the three modes — admin bearer, SMART backend services, or interactive OAuth), the resulting access token carries an `organization_id` claim. All resource access is automatically scoped to that organization. There is no way to read or write across organizations with a single token. An API key created in Practice Settings is bound to the organization that admin belongs to. A SMART backend services client assertion is verified against an `api_key` row that also carries an `organization_id`. An interactive OAuth token carries the organization the authenticated user belongs to. ## Cross-organization access Attempting to read a resource that belongs to a different organization returns: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "forbidden", "diagnostics": "HPB-00104: Insufficient scope" } ] } ``` `HPB-00104` covers both missing-scope and cross-organization violations. If you receive this on a resource you believe your token should reach, verify: 1. The resource ID belongs to your organization. 2. The token was issued for the same organization as the resource. 3. The token includes the scope required for the operation (e.g., `system/Patient.rs` for reads — see [Scopes](/v1/scopes)). ## Organization identity Your organization's FHIR `Organization` resource is readable at: The `id` is the UUID visible in Practice Settings and returned as `managingOrganization.reference` on Patient resources. It is stable and does not change. ## Implications for integrations - **Single-organization integrations** (one API key, one organization): the organization model imposes no additional complexity. Every call works within your org. - **Multi-organization integrations** (a partner serving multiple Huli organizations): you need one API key per organization. There is no cross-organization token or admin-level key that spans organizations. This is by design — it constrains PHI access to explicitly authorized integrations per org. - **Audit**: every authenticated request writes an audit record (see [Rate Limiting](/v1/concepts/rate-limiting) for details). The audit record includes the `organization_id`, which is the basis for per-org compliance reporting. ======================================================================== # Pagination # URL: https://developers.huli.ai/v1/concepts/pagination # Cursor-based pagination on all FHIR list endpoints — how _count, _cursor, and the next link work. # Pagination All FHIR search endpoints return a `Bundle` of type `searchset`. Navigation through large result sets uses cursor-based pagination — not page numbers or offsets. ## Parameters | Parameter | Type | Default | Max | Notes | | --------- | ------- | ------- | --- | ------------------------------------------------------ | | `_count` | integer | 20 | 100 | Number of entries per page | | `_cursor` | string | — | — | Opaque cursor from the previous response's `next` link | Request the first page: ```bash curl "https://api.huli.ai/fhir/R4/Patient?_count=50" \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Accept: application/fhir+json" ``` ## Response structure ```json { "resourceType": "Bundle", "type": "searchset", "total": 847, "link": [ { "relation": "self", "url": "https://api.huli.ai/fhir/R4/Patient?_count=50" }, { "relation": "next", "url": "https://api.huli.ai/fhir/R4/Patient?_count=50&_cursor=eyJ0IjoiMjAyNi0wNi0wMVQxNDozMjowMC4wMDAtMDY6MDAiLCJpZCI6IjAxOTY1ZTJhLThjNGQtNzAwMC05MDAxLTAwMDAwMDAwMDAwMiJ9" } ], "entry": [...] } ``` - **`total`** — the total number of matching resources at the time the first page was queried. It is advisory. For large sets it may become inaccurate as records are added or updated while you paginate. Do not use `total` as a loop terminator. - **`link[relation=next]`** — present when there are more results. Absent on the last page. - **`link[relation=self]`** — the canonical URL for the current page. ## Iterating all pages Pass the full `next` URL as-is to retrieve the next batch. The cursor encodes a time+UUID position. Do not parse or construct cursor strings manually — the format may change. ```bash NEXT_URL="https://api.huli.ai/fhir/R4/Patient?_count=50&_cursor=eyJ0IjoiMj..." curl "$NEXT_URL" \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Accept: application/fhir+json" ``` Stop when the response has no `link` with `relation: "next"`. ## Stable iteration The cursor is time+UUID ordered. Pages are stable within a single pagination session — new records created after the first request will not appear in subsequent pages of that session, and deleted records will not cause gaps. This makes the API safe for full patient-list syncs that span multiple pages. ## Combining with filters Search parameters combine with pagination. All parameters carry forward in the `next` link — you do not need to re-specify them: ```bash curl "https://api.huli.ai/fhir/R4/Patient?name=Fernández&active=true&_count=25" \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Accept: application/fhir+json" ``` The `next` link for this response will include `name=Fernández&active=true&_count=25` alongside the cursor. ## Empty result sets When no resources match, the response is still a valid `Bundle`: ```json { "resourceType": "Bundle", "type": "searchset", "total": 0, "link": [ { "relation": "self", "url": "https://api.huli.ai/fhir/R4/Patient?name=DoesNotExist" } ], "entry": [] } ``` `entry` is an empty array. There is no `next` link. ======================================================================== # Rate Limiting # URL: https://developers.huli.ai/v1/concepts/rate-limiting # Per-key and per-organization rate limits, the Retry-After header, and the audit log written on every authenticated request. # Rate Limiting The Huli Public API enforces rate limits at two levels: per API key and per organization. Every authenticated request also writes an immutable audit record. ## Limits | Ceiling | Scope | Reset window | | ----------------------------------- | ---------------------------------------------- | ----------------------- | | Per-key request rate | Configurable per API key (default: 60 req/min) | 1-minute sliding window | | Per-org request rate | Shared across all keys in the organization | 1-minute sliding window | | Token issuance (`POST /auth/token`) | 20 requests/minute per source IP | 1-minute fixed window | The per-key default and the per-org ceiling are set when the API key is provisioned in Practice Settings. Contact your organization admin to review or raise the limits for your key. ## Response headers Every response includes rate-limit headers: ```http X-RateLimit-Limit: 60 X-RateLimit-Remaining: 47 X-RateLimit-Reset: 1748808720 ``` - `X-RateLimit-Limit` — the current limit for this key (requests per minute). - `X-RateLimit-Remaining` — requests remaining in the current window. - `X-RateLimit-Reset` — Unix timestamp (UTC) when the window resets. ## When the limit is exceeded with error code `HPB-00105`: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "throttled", "diagnostics": "HPB-00105: Rate limit exceeded" } ] } ``` The response also includes a `Retry-After` header with the number of seconds to wait: ```http Retry-After: 14 ``` Respect this header. Retry immediately after a `429` without the `Retry-After` delay results in another `429` and increases the reset time. ## Recommended retry strategy For transient failures (`429` and `5xx`): 1. Read `Retry-After` on `429` responses. Wait exactly that many seconds. 2. For `5xx` responses: exponential backoff starting at 1s, cap at 60s, jitter ±10%. 3. Do not retry `4xx` responses other than `429` — they indicate a request error that retrying will not fix. Never auto-retry `401`, `403`, or `404`. ## Audit log Every authenticated request to the API writes an audit record. This is a NOM-024 compliance requirement (Mexico), not an optional feature. Each record captures: - The FHIR resource type and resource ID involved (e.g., `Patient` and its UUID). - The API key identifier and the organization the request was made for. - The query parameters used (for search operations). - The number of resources returned. - The client IP address. - The request correlation ID — also returned in the `X-Correlation-Id` response header. - A timestamp (ISO-8601 with UTC offset). **Retention:** audit records are retained for 7 years. They are immutable once written. **Read access:** audit records are not accessible via the public API in v1. Access is available to authorized Huli personnel for compliance audits. **PHI in audit records:** the recorded search parameters may contain patient identifiers (e.g., `identifier=https://www.gob.mx/curp|FEME800614MDFRRR09`). Audit records are stored with the same access controls as the primary data. ======================================================================== # Webhooks # URL: https://developers.huli.ai/v1/concepts/webhooks # Outbound webhooks via the FHIR R4 Subscription resource — create a rest-hook subscription, verify HMAC-signed deliveries, and recover missed events with replay. # Webhooks Outbound webhooks let Huli push a notification to your endpoint whenever a resource changes — a new Appointment, a finalized Encounter, a lab Observation — instead of you polling the FHIR API. They are configured entirely through the standard FHIR R4 `Subscription` resource: there is no Huli-native webhook object to learn. The model is deliberately small. You register a `Subscription` that names a resource type and an HTTPS endpoint; Huli signs and POSTs a compact FHIR `Bundle` to that endpoint on every matching event; your receiver verifies the signature, dedupes on an event id, and GETs the full resource. Deliveries are at-least-once, retried on failure, and recoverable after an outage via `$replay`. ## Creating a subscription The request body is a FHIR `Subscription`. A minimal one: ```json { "resourceType": "Subscription", "status": "requested", "reason": "Sync finalized encounters into our EHR", "criteria": "Encounter", "channel": { "type": "rest-hook", "endpoint": "https://hooks.example.com/huli", "payload": "application/fhir+json", "header": ["X-Source-System: clinica-san-rafael"] } } ``` | Field | Required | Notes | | ------------------ | -------- | ------------------------------------------------------------------------------------------------------ | | `status` | yes | Send `requested`; the server activates it and returns `active`. | | `reason` | yes | Non-empty free text describing the subscription. Not persisted — for your own audit trail on the call. | | `criteria` | yes | The resource **type** to notify on, e.g. `"Encounter"`. Resource-type only in v2 (see below). | | `channel.type` | yes | Must be `"rest-hook"`. No other channel type is supported. | | `channel.endpoint` | yes | HTTPS-only target URL. SSRF-checked at dial time — private, link-local, and metadata IPs are refused. | | `channel.payload` | yes | Must be `"application/fhir+json"`. | | `channel.header` | no | Extra HTTP headers to send on every delivery, each as a `"Name: Value"` string. | `criteria` is the resource type only. A FHIR query string like `Encounter?status=finished` is **rejected** in v2 — filter on the event type in your receiver instead (every delivery carries an `X-Huli-Event-Type` header). ### The signing secret is returned exactly once A successful create returns with the stored `Subscription` **plus the signing secret in an extension**, and a `Cache-Control: no-store` response header. The secret is shown this one time and is never returned again by any subsequent read. Capture the signing secret from the `201` response immediately and store it in your secrets manager. There is no endpoint that re-reveals it — losing it means deleting the subscription and creating a new one. The response also carries a non-blocking Business Associate Agreement (BAA) reminder as a `contained` `OperationOutcome` with `severity: information`. It does not affect the `201` — it is informational only. ### Credential requirements Creating a subscription requires a **machine (API key) credential** carrying the system/Subscription scope family, and that credential must have been minted with a BAA attestation. Interactive user tokens cannot create subscriptions. You can only subscribe to what you can read: creating a subscription (or retargeting its `criteria` with a PUT) also requires a **read grant on the criteria resource type** — for example, `criteria: "Encounter"` needs system/Encounter.rs on the same credential. A create or update without it is refused with `403 HPB-00104`. ## Lifecycle A subscription moves through four states: | `status` | Meaning | | ----------- | ----------------------------------------------------------------------------------------------- | | `requested` | The state you POST. The server transitions it to `active`. | | `active` | Deliveries flow. This is the only state that receives events (and the only state that replays). | | `error` | Auto-paused by the circuit breaker after repeated delivery failures. Reactivate with a PUT. | | `off` | Revoked (deleted). No further deliveries. | Manage the subscription with the standard FHIR interactions: To resume an auto-paused subscription, `PUT` it back with `status: "active"` once your endpoint is healthy again. ## Scopes | Scope | Grants | | --------------------------------------- | ------------------------------------------------------------------------------ | | system/Subscription.crud | Create, update, and delete subscriptions. | | system/Subscription.rs | Read and search subscriptions, plus the `$stats` and `$deliveries` operations. | Replay (`$replay`) mutates delivery state and requires system/Subscription.crud. Create and update additionally require a read scope (`.rs`) on the criteria resource type — see "Credential requirements" above. ## Events In v2, lifecycle events are emitted for these resource types: | Resource | Events | | ----------------- | --------------------------- | | Appointment | created, updated, cancelled | | Encounter | created, updated, finalized | | Patient | created, updated | | Observation | created | | MedicationRequest | created, updated, cancelled | | ServiceRequest | created, updated, completed | The event-type string combines the resource and the transition, e.g. `Encounter.finished`. It arrives on every delivery in the `X-Huli-Event-Type` header, so you can route or filter without parsing the body. ## The delivery payload Each delivery is an HTTP `POST` of a FHIR `Bundle` of `type: "history"` with a single `entry`. The entry's `request.method` encodes the change: - `POST` — the resource was **created** - `PUT` — the resource was **updated** - `DELETE` — the resource was **deleted or cancelled** Deliveries are **id-level notifications**, not full snapshots. A create/update carries only a minimal stub — `{ "resourceType": ..., "id": ... }` — and a delete carries only the reference. Your receiver then GETs the full, current resource from the FHIR API. This keeps payloads small and avoids shipping stale copies of PHI. ```json { "resourceType": "Bundle", "type": "history", "entry": [ { "resource": { "resourceType": "Encounter", "id": "01965e2a-8c4d-7000-9001-000000000042" }, "request": { "method": "PUT", "url": "Encounter/01965e2a-8c4d-7000-9001-000000000042" } } ] } ``` ## Headers on every delivery Every delivery POST carries these headers: | Header | Value | | -------------------- | -------------------------------------------------------------------------------------- | | `X-Huli-Signature` | `sha256=` + lowercase hex of `HMAC-SHA256(signing_secret, raw_request_body)`. | | `X-Huli-Event-Id` | Stable per event **across retries and replays**. Dedupe on this. | | `X-Huli-Delivery-Id` | Fresh per attempt — a retry or replay gets a new one. Use it to correlate one attempt. | | `X-Huli-Event-Type` | The event type, e.g. `Encounter.finished`. | | `X-Huli-Occurred-At` | RFC3339 timestamp of the source event. | | `X-Huli-Replay` | `true` only on replay deliveries. Absent otherwise. | | `Content-Type` | `application/fhir+json`. | Huli-owned headers always win. If a `channel.header[]` entry collides with any of the `X-Huli-*` headers or `Content-Type`, the Huli value is sent — your custom header is only honoured for names Huli does not set. ## Verifying the signature Compute `HMAC-SHA256(signing_secret, rawBody)`, hex-encode it (lowercase), prefix `sha256=`, and constant-time-compare the result against the `X-Huli-Signature` header. Verify against the **raw received body bytes**. Do not parse and re-serialize the JSON first — any whitespace or key-ordering change alters the bytes and the HMAC will not match. Read the body as raw bytes before your JSON framework touches it. :::CodeGroup ```javascript import crypto from 'node:crypto'; // `rawBody` MUST be the exact bytes received (a Buffer/string), not a re-serialized object. function verifyHuliSignature(rawBody, signatureHeader, secret) { const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(signatureHeader ?? ''); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` ```python import hashlib import hmac def verify_huli_signature(raw_body: bytes, signature_header: str, secret: str) -> bool: # raw_body MUST be the exact bytes received, not a re-serialized dict. digest = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() expected = f"sha256={digest}" return hmac.compare_digest(expected, signature_header or "") ``` ::: Reject any delivery whose signature does not verify — respond with a `4xx` and drop it. ## Delivery semantics - **At-least-once.** The same event may arrive more than once. Dedupe on `X-Huli-Event-Id`, which is stable across retries and replays. - **HTTPS only.** Endpoints must be `https://`. The target is SSRF-checked at dial time. - **Success is `2xx`.** Any `2xx` response marks the delivery delivered. - **Retries with backoff.** `5xx` responses and network errors are retried with exponential backoff, up to 5 attempts. - **Dead-letter.** A delivery that exhausts its attempts moves to a dead-letter state (visible in `$deliveries` and counted in `$stats`). - **Auto-pause.** After 100 consecutive failures the subscription is auto-paused (`status: error`). Reactivate it with a `PUT` setting `status` back to `active`. - **Rate-limited per subscription.** Deliveries to a single subscription are throttled so a large backlog cannot flood your endpoint. - **Redirects are refused.** A `3xx` response is **not** followed; it counts as a failure. ## Replay — recovering from an outage If your endpoint was down, re-enqueue the events you missed instead of losing them. The body is a FHIR `Parameters` with a `from` (`valueInstant`, required) and an optional `to` (`valueInstant`, defaults to now). You may also pass `from`/`to` as RFC3339 query parameters. ```json { "resourceType": "Parameters", "parameter": [ { "name": "from", "valueInstant": "2026-07-01T00:00:00Z" }, { "name": "to", "valueInstant": "2026-07-02T00:00:00Z" } ] } ``` Replay re-scans your organization's event history for the subscription's **criteria** (resource type) over the window and re-enqueues each matching event. This includes events that occurred **before the subscription was created** and events that were **already delivered** — replay is a re-scan of the outbox by criteria + window, so treat it as a backfill, not only a "redeliver what I missed" operation. Your event-id dedup (below) absorbs any overlap. Constraints: - The window is clamped to the **30-day retention horizon** — events older than that are gone. - A single call re-enqueues **at most 500 events**. For a larger backlog, just call again — the same window is fine: events with an in-flight replay are skipped, so each call advances to the next-older events. Repeat until `truncated` comes back `false`. - Only an **`active`** subscription may replay. - Repeated calls over the same window are **safe**: an event that already has an in-flight (undelivered) replay for this subscription is skipped, so you won't pile duplicates. Replayed deliveries carry `X-Huli-Replay: true` and the **original** `X-Huli-Event-Id`, so your existing dedupe logic transparently absorbs any overlap with events that did get through. The call returns a `Parameters` summary: ```json { "resourceType": "Parameters", "parameter": [ { "name": "deliveriesQueued", "valueInteger": 87 }, { "name": "truncated", "valueBoolean": false } ] } ``` ## Observability ### Metrics — `$stats` Returns a `Parameters` with aggregate delivery health: | Parameter | Meaning | | ------------------- | -------------------------------------------------- | | `delivered` | Deliveries that succeeded. | | `failed` | Deliveries that failed (all attempts counted). | | `pending` | Deliveries queued but not yet terminal. | | `dead` | Deliveries that exhausted retries (dead-lettered). | | `deadLetterDepth` | Current depth of the dead-letter backlog. | | `totalAttempts` | Total delivery attempts, including retries. | | `successRate` | Fraction of deliveries that succeeded. | | `latencyP50Seconds` | Median delivery latency, seconds. | | `latencyP95Seconds` | 95th-percentile delivery latency, seconds. | ### Delivery trail — `$deliveries` Returns a `Parameters` with one `delivery` group per recent attempt. Each group carries: `id`, `status`, `attempts`, `replay`, `event`, `resourceType`, `eventType`, `occurredAt`, `queuedAt`, and — when set — `lastStatusCode` and `deliveredAt`. `$deliveries` is a delivery **ledger**, not a payload store. It never exposes the delivery body, your endpoint URL, or the signing secret — only the metadata needed to debug delivery health. ======================================================================== # Error Codes # URL: https://developers.huli.ai/v1/errors # Complete reference for all error codes returned by the Huli Public FHIR API. # Error Codes All error responses use the FHIR [OperationOutcome](https://hl7.org/fhir/R4/operationoutcome.html) format. Each `issue` entry has `severity`, `code` (FHIR IssueType), and `diagnostics`; errors carrying a Huli catalog code also emit it as structured `details.coding` (`system` + `code`, e.g. `"HPB-00101"`). **Branch on the HTTP status and `issue[0].code`** — those are always present and authoritative. Catalog-coded errors additionally prefix `diagnostics` with the Huli code, in the form `"HPB-XXXXX: "`, so you can extract the code by splitting on `": "`. Treat the prefix and `details.coding` as **best-effort / when-present**: some FHIR handlers write an `OperationOutcome` directly with a plain-text `diagnostics`, no HPB prefix, and no `details`, so do not assume every response carries them. > This page is generated from the canonical Huli Public API error catalog — do not edit it directly. ## OperationOutcome format ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "diagnostics": "HPB-00101: Validation error" } ] } ``` ## HPB errors (Public FHIR API) | Code | HTTP Status | Message | |------|-------------|---------| | [HPB-00101](#hpb-00101) | 400 | Validation error | | [HPB-00102](#hpb-00102) | 404 | Resource not found | | [HPB-00103](#hpb-00103) | 409 | Version conflict | | [HPB-00104](#hpb-00104) | 403 | Insufficient scope | | [HPB-00105](#hpb-00105) | 429 | Rate limit exceeded | | [HPB-00106](#hpb-00106) | 401 | Authentication failed | | [HPB-00107](#hpb-00107) | 401 | Authentication failed | | [HPB-00108](#hpb-00108) | 409 | Patient has been merged into another record and cannot accept new clinical data | | [HPB-00109](#hpb-00109) | 409 | Patient is marked as deceased and cannot accept new clinical data | | [HPB-00110](#hpb-00110) | 401 | Inbound webhook authentication failed | | [HPB-00122](#hpb-00122) | 413 | Inbound webhook body exceeds the maximum allowed size | | [HPB-00111](#hpb-00111) | 404 | MedicationRequest not found | | [HPB-00112](#hpb-00112) | 404 | ServiceRequest not found | | [HPB-00113](#hpb-00113) | 400 | ServiceRequest must contain at least one item | | [HPB-00114](#hpb-00114) | 409 | Signed or cancelled prescription cannot be modified | | [HPB-00115](#hpb-00115) | 422 | This service offers multiple specialties; a specialty must be selected to book | | [HPB-00116](#hpb-00116) | 422 | The selected specialty is not offered by this service | | [HPB-00117](#hpb-00117) | 404 | Composition not found | | [HPB-00118](#hpb-00118) | 404 | DocumentReference not found | | [HPB-00119](#hpb-00119) | 413 | Document exceeds the maximum allowed size | | [HPB-00120](#hpb-00120) | 400 | Document content is invalid or its declared type does not match | | [HPB-00121](#hpb-00121) | 429 | Sandbox key volume cap exceeded | | [HPB-00123](#hpb-00123) | 409 | A finalized clinical note cannot be voided | | [HPB-00124](#hpb-00124) | 403 | Production credentials cannot be used from a browser origin; use a sandbox key | | [HPB-00135](#hpb-00135) | 409 | Signed or cancelled service order cannot be modified | | [HPB-00136](#hpb-00136) | 409 | Multi-item service orders are read-only on this surface | | [HPB-00137](#hpb-00137) | 422 | Appointment status transition is not allowed | | [HPB-00138](#hpb-00138) | 409 | Appointment already has an active encounter | | [HPB-00139](#hpb-00139) | 422 | Encounter date is outside the allowed registration window | | [HPB-00140](#hpb-00140) | 422 | Cannot modify a medication request in entered-in-error | | [HPB-00141](#hpb-00141) | 403 | Select a normativa-configured clinic before starting a consultation | | [HPB-00142](#hpb-00142) | 422 | Complete the patient's required normativa data before starting a consultation | | [HPB-00143](#hpb-00143) | 422 | Cannot update an observation in entered-in-error | | [HPB-00144](#hpb-00144) | 422 | Observation.subject cannot be changed on update | | [HPB-00145](#hpb-00145) | 400 | Invalid CURP format | | [HPB-00146](#hpb-00146) | 400 | CURP does not match the entered data | | [HPB-00147](#hpb-00147) | 422 | Mexican organizations must select every place from the Mexican national locality catalog instead of sending free-typed place values | | [HPB-00148](#hpb-00148) | 403 | Organization is blocked | | [HPB-00149](#hpb-00149) | 422 | Surname \ | ### HPB-00101 **HTTP 400** — Validation error **Recovery:** Check the request body against the [FHIR R4 resource schema](https://hl7.org/fhir/R4/). Ensure `resourceType` is present and all required fields are provided. ### HPB-00102 **HTTP 404** — Resource not found **Recovery:** The resource ID does not exist in your organization. Verify the UUID is correct and belongs to your organization. ### HPB-00103 **HTTP 409** — Version conflict **Recovery:** Version conflict on update. Re-fetch the resource, apply your changes, and retry. Include the current `meta.versionId` in your request. ### HPB-00104 **HTTP 403** — Insufficient scope **Recovery:** Your access token does not include the required scope for this operation. Request a new token with the correct scope — see [Scopes](/v1/scopes). ### HPB-00105 **HTTP 429** — Rate limit exceeded **Recovery:** You have exceeded a rate limit. Token issuance is capped at 20 requests per minute per IP on `/auth/token`; resource requests use a separate per-key ceiling (default 60 requests per minute). Read the `Retry-After` header for the exact delay, then retry. ### HPB-00106 **HTTP 401** — Authentication failed **Recovery:** Authentication failed. Verify your client assertion JWT: check `iss`, `sub`, `aud`, `exp`, and signature. Ensure your JWKS endpoint is reachable. ### HPB-00107 **HTTP 401** — Authentication failed **Recovery:** Your access token has expired (5-minute TTL). Request a new token via `POST /auth/token`. ### HPB-00108 **HTTP 409** — Patient has been merged into another record and cannot accept new clinical data ### HPB-00109 **HTTP 409** — Patient is marked as deceased and cannot accept new clinical data ### HPB-00110 **HTTP 401** — Inbound webhook authentication failed ### HPB-00122 **HTTP 413** — Inbound webhook body exceeds the maximum allowed size ### HPB-00111 **HTTP 404** — MedicationRequest not found ### HPB-00112 **HTTP 404** — ServiceRequest not found ### HPB-00113 **HTTP 400** — ServiceRequest must contain at least one item ### HPB-00114 **HTTP 409** — Signed or cancelled prescription cannot be modified ### HPB-00115 **HTTP 422** — This service offers multiple specialties; a specialty must be selected to book ### HPB-00116 **HTTP 422** — The selected specialty is not offered by this service ### HPB-00117 **HTTP 404** — Composition not found ### HPB-00118 **HTTP 404** — DocumentReference not found ### HPB-00119 **HTTP 413** — Document exceeds the maximum allowed size ### HPB-00120 **HTTP 400** — Document content is invalid or its declared type does not match ### HPB-00121 **HTTP 429** — Sandbox key volume cap exceeded ### HPB-00123 **HTTP 409** — A finalized clinical note cannot be voided ### HPB-00124 **HTTP 403** — Production credentials cannot be used from a browser origin; use a sandbox key ### HPB-00135 **HTTP 409** — Signed or cancelled service order cannot be modified ### HPB-00136 **HTTP 409** — Multi-item service orders are read-only on this surface ### HPB-00137 **HTTP 422** — Appointment status transition is not allowed ### HPB-00138 **HTTP 409** — Appointment already has an active encounter ### HPB-00139 **HTTP 422** — Encounter date is outside the allowed registration window ### HPB-00140 **HTTP 422** — Cannot modify a medication request in entered-in-error ### HPB-00141 **HTTP 403** — Select a normativa-configured clinic before starting a consultation ### HPB-00142 **HTTP 422** — Complete the patient's required normativa data before starting a consultation ### HPB-00143 **HTTP 422** — Cannot update an observation in entered-in-error ### HPB-00144 **HTTP 422** — Observation.subject cannot be changed on update ### HPB-00145 **HTTP 400** — Invalid CURP format ### HPB-00146 **HTTP 400** — CURP does not match the entered data ### HPB-00147 **HTTP 422** — Mexican organizations must select every place from the Mexican national locality catalog instead of sending free-typed place values ### HPB-00148 **HTTP 403** — Organization is blocked ### HPB-00149 **HTTP 422** — Surname \ ## HULI errors (generic) These generic error codes are shared across all Huli APIs and may appear in responses when a common platform-level condition is triggered. | Code | HTTP Status | Message | |------|-------------|---------| | [HULI-00001](#huli-00001) | 500 | Internal server error | | [HULI-00002](#huli-00002) | 404 | Resource not found | | [HULI-00003](#huli-00003) | 400 | Bad request | | [HULI-00004](#huli-00004) | 401 | Unauthorized | | [HULI-00005](#huli-00005) | 403 | Forbidden | | [HULI-00007](#huli-00007) | 503 | Service unavailable | ### HULI-00001 **HTTP 500** — Internal server error **Recovery:** An unexpected server error occurred. Retry with exponential backoff. If the problem persists, contact support with the request ID from the response. ### HULI-00002 **HTTP 404** — Resource not found ### HULI-00003 **HTTP 400** — Bad request ### HULI-00004 **HTTP 401** — Unauthorized **Recovery:** No valid Bearer token was provided. Include `Authorization: Bearer ` on your request. ### HULI-00005 **HTTP 403** — Forbidden ### HULI-00007 **HTTP 503** — Service unavailable **Recovery:** The service is temporarily unavailable. Retry after a short delay. ======================================================================== # Recipes # URL: https://developers.huli.ai/v1/recipes # End-to-end integration workflows for the Huli Public API — each one ships a working result against the v1 FHIR R4 surface. # Recipes Opinionated, end-to-end workflows. Each recipe ships a working result against the v1 surface — bearer auth, the four read-write FHIR R4 resources (Patient, Appointment, Encounter, Observation), and the `huli` CLI. Code samples are shown in cURL, TypeScript, Python, Java, and Go; every one runs as-is. ## What do you want to build? ## v1 recipes - **[Sandbox quickstart](/v1/recipes/sandbox-quickstart)** — get a sandbox organization pre-seeded with fake FHIR data from a Huli org admin, receive the bearer credential through a one-time share link, and make your first call. Five minutes from link to a `200`. - **[Run your first authenticated Patient search](/v1/recipes/getting-started-patient-search)** — bearer token + `system/Patient.rs`, the canonical first request. Five minutes from key to `searchset`. - **[Registering a patient](/v1/recipes/registering-a-patient)** — discover the NOM-024 / MX address codes via the terminology ValueSets, then `POST` a Patient with CURP/RFC identifiers and the second-lastname extension using `system/Patient.cru`. - **[Booking an appointment end-to-end](/v1/recipes/booking-an-appointment)** — discover a service, practitioner, room, and free slot with `system/Appointment.cru` + `system/Practitioner.rs`, then `POST` the Appointment past its booking preconditions — including the `specialty` selection a multi-specialty service requires. - **[Scheduling an administrative meeting](/v1/recipes/scheduling-an-administrative-meeting)** — book an internal meeting with no patient: a required title, optional all-day flag, and external email invitees, against a service whose appointment type is `administrative`. - **[Creating a clinical encounter](/v1/recipes/creating-an-encounter)** — discover the practitioner participant, then `POST` an Encounter for an existing patient with `system/Encounter.cru` + `system/Patient.rs` + `system/Practitioner.rs`. - **[Writing and amending a clinical note](/v1/recipes/writing-a-clinical-note)** — create the LOINC-sectioned `Composition` projection of a visit and amend it with `If-Match` optimistic concurrency, using the BAA-gated `system/Composition.cru`. - **[Uploading a document](/v1/recipes/uploading-a-document)** — attach a PDF, scan, or image as a `DocumentReference` (multipart `$upload` or inline base64) and read it back via a 30-minute signed URL, using the BAA-gated `system/DocumentReference.cru`. - **[Fetching a patient's full record](/v1/recipes/fetching-a-patient-record)** — pull a patient's encounters, observations, notes, documents, medications, and orders in one scope-filtered `Patient/$everything` Bundle. - **[Wire a read-only partner](/v1/recipes/wiring-a-read-only-partner)** — consume Appointment + Encounter read-only with `system/Appointment.rs` + `system/Encounter.rs`, resolving the Practitioner/Organization references they point at. - **[Sync a daily patient list with the huli CLI](/v1/recipes/daily-roster-sync-cli)** — cron-safe, restart-idempotent Patient + Appointment pagination driven by the CLI. - **[Send lab results to the chart](/v1/recipes/posting-lab-observations-lis)** — create Observations with required LOINC codes and UCUM units, with reference-range handling. - **[Creating and sharing an API key as a clinic admin](/v1/recipes/creating-and-sharing-an-api-key)** — the Practice Settings flow to mint, scope, reveal, share, and rotate an admin bearer key. - **[Choosing a CLI authentication path](/v1/recipes/cli-authentication-paths)** — interactive OAuth vs M2M (`huli auth setup`) vs a one-off bearer token; when to use which. - **[Debugging a failed FHIR search](/v1/recipes/debugging-a-failed-fhir-search)** — read the `OperationOutcome`, map the common failures, and trace a correlation id to its audit record. - **[Receive webhooks](/v1/recipes/receiving-webhooks)** — register a `rest-hook` Subscription with `system/Subscription.crud`, verify the HMAC signature on every delivery, dedupe on the event id, and recover missed events with `$replay`. ======================================================================== # Booking an appointment end-to-end # URL: https://developers.huli.ai/v1/recipes/booking-an-appointment # Discover a bookable service, a practitioner and their room, a free slot, then POST a FHIR R4 Appointment — with the booking preconditions the discovery steps exist to satisfy. # Booking an appointment end-to-end Turn a "who is free, and for what?" question into a stored `Appointment`. You will discover a bookable service, find a practitioner and the room they work in, locate a free slot, and `POST` the booking — assembling exactly the four references the create call needs to pass its preconditions. Two scopes carry the whole flow: system/Appointment.cru for the write (and the discovery resources gated behind it) and system/Practitioner.rs for the practitioner wiring. A blind `POST /fhir/R4/Appointment` rarely succeeds on the first try: the server rejects a booking whose `serviceType` is unknown, whose practitioner carries no location, or whose room sits outside the practitioner's assigned rooms. The discovery steps below exist precisely to hand you values that satisfy each of those checks, so the final write goes through. ## Audience You build a patient-booking flow — a portal, a referral intake, or a front-desk tool — and you have already run [your first authenticated search](/v1/recipes/getting-started-patient-search). You read a `Bundle` without a viewer, you know what a FHIR reference is, and you want to take a booking from discovery to a `201 Created`. ## You'll need - A bearer token from HuliPractice (**Practice Settings → Integrations → API Keys**), or a SMART Backend Services access token. See [Bearer Tokens](/v1/auth/bearer) for provisioning and [`POST /auth/token`](/v1/auth) for the token exchange — this recipe assumes you already hold one. - These two scopes on that token: - — create `Appointment` (`.cru` also grants read + search). The discovery resources `HealthcareService`, `Location`, `Schedule`, and `Slot` are gated behind the Appointment scope, so this one grant covers them. - — read + search `Practitioner` and `PractitionerRole`. - `curl`, or Node, Python, Java, or Go if you prefer a language client. The discovery resources page on offset pagination (`_count` + `_offset`), not the keyset `_cursor` that Patient and Appointment search use. `_count` defaults to 20 and caps at 100; walk pages by adding `_offset` in multiples of `_count`. The `Slot` and `Schedule` searches follow the same offset rule. ## End state You hold a `201 Created` whose body is the stored `Appointment` — booked for a real practitioner, in a room that practitioner actually works in, at a slot that was free, for a service the organization offers. Along the way you have the four values the create call consumed: the `org-service` serviceType coding, the `Practitioner` reference, the room `Location` reference, and the slot's `start`/`end`. ## Steps ### 1. Export the token ```bash export HULI_TOKEN="" ``` ### 2. Discover a bookable service Each org service-catalog entry is one `HealthcareService`. The value you need is the entry's `type.coding` — its `code` is the org-service UUID, published under the `org-service` CodeSystem. **That coding is exactly what you put in `Appointment.serviceType`** when you book; copy it verbatim, do not rebuild it from the name. :::CodeGroup ```bash curl "https://api.huli.ai/fhir/R4/HealthcareService?_count=50" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` ```typescript const resp = await fetch('https://api.huli.ai/fhir/R4/HealthcareService?_count=50', { headers: { Authorization: `Bearer ${process.env.HULI_TOKEN}`, Accept: 'application/fhir+json', }, }); const bundle = await resp.json(); // The serviceType coding you book with is entry.resource.type[0].coding[0]. const service = bundle.entry?.[0]?.resource; const serviceType = service?.type?.[0]; console.log(JSON.stringify(serviceType, null, 2)); ``` ```python import os import requests resp = requests.get( "https://api.huli.ai/fhir/R4/HealthcareService", params={"_count": 50}, headers={ "Authorization": f"Bearer {os.environ['HULI_TOKEN']}", "Accept": "application/fhir+json", }, timeout=30, ) bundle = resp.json() # The serviceType coding you book with is entry.resource.type[0]. service = bundle["entry"][0]["resource"] service_type = service["type"][0] print(service_type) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class DiscoverService { public static void main(String[] args) throws Exception { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/HealthcareService?_count=50")) .header("Authorization", "Bearer " + System.getenv("HULI_TOKEN")) .header("Accept", "application/fhir+json") .GET() .build(); HttpResponse response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); // entry[].resource.type[0].coding[0] carries the org-service code you // place in Appointment.serviceType. Parse with a JSON library in real code. System.out.println(response.body()); } } ``` ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { req, err := http.NewRequest(http.MethodGet, "https://api.huli.ai/fhir/R4/HealthcareService?_count=50", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("HULI_TOKEN")) req.Header.Set("Accept", "application/fhir+json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { panic(err) } // entry[].resource.type[0].coding[0] holds the org-service serviceType code. fmt.Printf("%s\n", body) } ``` ::: A representative `HealthcareService` entry inside the `searchset`: ```json { "resourceType": "HealthcareService", "id": "01965e2a-8c4d-7000-9010-0000000000f1", "active": true, "name": "Consulta general", "type": [ { "coding": [ { "system": "https://fhir.huli.ai/r4/CodeSystem/org-service", "code": "01965e2a-8c4d-7000-9010-0000000000f1", "display": "Consulta general" } ], "text": "Consulta general" } ], "specialty": [ { "coding": [ { "system": "https://fhir.huli.ai/r4/CodeSystem/specialty", "code": "01965e2a-8c4d-7000-9011-0000000000d1", "display": "Medicina general" }, { "system": "http://snomed.info/sct", "code": "394814009", "display": "General practice" } ], "text": "Medicina general" }, { "coding": [ { "system": "https://fhir.huli.ai/r4/CodeSystem/specialty", "code": "01965e2a-8c4d-7000-9011-0000000000d2", "display": "Pediatría" }, { "system": "http://snomed.info/sct", "code": "394537008", "display": "Pediatrics" } ], "text": "Pediatría" } ], "providedBy": { "reference": "Organization/01965e2a-8c4d-7000-9001-0000000000b0" }, "location": [ { "reference": "Location/01965e2a-8c4d-7000-9020-0000000000a1", "display": "Consultorio 1" } ] } ``` Two fields beyond `type` matter for booking. `specialty[]` lists **every** specialty the service is offered for — one `CodeableConcept` per specialty. Each one carries a **bookable** coding under the `specialty` CodeSystem (`system` = `https://fhir.huli.ai/r4/CodeSystem/specialty`, `code` = the specialty UUID) plus, when the catalog has one, a human SNOMED coding. The example above offers two specialties (general medicine and pediatrics), so it is a **multi-specialty service**: when you book it you MUST pick one and send its `specialty` coding verbatim in `Appointment.specialty` (step 6) — omitting the selection is rejected (see **What can go wrong**). A service with a single specialty, or an empty `specialty[]` (offered for all specialties), derives the specialty server-side and needs no selection. Separately, every practitioner you book must carry the chosen specialty in their `PractitionerRole.specialty`. The `location[]` array lists the rooms the service is offered in — useful coverage context, but the _authoritative_ room set for the booking is the practitioner's, which you read next. **Shortcut — let the service hand you the valid practitioner/room pairs.** Rather than guessing which practitioner works in which room, search `Schedule` by the service you just discovered: every schedule configured to deliver that service binds a practitioner (its `PractitionerRole` actor) to the room they serve it in (its `Location` actor). Each returned schedule is therefore a practitioner/room **combination the booking will accept** — pick one and you sidestep the "practitioner has no location" and "room outside the practitioner's rooms" rejections. ```bash curl "https://api.huli.ai/fhir/R4/Schedule?service-type=01965e2a-8c4d-7000-9010-0000000000f1" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` The `service-type` value is the org-service `code` from `HealthcareService.type[0].coding[0]` (step 2) — `service-type` is the FHIR-standard Schedule search parameter that targets `Schedule.serviceType`. Each entry's `Schedule.actor` carries a `PractitionerRole/` and a `Location/`; lift that pair and jump straight to step 5 to find a free `Slot` for the schedule. A schedule with an empty service set serves every service and is returned for any `service-type` query that names a live org service (an unknown service id returns an empty bundle). This axis needs only `system/Appointment.rs`; steps 3–4 below are the longer practitioner-first path (they also read `Practitioner`/`PractitionerRole`, so they additionally need `system/Practitioner.rs`). Use whichever fits your flow. ### 3. Find a practitioner and their wiring Search for the practitioner, then read their `PractitionerRole` — that role names the rooms the practitioner works in and the specialties they carry. ```bash curl "https://api.huli.ai/fhir/R4/Practitioner?name=Fern%C3%A1ndez" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` The Run button above searches without a name filter (`_count=1`) so it resolves to *some* practitioner in your sandbox rather than requiring one literally named "Fernández" — swap in `?name=…` once you know who you're booking. With the practitioner's id in hand, read their role wiring: ```bash curl "https://api.huli.ai/fhir/R4/PractitionerRole?practitioner=01965e2a-8c4d-7000-9001-0000000000c1" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` A `PractitionerRole` resource: ```json { "resourceType": "PractitionerRole", "id": "01965e2a-8c4d-7000-9030-0000000000b1", "active": true, "practitioner": { "reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1" }, "organization": { "reference": "Organization/01965e2a-8c4d-7000-9001-0000000000b0" }, "location": [ { "reference": "Location/01965e2a-8c4d-7000-9020-0000000000a1", "display": "Consultorio 1" } ], "specialty": [ { "coding": [ { "system": "http://snomed.info/sct", "code": "394814009", "display": "General practice" } ], "text": "Medicina general" } ] } ``` Three things to lift from the role: the `location[]` (the rooms this practitioner works in — pick one in step 4), the `specialty[]` (it must include the service's specialty if step 2 carried one), and the `PractitionerRole.id` — that id is the schedulable resource you query slots against in step 5. **Book against the `PractitionerRole.id`, not the `Practitioner` it points at.** When you POST the Appointment (step 6), `participant[].actor.reference` carries the **`PractitionerRole.id`** you just discovered, under a `Practitioner/` reference. The practitioner-user id that `PractitionerRole.practitioner` references is **not** a schedulable resource; booking against it returns `404`. The reference *type* is `Practitioner` (FHIR conformance), but the *id-space* is the practitioner-role / schedulable resource — the same id `Schedule` and `Slot` reference. ### 4. Pick a room Pick a room from `PractitionerRole.location`, and read it back to confirm it is active. ```bash curl "https://api.huli.ai/fhir/R4/Location/01965e2a-8c4d-7000-9020-0000000000a1" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` ```json { "resourceType": "Location", "id": "01965e2a-8c4d-7000-9020-0000000000a1", "status": "active", "name": "Consultorio 1", "managingOrganization": { "reference": "Organization/01965e2a-8c4d-7000-9001-0000000000b0" } } ``` Because this room came from the practitioner's own `PractitionerRole.location`, it is guaranteed to be inside their assigned-location set — which is the set the booking guard checks. A room taken only from `HealthcareService.location` is not guaranteed to be, and can be rejected at write time. ### 5. Find a free slot Slots are computed on the fly from the schedulable resource's schedules minus its booked appointments, so every slot you get back has `status: "free"`. Search either by `schedule` or directly by `actor` (the `PractitionerRole.id` from step 3). The `start`/`end` window is capped at 31 days. To find the schedule first: Or skip straight to slots by actor: :::CodeGroup ```bash curl "https://api.huli.ai/fhir/R4/Slot?actor=01965e2a-8c4d-7000-9030-0000000000b1" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` ```typescript const params = new URLSearchParams(); params.set('actor', '01965e2a-8c4d-7000-9030-0000000000b1'); const resp = await fetch(`https://api.huli.ai/fhir/R4/Slot?${params}`, { headers: { Authorization: `Bearer ${process.env.HULI_TOKEN}`, Accept: 'application/fhir+json', }, }); const bundle = await resp.json(); const slot = bundle.entry?.[0]?.resource; // Carry slot.start and slot.end into the Appointment you POST next. console.log(slot?.start, slot?.end); ``` ```python import os import requests resp = requests.get( "https://api.huli.ai/fhir/R4/Slot", params={ "actor": "01965e2a-8c4d-7000-9030-0000000000b1", }, headers={ "Authorization": f"Bearer {os.environ['HULI_TOKEN']}", "Accept": "application/fhir+json", }, timeout=30, ) bundle = resp.json() slot = bundle["entry"][0]["resource"] # Carry slot["start"] and slot["end"] into the Appointment you POST next. print(slot["start"], slot["end"]) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class FindSlot { public static void main(String[] args) throws Exception { String query = "actor=01965e2a-8c4d-7000-9030-0000000000b1"; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Slot?" + query)) .header("Authorization", "Bearer " + System.getenv("HULI_TOKEN")) .header("Accept", "application/fhir+json") .GET() .build(); HttpResponse response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); // entry[0].resource.start / .end feed the Appointment you POST next. System.out.println(response.body()); } } ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" ) func main() { endpoint, err := url.Parse("https://api.huli.ai/fhir/R4/Slot") if err != nil { panic(err) } q := endpoint.Query() q.Set("actor", "01965e2a-8c4d-7000-9030-0000000000b1") endpoint.RawQuery = q.Encode() req, err := http.NewRequest(http.MethodGet, endpoint.String(), nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("HULI_TOKEN")) req.Header.Set("Accept", "application/fhir+json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { panic(err) } // entry[0].resource.start / .end feed the Appointment you POST next. fmt.Printf("%s\n", body) } ``` ::: The Run button above sends no `start`/`end`, so the API applies its default window — now through the next 7 days — which is what most integrations want. Every sandbox practitioner is seeded with a Mon–Fri 09:00–17:00 schedule valid for a year, so a fresh sandbox always returns free slots here. If you pass explicit dates instead, keep the span at 31 days or less (the cap above) and in the schedule's validity window. A `Slot` inside the `searchset`: ```json { "resourceType": "Slot", "id": "01965e2a-8c4d-7000-9040-0000000000c2", "schedule": { "reference": "Schedule/01965e2a-8c4d-7000-9035-0000000000d3" }, "status": "free", "start": "2026-06-17T09:00:00.000-06:00", "end": "2026-06-17T09:30:00.000-06:00" } ``` Carry that slot's `start` and `end` into the booking. ### 6. POST the Appointment Assemble the four discovered values into the create body: the `serviceType` coding from step 2 (verbatim), the slot's `start`/`end` from step 5, and a `participant` array naming the practitioner and the room `Location`. Add the patient participant for a patient-facing booking. The body below is the **comprehensive** form — every field the create decoder honors on an Appointment write. Required fields are flagged inline. The **Full field reference** after the example is precise about which fields the create decoder reads into the stored appointment versus which are server-derived from the chosen service — read it before assuming a field round-trips. A minimal write needs `status`, `start`, `end`, a `serviceType`, at least one practitioner participant, and a room `Location` participant — plus a `specialty` selection when the chosen service is multi-specialty (the example includes one). :::CodeGroup ```bash curl -i -X POST https://api.huli.ai/fhir/R4/Appointment \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Content-Type: application/fhir+json" \ -H "Accept: application/fhir+json" \ -d '{ "resourceType": "Appointment", "status": "booked", "priority": 5, "description": "Consulta general — control", "patientInstruction": "Llegar 10 minutos antes y traer estudios previos.", "serviceType": [ { "coding": [ { "system": "https://fhir.huli.ai/r4/CodeSystem/org-service", "code": "01965e2a-8c4d-7000-9010-0000000000f1", "display": "Consulta general" } ] } ], "specialty": [ { "coding": [ { "system": "https://fhir.huli.ai/r4/CodeSystem/specialty", "code": "01965e2a-8c4d-7000-9011-0000000000d1", "display": "Medicina general" } ] } ], "start": "2026-06-17T09:00:00.000-06:00", "end": "2026-06-17T09:30:00.000-06:00", "participant": [ { "actor": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2" }, "status": "accepted" }, { "actor": { "reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1" }, "status": "accepted" }, { "actor": { "reference": "Location/01965e2a-8c4d-7000-9020-0000000000a1" }, "status": "accepted" } ], "extension": [ { "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-insurance-snapshot", "extension": [ { "url": "provider", "valueString": "Seguros Monterrey" }, { "url": "policyNumber", "valueString": "POL-99812" }, { "url": "certificateNumber", "valueString": "CERT-44120" } ] } ] }' ``` ```typescript const serviceType = { coding: [ { system: 'https://fhir.huli.ai/r4/CodeSystem/org-service', code: '01965e2a-8c4d-7000-9010-0000000000f1', display: 'Consulta general', }, ], }; const appointment = { resourceType: 'Appointment', status: 'booked', // required priority: 5, // optional — uint; the decoder stores it verbatim description: 'Consulta general — control', // optional — stored patientInstruction: 'Llegar 10 minutos antes y traer estudios previos.', // optional — stored serviceType: [serviceType], // required — verbatim from HealthcareService.type; resolves to the org service specialty: [ // required ONLY when the chosen service offers ≥2 specialties; the bookable // coding is verbatim from HealthcareService.specialty[].coding (specialty CodeSystem). // A single-/all-specialty service derives it server-side — omit it then. { coding: [ { system: 'https://fhir.huli.ai/r4/CodeSystem/specialty', code: '01965e2a-8c4d-7000-9011-0000000000d1', display: 'Medicina general', }, ], }, ], start: '2026-06-17T09:00:00.000-06:00', // required — from the free slot end: '2026-06-17T09:30:00.000-06:00', // required participant: [ // All three actors are decoded and persisted as the appointment's participants: // the patient (optional), the practitioner (≥1 required), and the room Location (required). // Equipment is optional via a Device/ actor. { actor: { reference: 'Patient/01965e2a-8c4d-7000-9001-0000000000a2' }, status: 'accepted' }, { actor: { reference: 'Practitioner/01965e2a-8c4d-7000-9001-0000000000c1' }, status: 'accepted', }, { actor: { reference: 'Location/01965e2a-8c4d-7000-9020-0000000000a1' }, status: 'accepted' }, ], extension: [ { // insurance snapshot — provider required within the block; policy/certificate optional url: 'https://fhir.huli.ai/r4/StructureDefinition/huli-insurance-snapshot', extension: [ { url: 'provider', valueString: 'Seguros Monterrey' }, { url: 'policyNumber', valueString: 'POL-99812' }, { url: 'certificateNumber', valueString: 'CERT-44120' }, ], }, ], }; const resp = await fetch('https://api.huli.ai/fhir/R4/Appointment', { method: 'POST', headers: { Authorization: `Bearer ${process.env.HULI_TOKEN}`, 'Content-Type': 'application/fhir+json', Accept: 'application/fhir+json', }, body: JSON.stringify(appointment), }); if (resp.status === 201) { const created = (await resp.json()) as { id: string }; console.log('booked', created.id); } else { const outcome = (await resp.json()) as { issue: { diagnostics: string }[] }; // The HP-/HPB- code is the prefix of issue[0].diagnostics — split on ': '. const [code] = outcome.issue[0].diagnostics.split(': ', 1); console.log(resp.status, code, outcome.issue[0].diagnostics); } ``` ```python import os import requests service_type = { "coding": [ { "system": "https://fhir.huli.ai/r4/CodeSystem/org-service", "code": "01965e2a-8c4d-7000-9010-0000000000f1", "display": "Consulta general", } ] } appointment = { "resourceType": "Appointment", "status": "booked", # required "priority": 5, # optional — uint; stored verbatim "description": "Consulta general — control", # optional — stored "patientInstruction": "Llegar 10 minutos antes y traer estudios previos.", # optional — stored "serviceType": [service_type], # required — verbatim from HealthcareService.type; resolves to the org service "specialty": [ # required ONLY when the chosen service offers ≥2 specialties; the bookable # coding is verbatim from HealthcareService.specialty[].coding (specialty CodeSystem). # A single-/all-specialty service derives it server-side — omit it then. { "coding": [ { "system": "https://fhir.huli.ai/r4/CodeSystem/specialty", "code": "01965e2a-8c4d-7000-9011-0000000000d1", "display": "Medicina general", } ] } ], "start": "2026-06-17T09:00:00.000-06:00", # required — from the free slot "end": "2026-06-17T09:30:00.000-06:00", # required "participant": [ # All three actors are decoded and persisted as the appointment's participants: # the patient (optional), the practitioner (≥1 required), and the room Location (required). # Equipment is optional via a Device/ actor. {"actor": {"reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2"}, "status": "accepted"}, {"actor": {"reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1"}, "status": "accepted"}, {"actor": {"reference": "Location/01965e2a-8c4d-7000-9020-0000000000a1"}, "status": "accepted"}, ], "extension": [ { # insurance snapshot — provider required within the block; policy/certificate optional "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-insurance-snapshot", "extension": [ {"url": "provider", "valueString": "Seguros Monterrey"}, {"url": "policyNumber", "valueString": "POL-99812"}, {"url": "certificateNumber", "valueString": "CERT-44120"}, ], } ], } resp = requests.post( "https://api.huli.ai/fhir/R4/Appointment", headers={ "Authorization": f"Bearer {os.environ['HULI_TOKEN']}", "Content-Type": "application/fhir+json", "Accept": "application/fhir+json", }, json=appointment, timeout=30, ) if resp.status_code == 201: print("booked", resp.json()["id"]) else: outcome = resp.json() # The HP-/HPB- code is the prefix of issue[0].diagnostics — split on ": ". code = outcome["issue"][0]["diagnostics"].split(": ", 1)[0] print(resp.status_code, code, outcome["issue"][0]["diagnostics"]) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class BookAppointment { public static void main(String[] args) throws Exception { // serviceType is the org-service coding from HealthcareService, verbatim. // start/end come from the free slot. Hand-built JSON keeps this // dependency-free; a real client would use a JSON library. // status/start/end and serviceType are required; priority/description/ // patientInstruction and the insurance-snapshot extension are optional // fields the decoder stores. The patient, practitioner (≥1 required) and room // Location (required) participants are all decoded and persisted. String appointment = "{" + "\"resourceType\":\"Appointment\"," + "\"status\":\"booked\"," + "\"priority\":5," + "\"description\":\"Consulta general — control\"," + "\"patientInstruction\":\"Llegar 10 minutos antes y traer estudios previos.\"," + "\"serviceType\":[{\"coding\":[{" + "\"system\":\"https://fhir.huli.ai/r4/CodeSystem/org-service\"," + "\"code\":\"01965e2a-8c4d-7000-9010-0000000000f1\"," + "\"display\":\"Consulta general\"}]}]," // specialty is required only for a multi-specialty service. + "\"specialty\":[{\"coding\":[{" + "\"system\":\"https://fhir.huli.ai/r4/CodeSystem/specialty\"," + "\"code\":\"01965e2a-8c4d-7000-9011-0000000000d1\"," + "\"display\":\"Medicina general\"}]}]," + "\"start\":\"2026-06-17T09:00:00.000-06:00\"," + "\"end\":\"2026-06-17T09:30:00.000-06:00\"," + "\"participant\":[" + "{\"actor\":{\"reference\":\"Patient/01965e2a-8c4d-7000-9001-0000000000a2\"},\"status\":\"accepted\"}," + "{\"actor\":{\"reference\":\"Practitioner/01965e2a-8c4d-7000-9001-0000000000c1\"},\"status\":\"accepted\"}," + "{\"actor\":{\"reference\":\"Location/01965e2a-8c4d-7000-9020-0000000000a1\"},\"status\":\"accepted\"}" + "]," + "\"extension\":[{" + "\"url\":\"https://fhir.huli.ai/r4/StructureDefinition/huli-insurance-snapshot\"," + "\"extension\":[" + "{\"url\":\"provider\",\"valueString\":\"Seguros Monterrey\"}," + "{\"url\":\"policyNumber\",\"valueString\":\"POL-99812\"}," + "{\"url\":\"certificateNumber\",\"valueString\":\"CERT-44120\"}" + "]}]}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Appointment")) .header("Authorization", "Bearer " + System.getenv("HULI_TOKEN")) .header("Content-Type", "application/fhir+json") .header("Accept", "application/fhir+json") .POST(HttpRequest.BodyPublishers.ofString(appointment)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); switch (response.statusCode()) { case 201 -> System.out.println("201 booked\n" + response.body()); case 400 -> // HPB-00101 structural validation System.out.println("400 validation\n" + response.body()); case 409 -> // HP-00803 the slot is no longer free System.out.println("409 conflict\n" + response.body()); case 422 -> // HP-008xx booking precondition System.out.println("422 precondition\n" + response.body()); default -> System.out.println(response.statusCode() + "\n" + response.body()); } } } ``` ```go package main import ( "bytes" "fmt" "io" "net/http" "os" ) func main() { // serviceType is the org-service coding from HealthcareService, verbatim, and is // required; start/end come from the free slot. priority/description/ // patientInstruction and the insurance-snapshot extension are optional // fields the decoder stores. The patient, practitioner (≥1 required) and room // Location (required) participants are all decoded and persisted. body := []byte(`{ "resourceType": "Appointment", "status": "booked", "priority": 5, "description": "Consulta general — control", "patientInstruction": "Llegar 10 minutos antes y traer estudios previos.", "serviceType": [{"coding": [{ "system": "https://fhir.huli.ai/r4/CodeSystem/org-service", "code": "01965e2a-8c4d-7000-9010-0000000000f1", "display": "Consulta general" }]}], "specialty": [{"coding": [{ "system": "https://fhir.huli.ai/r4/CodeSystem/specialty", "code": "01965e2a-8c4d-7000-9011-0000000000d1", "display": "Medicina general" }]}], "start": "2026-06-17T09:00:00.000-06:00", "end": "2026-06-17T09:30:00.000-06:00", "participant": [ {"actor": {"reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2"}, "status": "accepted"}, {"actor": {"reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1"}, "status": "accepted"}, {"actor": {"reference": "Location/01965e2a-8c4d-7000-9020-0000000000a1"}, "status": "accepted"} ], "extension": [{ "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-insurance-snapshot", "extension": [ {"url": "provider", "valueString": "Seguros Monterrey"}, {"url": "policyNumber", "valueString": "POL-99812"}, {"url": "certificateNumber", "valueString": "CERT-44120"} ] }] }`) req, err := http.NewRequest(http.MethodPost, "https://api.huli.ai/fhir/R4/Appointment", bytes.NewReader(body)) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("HULI_TOKEN")) req.Header.Set("Content-Type", "application/fhir+json") req.Header.Set("Accept", "application/fhir+json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, err := io.ReadAll(resp.Body) if err != nil { panic(err) } switch resp.StatusCode { case http.StatusCreated: fmt.Printf("201 booked\n%s\n", out) case http.StatusBadRequest: // HPB-00101 structural validation fmt.Printf("400 validation\n%s\n", out) case http.StatusConflict: // HP-00803 the slot is no longer free fmt.Printf("409 conflict\n%s\n", out) case http.StatusUnprocessableEntity: // HP-008xx booking precondition fmt.Printf("422 precondition\n%s\n", out) default: fmt.Printf("%d\n%s\n", resp.StatusCode, out) } } ``` ::: The Run button above sends the **minimal** write — no patient participant, no `specialty` (only required when the discovered service offers two or more specialties; see the full field reference below). Chained from the steps above: `serviceTypeCode` (step 2), `practitionerRoleId` + `roomLocationRef` (step 3), `slotStart`/`slotEnd` (step 5). A `201 Created` returns the stored `Appointment` with a server-assigned `id`. Set `status` to `proposed` instead of `booked` if your flow needs an intermediate "requested, awaiting confirmation" state before it firms up. #### Full field reference Every field the Appointment write surface touches. The public write routes through the same scheduling service the in-app calendar uses, so the request body must name the resources an appointment needs; the service then derives the appointment type, specialty, and booking policy from the chosen service. Required: `status`, `start`, `end`, `serviceType`, at least one practitioner participant, and a room `Location` participant — plus `specialty` when the chosen service offers two or more specialties. | Field | Req? | Honored on write | Notes | | -------------------------------------------- | ----------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | **required** | yes | A valid FHIR appointment status; maps to the Huli status id. | | `start` | **required** | yes | Slot start (from step 5). | | `end` | **required** | yes | Slot end. | | `serviceType[].coding` | **required** | yes | The `org-service` coding from `HealthcareService.type` (system `…/CodeSystem/org-service`, `code` = the org service UUID). Resolves to the org service; the service derives appointment type, specialty, booking policy, and per-service resource requirements. Missing or unresolvable → `422`. | | `participant[].actor` `Practitioner/` | **required** (≥1) | yes | Decoded and persisted as the appointment's participants. At least one practitioner is required. | | `participant[].actor` `Location/` | **required** | yes | The room. Decoded and persisted; missing → `400` (`HP-00816`). | | `participant[].actor` `Patient/` | optional | yes | The patient participant (omit for walk-ins / admin meetings). | | `participant[].actor` `Device/` | optional | yes | Equipment participant; decoded and persisted when the service requires it. | | `priority` | optional | yes | Unsigned integer; stored verbatim (`0` = routine). | | `description` | optional | yes | Reason / short label, stored on the appointment. | | `patientInstruction` | optional | yes | Instructions shown to the patient. | | `extension[]` `…/huli-insurance-snapshot` | optional | yes | Insurance snapshot: nested `provider` (required within the block), `policyNumber`, `certificateNumber`. | | `cancelationReason` | optional | on cancel only | Resolved against the org's cancellation reasons on `PUT status=cancelled`; not read on create. | | `appointmentType` | optional | server-derived | Read-only on write — derived from the chosen `serviceType` and re-emitted on read. | | `specialty[].coding` | conditional | yes | The bookable `specialty` coding (system `…/CodeSystem/specialty`, `code` = the specialty UUID) copied verbatim from `HealthcareService.specialty`. **Required** when the chosen service offers ≥2 specialties — omitted → `422` (`HPB-00115`); a specialty the service does not offer → `422` (`HPB-00116`); a non-UUID code → `422` (`value`). For a single-/all-specialty service it is optional (derived server-side; a human/SNOMED-only coding is ignored). | | `extension[]` `…/confirmation-status` | optional | server-derived | Read-only on write — tracks the patient-confirmation workflow; re-emitted on read. | | `participant[].status` / `required` / `type` | optional | server-stamped | The read endpoint stamps these from the stored participant rows; input is not used. | ## What to verify - HTTP status is `201`. - The response body's `resourceType` is `Appointment` and it carries a server-assigned `id`. - The `serviceType.coding[0].code` you sent round-trips unchanged — proof the org-service code was accepted, not silently dropped. - `start`/`end` match the slot you chose, and the practitioner + room participants are present. - Re-search `GET /fhir/R4/Slot?actor=…` for the same window: the slot you booked is no longer in the free list. ## What can go wrong All errors return a FHIR `OperationOutcome`, never a bare string — `{severity, code, diagnostics}`, with no `details` object. Branch on the HTTP status and `issue[0].code` (the FHIR IssueType); the Huli code is the prefix of `issue[0].diagnostics`, split on `": "` to extract it. Structural problems (a missing required field, malformed JSON) surface as `HPB-00101`; the booking preconditions surface the practice-layer `HP-008xx` codes inside the same `diagnostics`. **`serviceType` missing or unresolvable.** The booking must carry a `serviceType` whose `coding` uses the `org-service` system with a `code` that is a current org service UUID. Absent → a `required` issue; present but the wrong system or an unparseable code → a `value` issue. A bare name string is rejected. This is why step 2 lifts the coding verbatim from `HealthcareService.type` rather than constructing one. (The old behavior — silently defaulting to the org's first active service — has been removed.) `HP-00807` — **no resource participant.** The booking needs at least one `participant.actor` referencing a `Practitioner`. Resolve the practitioner in step 3 before you build the participant array. `HP-00816` — **room/location missing.** The booking needs a room: a `participant.actor` referencing a `Location`. Take it from `PractitionerRole.location` in step 4. `HP-00817` — **the practitioner has no assigned locations.** Every practitioner must have at least one assigned room to be bookable. If `PractitionerRole.location` is empty, the practitioner cannot be booked until a room is assigned in HuliPractice. `HP-00818` — **the room is not in the practitioners' assigned locations.** The room you sent must be inside the participating practitioner's assigned set. Sourcing the room from that practitioner's own `PractitionerRole.location` (step 4) avoids this — a room taken only from `HealthcareService.location` can fall outside it. `HPB-00115` — **a specialty must be selected.** The chosen service offers two or more specialties (its `HealthcareService.specialty` has ≥2 entries), so the booking must name which one in `Appointment.specialty` — the API will not guess. Copy one of the service's `specialty[].coding` entries (the `specialty`-CodeSystem coding) verbatim into the Appointment. `HPB-00116` — **specialty not offered by this service.** The `specialty` you sent is not among the ones the service advertises. Pick a coding straight from the service's `HealthcareService.specialty` list rather than constructing one. (A present-but-malformed specialty `code` — a non-UUID — is rejected `422` with a `value` issue instead.) `HP-00819` — **practitioner specialty mismatch.** When the service carries a `specialty` (step 2), every participating practitioner must carry the booked specialty in their `PractitionerRole.specialty`. Cross-check the role's specialty against the service's before you book. `HP-00803` — **the slot is no longer free.** Between your slot search and your `POST`, someone else booked it (or it overlaps an existing appointment). Re-run the step 5 slot search and pick another free slot; do not blindly retry the same body. To cancel a booking later, `PUT` the `Appointment` with `status: "cancelled"` and a `cancelationReason` whose code comes from the cancellation-reason ValueSet. Expand it with `GET /fhir/R4/ValueSet/$expand?url=https://fhir.huli.ai/r4/ValueSet/cancellation-reason` and pick a code from the returned `expansion.contains[]` — a free-text reason without a valid code is rejected. A cancel is **blocked `409` (`HP-00812`)** when a clinical encounter is already linked to the appointment. To mark a booking as a data-entry mistake instead, `PUT` `status: "entered-in-error"` — that path takes no reason and runs no cancel guards. A representative `422` precondition body: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "processing", "diagnostics": "HP-00818: Room's location is not among the practitioners' assigned locations" } ] } ``` ## Next recipes - **[Scheduling an administrative meeting](/v1/recipes/scheduling-an-administrative-meeting)** — book an internal meeting with no patient, a title, and external email invitees, against a service whose appointment type is `administrative`. - **[Wiring a read-only partner](/v1/recipes/wiring-a-read-only-partner)** — consume the Appointment + Encounter feed read-only once bookings exist, resolving the Practitioner/Organization references they point at. - **[Run your first authenticated Patient search](/v1/recipes/getting-started-patient-search)** — resolve the patient you book for by name or identifier first. - **Authenticate as a SMART Backend Service** — swap the admin bearer token for `client_credentials` + `private_key_jwt` (RS384, 5-minute access token) when you ship the booking flow server-to-server. ======================================================================== # Choosing a CLI authentication path # URL: https://developers.huli.ai/v1/recipes/cli-authentication-paths # Decide between interactive OAuth (no CLI subcommand today), M2M client_credentials, and a one-off bearer token for the huli CLI — a decision table and the exact command for each. # Choosing a CLI authentication path The `huli` CLI talks to the same FHIR R4 surface three different ways, and the right one depends on who runs it and how long it runs. Pick the path first, then copy the one command that matches. This recipe maps each workload to a path, gives you the exact invocation, and names the failures that tell you the path was wrong. ## Audience You wire integrations against the Huli FHIR API and you are about to script the `huli` CLI into something — a developer's laptop, a cron job, a CI pipeline, or a quick one-shot from a shell. You know what a bearer token is and you have read the FHIR base URL at least once. ## You'll need - The `huli` CLI on your `PATH`, with `huli auth setup` available (run `huli --help` to confirm the binary resolves). - For the M2M path: an `api_key` registered with a JWKS URI, an RS384 signing key whose public half is published at that JWKS URI, and the scopes the workload needs. An admin on your organization registers the key in **Practice Settings → Integrations → API Keys**. - For the one-off path: an admin bearer token, minted once in the same place and shown once. It is long-lived and scoped to one organization. - The base host `https://api.huli.ai` and the FHIR base `https://api.huli.ai/fhir/R4/`. The token endpoint is host-rooted at `https://api.huli.ai/auth/token`, not under `/fhir`. SMART discovery and JWKS are issuer-rooted under /fhir, not the host root: discovery at https://api.huli.ai/fhir/.well-known/smart-configuration and the server's signing keys at https://api.huli.ai/fhir/.well-known/jwks.json. The token POST, by contrast, is host-rooted at https://api.huli.ai/auth/token. Mixing these up is the most common first-run misconfiguration. ## End state You have chosen one of three paths and run one authenticated request through it. The CLI holds credentials in the shape that path expects — a stored M2M profile, an interactive session, or a bearer string passed per command — and a Patient search returns a `searchset` Bundle instead of an `OperationOutcome`. ## Steps ### 1. Match your workload to a path Read the row that describes who runs the CLI and how often, then jump to that path's command below. | Workload | Path | CLI surface | Credential lifetime | Status | | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ---------------------------------------- | -------------------------------------------------------------------- | ----------------------- | | A human running the CLI interactively from a laptop, acting as themselves | Interactive OAuth (Auth-Code + PKCE) | `huli auth login` | Short-lived session, refreshed in the background | No CLI subcommand today | | An unattended server, cron job, or CI pipeline acting as a service, not a person | SMART Backend Services (`client_credentials` + `private_key_jwt`, RS384) | `huli auth setup` | 5-minute access token, re-minted automatically from your signing key | Available | | A one-shot command, a debugging session, or a script that already holds an admin key | Admin bearer token passed per command | `--token` flag or `Authorization` header | Long-lived admin key, no refresh | Available | The dividing questions, in order: 1. Is a person sitting at the keyboard, and do you want requests attributed to that person? That is interactive OAuth — a v1 API auth mode the CLI has no subcommand for today, so fall through to one of the next two. 2. Is the caller a service running without a human present? That is `huli auth setup`. 3. Is this a single throwaway call, or do you already hold an admin bearer token? Pass it per command with `--token`. ### 2a. Interactive OAuth — no CLI subcommand today huli auth login is not part of the current CLI. Interactive OAuth (Auth-Code + PKCE) is a defined v1 auth mode on the API, but the CLI has no subcommand that drives it. A human who needs to run the CLI uses an admin bearer token (step 2c) scoped to what they need. Treat the interactive row in the decision table as a signpost, not a CLI instruction: for CLI access today, use M2M (`huli auth setup`, step 2b) or an admin bearer token (step 2c). ### 2b. M2M — `huli auth setup` (client_credentials + private_key_jwt) This is the path for any unattended caller. The CLI signs a `private_key_jwt` client assertion with your RS384 key, exchanges it at the token endpoint for a 5-minute access token, and re-mints that token automatically as it expires. Run the one-time setup: ```bash huli auth setup \ --base-url https://api.huli.ai \ --client-id clinica-san-rafael-integration \ --jwks-uri https://integrations.clinica-san-rafael.example/jwks.json \ --private-key ./san-rafael-signing-key.pem \ --scope "system/Patient.rs system/Appointment.rs" ``` The flags map one-to-one onto the SMART Backend Services handshake: - `--base-url` is the host root. The CLI derives the token endpoint as `https://api.huli.ai/auth/token` and discovery as `https://api.huli.ai/fhir/.well-known/smart-configuration` from it. - `--client-id` is the `sub` of your signed assertion and identifies the `api_key` row. - `--jwks-uri` must match the JWKS URI registered on that `api_key` — the server fetches your public key from there to verify the RS384 signature. - `--private-key` points at the RS384 private key whose public half lives at that JWKS URI. - `--scope` is the space-separated set of scopes to request. Request only what the workload uses. Once setup completes, every CLI command authenticates from the stored profile — no token flag, no header. Run any read command (a Patient search filtered by name, for example) and behind it the CLI POSTs the assertion to `https://api.huli.ai/auth/token`, receives an RS384 access token valid for 5 minutes, and attaches it as a bearer on the FHIR request. When the token expires, the next command re-mints it from your key — you never handle the access token yourself. Pick scopes from the v1 set. Letters are `r`=read, `s`=search, `c`=create, `u`=update. Read plus search is `.rs`; full write is `.cru`. - Read + write resources: , , , . - Read-only resources: , . - Provenance is create plus read plus search only, client-POSTed: together with . ### 2c. One-off — admin bearer token via `--token` or header For a single call, a debugging session, or a script that already holds an admin key, skip the stored profile and pass the token per command. The admin bearer token comes from **Practice Settings → Integrations → API Keys**, is shown once, and is long-lived. Export it so it never lands in shell history: ```bash export HULI_API_KEY="" ``` Pass it to the CLI with the `--token` flag on any read command, or — calling the API directly rather than through the CLI — ride the same token in the `Authorization` header with one space after `Bearer`: ```bash curl "https://api.huli.ai/fhir/R4/Patient?name=Fern%C3%A1ndez&_count=20" \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Accept: application/fhir+json" ``` The admin bearer token does not expire on a timer the way the M2M access token does, so there is no refresh to manage — but that also means a leaked admin token stays valid until an admin revokes it. Keep it to one-off and interactive-human use; for anything unattended and long-running, prefer the M2M path, where each access token lives 5 minutes. ## What to verify - For the M2M path: `huli auth setup` exits cleanly, and a follow-up read command (a Patient search by name) with no `--token` flag returns a `Bundle` of type `searchset`. That proves the stored profile minted a token without you handling it. - For the one-off path: the same read with `--token "$HULI_API_KEY"` returns a `searchset` Bundle. Status is `200`. - Either way, the response `resourceType` is `Bundle` and `type` is `searchset` — not `OperationOutcome`. - On the M2M path, you requested only the scopes the workload uses. A read-only reporting job should not request `.cru` on any resource. ## What can go wrong Every failure comes back as a FHIR `OperationOutcome` with exactly this shape — no `details`, no `coding`, no `text`: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "security", "diagnostics": "HPB-00106: Authentication failed" } ] } ``` Classify machine-side on the HTTP status and `issue[0].code` (the FHIR IssueType: `400`→`invalid`, `401`→`security`, `403`→`forbidden`, `404`→`not-found`, `409`→`conflict`, `429`→`throttled`, `5xx`→`exception`). The Huli code is the prefix of `issue[0].diagnostics` — split on `": "` to read it. The five you will hit choosing a path: `HPB-00106` — auth failed. The credential is missing, malformed, or revoked. On the M2M path, the assertion signature did not verify — confirm `--jwks-uri` matches the URI registered on the `api_key` and that `--private-key` is the RS384 key whose public half sits there. On the one-off path, confirm the header reads `Authorization: Bearer ` with a single space and that `$HULI_API_KEY` is exported in this shell. `HPB-00107` — auth expired. You presented an access token past its lifetime. This is specific to the 5-minute M2M token: it means a cached token outlived its window. With `huli auth setup` the CLI re-mints automatically, so seeing this usually means you pinned a raw token by hand instead of letting the stored profile refresh it. You will not see this from an admin bearer token, which does not expire on a timer. `HPB-00104` — insufficient scope. The credential authenticated but lacks the scope the request needs. On the M2M path, widen `--scope` and re-run `huli auth setup` (and confirm the `api_key` is allowed those scopes). On the one-off path, confirm the admin key was granted the scopes the request needs; if not, mint a new key in **Practice Settings → Integrations → API Keys**. `HPB-00101` — validation error. A request parameter is malformed — most often an un-encoded accent in a hand-built URL. Encode `á` as `%C3%A1`, or let the CLI and HTTP clients encode the raw string for you. `HPB-00105` — rate limited. You exceeded the per-key request budget. Read the `Retry-After` response header and back off for that many seconds before retrying. Unattended M2M jobs should honor `Retry-After` rather than tight-looping. ## Next recipes - **Run your first authenticated Patient search** — one round-trip against the FHIR API with an admin bearer token, and the four errors you hit first. - **Authenticate as a SMART Backend Service** — the full `client_credentials` + `private_key_jwt` (RS384) handshake under the hood, for when you want to build the token exchange yourself instead of letting the CLI drive it. - **Paginate a large patient list** — follow the `Bundle.link` entry whose relation is `next` to walk every page of a `searchset`, whichever auth path you chose here. ======================================================================== # Creating a clinical encounter # URL: https://developers.huli.ai/v1/recipes/creating-an-encounter # Record a visit as a FHIR R4 Encounter — discover the practitioner the create requires as a participant, then POST the Encounter with its status, ActCode class, patient subject, and period. # Creating a clinical encounter Record a visit for an existing patient as a stored `Encounter`. You will authenticate, discover the practitioner the create requires as a participant, then `POST` the encounter with its `status`, `class`, patient `subject`, and `period`. Three scopes carry the flow: system/Encounter.cru for the write, system/Patient.rs so the subject resolves, and system/Practitioner.rs to discover the participant. The participant is the part most first writes miss. An `Encounter` create requires at least one practitioner participant — the visit has to name who attended it. The discovery step below hands you a real `Practitioner` reference so the participant array satisfies that check, and the patient subject and class round out a body the server accepts. ## Audience You integrate an EHR and record visits into HuliPractice. You have already [registered or resolved the patient](/v1/recipes/registering-a-patient), you read a `Bundle` without a viewer, and you know what a FHIR reference is. You want to take a visit from a patient plus a practitioner to a `201 Created` `Encounter`. ## You'll need - A bearer token from HuliPractice (**Practice Settings → Integrations → API Keys**), or a SMART Backend Services access token. See [Bearer Tokens](/v1/auth/bearer) for provisioning and [`POST /auth/token`](/v1/auth) for the token exchange — this recipe assumes you already hold one. - These three scopes on that token: - — create `Encounter` (`.cru` also grants read + search). - — read + search `Patient`; the encounter's `subject` must reference a patient that resolves in your organization. - — read + search `Practitioner` and `PractitionerRole` to discover the participant. - The `id` of the patient the visit is for. Resolve it with [a Patient search](/v1/recipes/getting-started-patient-search) if you only hold a name or identifier. - `curl`, or Node, Python, Java, or Go if you prefer a language client. `Encounter.class` is a fixed FHIR value set — the v3 ActCode codes `AMB` (ambulatory), `EMER` (emergency), `IMP` (inpatient), and `VR` (virtual), under `http://terminology.hl7.org/CodeSystem/v3-ActCode`. It is not a per-organization catalog you discover; pick the one ActCode that matches the visit. A class code outside that set is rejected. ## End state You hold a `201 Created` whose body is the stored `Encounter` — with a server-assigned `id`, the patient as `subject`, the discovered practitioner in `participant[0].individual`, the ActCode `class` you chose, and the `period` you sent. The encounter is then readable and searchable by `patient` or `practitioner`. ## Steps ### 1. Export the token and the patient id ```bash export HULI_TOKEN="" export PATIENT_ID="01965e2a-8c4d-7000-9001-0000000000a2" ``` Resolve a real patient id in your sandbox instead of the illustrative one above: ### 2. Discover the practitioner The encounter needs a practitioner participant. Search `Practitioner` by name to get the reference; the `id` of the matching entry is what you put in `participant[0].individual.reference`. The Practitioner search pages on offset pagination (`_count` + `_offset`), the same model the discovery resources use. If you also need the practitioner's rooms or specialties — for example to pre-check a downstream booking — read their `PractitionerRole`; for recording a completed visit, the `Practitioner` reference alone is enough. :::CodeGroup ```bash curl "https://api.huli.ai/fhir/R4/Practitioner?name=Fern%C3%A1ndez&_count=20" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` ```typescript const params = new URLSearchParams({ name: 'Fernández', _count: '20' }); const resp = await fetch(`https://api.huli.ai/fhir/R4/Practitioner?${params}`, { headers: { Authorization: `Bearer ${process.env.HULI_TOKEN}`, Accept: 'application/fhir+json', }, }); const bundle = await resp.json(); // The participant reference is Practitioner/. const practitioner = bundle.entry?.[0]?.resource; console.log(`Practitioner/${practitioner?.id}`); ``` ```python import os import requests resp = requests.get( "https://api.huli.ai/fhir/R4/Practitioner", params={"name": "Fernández", "_count": 20}, headers={ "Authorization": f"Bearer {os.environ['HULI_TOKEN']}", "Accept": "application/fhir+json", }, timeout=30, ) bundle = resp.json() # The participant reference is Practitioner/. practitioner = bundle["entry"][0]["resource"] print(f"Practitioner/{practitioner['id']}") ``` ::: ### 3. POST the Encounter Assemble the discovered practitioner and the patient into the create body: `status` (use `finished` for a completed visit or `in-progress` while it is ongoing), the ActCode `class`, the patient `subject`, a `participant` array naming the practitioner, and the `period`. The body below is the **comprehensive** form — every field the create decoder honors on an Encounter write, including the optional `appointment` link, the visit `reasonCode`, and the `contained` clinical resources (an ICD-10 `Condition` for a diagnosis and a `ClinicalImpression` for the subjective summary). Required fields are flagged inline; the **Full field reference** after the example lists each field and whether the decoder reads it on write. A minimal write needs only `status`, `subject`, and one `participant`. Use `finished` for a completed visit — that is the FHIR R4 status. The internal Huli status "completed" maps to the FHIR token `finished`, so always send `finished`, never `completed`. For an ongoing visit send `in-progress` and omit `period.end`. :::CodeGroup ```bash curl -i -X POST https://api.huli.ai/fhir/R4/Encounter \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Content-Type: application/fhir+json" \ -H "Accept: application/fhir+json" \ -d '{ "resourceType": "Encounter", "status": "finished", "class": { "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "AMB", "display": "ambulatory" }, "subject": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2" }, "participant": [ { "individual": { "reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1" } } ], "appointment": [ { "reference": "Appointment/01965e2a-8c4d-7000-9050-0000000000e1" } ], "period": { "start": "2026-06-15T09:00:00.000-06:00", "end": "2026-06-15T09:30:00.000-06:00" }, "reasonCode": [ { "text": "Control de hipertensión" } ], "contained": [ { "resourceType": "Condition", "id": "condition-1", "code": { "coding": [ { "system": "http://hl7.org/fhir/sid/icd-10", "code": "I10", "display": "Hipertensión esencial (primaria)" } ], "text": "Hipertensión esencial (primaria)" }, "subject": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2" } }, { "resourceType": "ClinicalImpression", "id": "clinical-impression-1", "status": "completed", "subject": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2" }, "summary": "Paciente refiere cefalea ocasional; sin otros síntomas." } ] }' ``` ```typescript const encounter = { resourceType: 'Encounter', status: 'finished', // required — FHIR R4 status; Huli's "completed" maps to this class: { // optional — only class.code is read; AMB | EMER | IMP | VR (defaults to AMB if omitted) system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', code: 'AMB', display: 'ambulatory', }, subject: { reference: `Patient/${process.env.PATIENT_ID}` }, // required participant: [ // At least one practitioner participant is required; participant[0].individual is read. { individual: { reference: 'Practitioner/01965e2a-8c4d-7000-9001-0000000000c1' } }, ], appointment: [ // optional — links the visit to the appointment that scheduled it (appointment[0] read) { reference: 'Appointment/01965e2a-8c4d-7000-9050-0000000000e1' }, ], period: { start: '2026-06-15T09:00:00.000-06:00', end: '2026-06-15T09:30:00.000-06:00', // omit for an in-progress visit }, reasonCode: [{ text: 'Control de hipertensión' }], // optional — reasonCode[0].text read contained: [ { // ICD-10 diagnosis — code.coding[0] + code.text read into the encounter's diagnoses resourceType: 'Condition', id: 'condition-1', code: { coding: [ { system: 'http://hl7.org/fhir/sid/icd-10', code: 'I10', display: 'Hipertensión esencial (primaria)', }, ], text: 'Hipertensión esencial (primaria)', }, subject: { reference: `Patient/${process.env.PATIENT_ID}` }, }, { // subjective summary — ClinicalImpression.summary read resourceType: 'ClinicalImpression', id: 'clinical-impression-1', status: 'completed', subject: { reference: `Patient/${process.env.PATIENT_ID}` }, summary: 'Paciente refiere cefalea ocasional; sin otros síntomas.', }, ], }; const resp = await fetch('https://api.huli.ai/fhir/R4/Encounter', { method: 'POST', headers: { Authorization: `Bearer ${process.env.HULI_TOKEN}`, 'Content-Type': 'application/fhir+json', Accept: 'application/fhir+json', }, body: JSON.stringify(encounter), }); if (resp.status === 201) { const created = (await resp.json()) as { id: string }; console.log('recorded', created.id); } else { const outcome = (await resp.json()) as { issue: { code: string; diagnostics: string }[] }; // issue[0].code is the FHIR IssueType; diagnostics describes the problem. console.log(resp.status, outcome.issue[0].code, outcome.issue[0].diagnostics); } ``` ```python import os import requests patient_ref = f"Patient/{os.environ['PATIENT_ID']}" encounter = { "resourceType": "Encounter", "status": "finished", # required — FHIR R4 status; Huli's "completed" maps to this "class": { # optional — only class.code is read; AMB | EMER | IMP | VR (defaults to AMB if omitted) "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "AMB", "display": "ambulatory", }, "subject": {"reference": patient_ref}, # required "participant": [ # At least one practitioner participant is required; participant[0].individual is read. {"individual": {"reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1"}}, ], "appointment": [ # optional — links the visit to the appointment that scheduled it (appointment[0] read) {"reference": "Appointment/01965e2a-8c4d-7000-9050-0000000000e1"}, ], "period": { "start": "2026-06-15T09:00:00.000-06:00", "end": "2026-06-15T09:30:00.000-06:00", # omit for an in-progress visit }, "reasonCode": [{"text": "Control de hipertensión"}], # optional — reasonCode[0].text read "contained": [ { # ICD-10 diagnosis — code.coding[0] + code.text read into the encounter's diagnoses "resourceType": "Condition", "id": "condition-1", "code": { "coding": [ { "system": "http://hl7.org/fhir/sid/icd-10", "code": "I10", "display": "Hipertensión esencial (primaria)", } ], "text": "Hipertensión esencial (primaria)", }, "subject": {"reference": patient_ref}, }, { # subjective summary — ClinicalImpression.summary read "resourceType": "ClinicalImpression", "id": "clinical-impression-1", "status": "completed", "subject": {"reference": patient_ref}, "summary": "Paciente refiere cefalea ocasional; sin otros síntomas.", }, ], } resp = requests.post( "https://api.huli.ai/fhir/R4/Encounter", headers={ "Authorization": f"Bearer {os.environ['HULI_TOKEN']}", "Content-Type": "application/fhir+json", "Accept": "application/fhir+json", }, json=encounter, timeout=30, ) if resp.status_code == 201: print("recorded", resp.json()["id"]) else: outcome = resp.json() # issue[0].code is the FHIR IssueType; diagnostics describes the problem. print(resp.status_code, outcome["issue"][0]["code"], outcome["issue"][0]["diagnostics"]) ``` ```go package main import ( "bytes" "fmt" "io" "net/http" "os" ) func main() { // status "finished" is the FHIR token for a completed visit (Huli's // "completed" maps to it). class is the fixed ActCode set; at least one // practitioner participant is required. appointment, reasonCode, and the // contained Condition/ClinicalImpression are optional enrichment the create // decoder honors. patientRef := "Patient/" + os.Getenv("PATIENT_ID") body := []byte(`{ "resourceType": "Encounter", "status": "finished", "class": { "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "AMB", "display": "ambulatory" }, "subject": {"reference": "` + patientRef + `"}, "participant": [ {"individual": {"reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1"}} ], "appointment": [ {"reference": "Appointment/01965e2a-8c4d-7000-9050-0000000000e1"} ], "period": { "start": "2026-06-15T09:00:00.000-06:00", "end": "2026-06-15T09:30:00.000-06:00" }, "reasonCode": [{"text": "Control de hipertensión"}], "contained": [ { "resourceType": "Condition", "id": "condition-1", "code": { "coding": [{ "system": "http://hl7.org/fhir/sid/icd-10", "code": "I10", "display": "Hipertensión esencial (primaria)" }], "text": "Hipertensión esencial (primaria)" }, "subject": {"reference": "` + patientRef + `"} }, { "resourceType": "ClinicalImpression", "id": "clinical-impression-1", "status": "completed", "subject": {"reference": "` + patientRef + `"}, "summary": "Paciente refiere cefalea ocasional; sin otros síntomas." } ] }`) req, err := http.NewRequest(http.MethodPost, "https://api.huli.ai/fhir/R4/Encounter", bytes.NewReader(body)) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("HULI_TOKEN")) req.Header.Set("Content-Type", "application/fhir+json") req.Header.Set("Accept", "application/fhir+json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, err := io.ReadAll(resp.Body) if err != nil { panic(err) } switch resp.StatusCode { case http.StatusCreated: fmt.Printf("201 recorded\n%s\n", out) case http.StatusBadRequest: // structural validation (missing participant, bad class/status) fmt.Printf("400 validation\n%s\n", out) case http.StatusUnprocessableEntity: // a subject/practitioner reference that does not resolve fmt.Printf("422 reference not found\n%s\n", out) case http.StatusConflict: // patient merged (HPB-00108) or deceased (HPB-00109) fmt.Printf("409 patient not writable\n%s\n", out) default: fmt.Printf("%d\n%s\n", resp.StatusCode, out) } } ``` ::: The Run button above sends the **minimal** encounter — `status`, `class`, `subject`, and one practitioner participant — chaining `patientId` (step 1) and `practitionerId` (step 2). The `appointment` link, `reasonCode`, and `contained` clinical resources from the full body are all optional. A `201 Created` returns the stored `Encounter` with a server-assigned `id`. The `class` mapping round-trips (the `AMB` ActCode you sent comes back as `AMB`), and the participant carries the practitioner you discovered. #### Full field reference Every field the Encounter create decoder reads on write. "Honored" means the create decoder maps the field into the stored visit; fields not listed (or marked **ignored**) are accepted but not persisted from your input. Required: `status`, `subject`, and at least one `participant`. | Field | Req? | Honored on write | Notes | | ------------------------------------- | ------------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------- | | `status` | **required** | yes | FHIR status; send `finished` (maps to Huli `completed`) or `in-progress`. | | `subject.reference` | **required** | yes | `Patient/` — must resolve in the organization (else `422`). | | `participant[0].individual.reference` | **required** | yes | `Practitioner/` — at least one participant is required; the first individual is read. | | `class.code` | optional | yes | One of `AMB`/`EMER`/`IMP`/`VR`; maps to ambulatory/emergency/inpatient/virtual. Absent defaults to `AMB`. | | `class.system` / `display` | optional | ignored | Re-emitted from the code mapping on read. | | `appointment[0].reference` | optional | yes | `Appointment/` linking the visit to its scheduling appointment. | | `period.start` | optional | yes | Visit start. | | `period.end` | optional | yes | Visit end; omit for an `in-progress` visit. | | `reasonCode[0].text` | optional | yes | Free-text reason for the visit. Only the first entry's `text` is read. | | `contained[]` `Condition` | optional | yes | ICD-10 diagnosis: `code.coding[0]` (system/code/display) + `code.text` are read into the encounter's diagnoses. | | `contained[]` `ClinicalImpression` | optional | yes | `summary` is read as the subjective note. | | `participant[].type` / `period` | optional | ignored | Only `individual` is consumed on write. | | `diagnosis[]` | optional | ignored on write | Built on read from the `contained` Conditions — send diagnoses as `contained` Conditions, not as `diagnosis[]` references. | | `serviceProvider` | optional | ignored | The server stamps the token's organization. | Clinical content — vital signs, lab results — is not carried inside the `Encounter` body. Each measurement is a separate `Observation` resource that references this encounter through its `encounter` field. Record those after the encounter exists; see [Send lab results to the chart](/v1/recipes/posting-lab-observations-lis), which links its `Observation` to both the patient and the encounter. The full Encounter ↔ Observation model is in the [FHIR Implementation Guide](https://developers.huli.ai/fhir/). The same visit is also a **`Composition`** — a sibling projection of this exact row. `Encounter` exposes the visit envelope (status, class, period, participant); `Composition` exposes the clinical narrative (chief complaint, history, findings, assessment, plan) as LOINC-coded sections. Read or amend that narrative — with optimistic concurrency — through the BAA-gated `medical_records` scope; see [Writing and amending a clinical note](/v1/recipes/writing-a-clinical-note). The two projections own different fields, so a `Composition` write never clobbers the diagnoses this `Encounter` surface set. ## What to verify - HTTP status is `201`. - The response body's `resourceType` is `Encounter` and it carries a server-assigned `id`. - `status` is `finished` (or `in-progress` if ongoing) and `class.code` is the ActCode you sent. - `subject.reference` resolves to your `PATIENT_ID`, and `participant[0].individual.reference` is the practitioner you discovered in step 2. - `period.start` matches what you sent; `period.end` is present for a finished visit and absent for an in-progress one. ## What can go wrong All errors return a FHIR `OperationOutcome`, never a bare string — `{severity, code, diagnostics}`, with no `details` object. Branch on the HTTP status and `issue[0].code` (the FHIR IssueType) for machine classification; the `diagnostics` string describes the specific problem. Structural problems on a writable resource are the `HPB-00101` validation family. **No practitioner participant.** The create requires at least one participant with a Practitioner `individual` reference — an `Encounter` records who attended the visit. A body with an empty or absent `participant` array is rejected with `issue[0].code` `required` and a diagnostics of "at least one participant (practitioner) is required". Discover the practitioner in step 2 and build the participant before posting. **Missing subject, bad status, or unknown class.** `subject` (a patient reference) and `status` are required; `class.code` must be one of `AMB`, `EMER`, `IMP`, `VR`. On **create**, `status` is restricted to the four round-trippable states — `planned`, `in-progress`, `finished`, `cancelled`; the transitional/terminal markers (`arrived`, `triaged`, `onleave`, `entered-in-error`, `unknown`) are rejected with `Encounter.status must be one of: planned, in-progress, finished, cancelled on create`. Huli's internal `completed` is not a FHIR status (send `finished`), and a class code outside the ActCode set also fails. (A `PUT` accepts the broader FHIR status set, governed by the status-transition table.) Send `finished`/`in-progress` and a valid ActCode. **Subject or practitioner does not resolve.** A `subject` or `participant.individual` reference whose UUID is well-formed but does not name a patient / practitioner in your organization is rejected with a diagnostics of "Referenced Patient not found in organization" (or "Referenced Practitioner not found in organization"). Resolve both against Patient / Practitioner search first, and confirm the token's organization owns them. **The patient cannot accept new clinical data.** A subject that has been merged into another record (`HPB-00108`) or marked deceased (`HPB-00109`) is rejected — the encounter would attach clinical data to a patient that can no longer take it. Resolve the surviving record (for a merge) or stop, and do not retry the same subject. A representative `400` body for the missing-participant case: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "required", "diagnostics": "at least one participant (practitioner) is required", "expression": ["Encounter.participant"] } ] } ``` ## Next recipes - **[Send lab results to the chart](/v1/recipes/posting-lab-observations-lis)** — record the visit's vitals and lab results as `Observation` resources linked to this encounter. - **[Registering a patient](/v1/recipes/registering-a-patient)** — onboard the patient first when the subject does not yet exist, including the MX NOM-024 address path. - **Authenticate as a SMART Backend Service** — swap the admin bearer token for `client_credentials` + `private_key_jwt` (RS384, 5-minute access token) when you ship the encounter flow server-to-server. ======================================================================== # Creating and sharing an API key as a clinic admin # URL: https://developers.huli.ai/v1/recipes/creating-and-sharing-an-api-key # Mint an admin bearer API key in Practice Settings, choose its scopes, capture the one-time secret, then share it with an integration partner and rotate it on a schedule. # Creating and sharing an API key as a clinic admin Mint an admin bearer API key from Practice Settings, pick the scopes your integration partner actually needs, capture the secret on the one screen that ever shows it, hand it off without leaking it, and set the rotation habit that keeps the whole thing safe. This is the workflow you run before a vendor writes a single line of code. ## Audience You run operations or administration at a clinic — at Clínica San Rafael, that's the admin who manages access for Doctora María Fernández's integration vendor. You are comfortable in Practice Settings and on a terminal for one verification command, but you are not the one building the integration. You decide which data leaves the clinic and who holds the key. ## You'll need - An admin-role user on your organization in HuliPractice. Only admins see the API Keys surface; a clinician or front-desk login does not. - A short list from your integration partner of exactly which resources they read and write. You translate that list into scopes in step 2 — granting more than they need is the most common avoidable risk. - A secure channel to hand the secret to the partner — a password manager share, an enterprise secrets vault, or an equivalent. Plain email and chat do not count. - `curl` (or any HTTP client) for the single verification request at the end. You can also hand the verification step to the partner. An admin bearer token is a long-lived credential scoped to one organization — Clínica San Rafael's data, and nothing from any other clinic. It does not expire on a timer the way a SMART Backend Services access token does. Treat it like a master key to that organization: it stays in a secrets manager, never in email, chat, a shared doc, or a code repository. ## End state A named, active API key exists on your organization with the scopes your partner needs. You have captured its secret once, shared it through a secure channel, and confirmed it works with one `200 OK` from a Patient search. You know how to rotate it and how to revoke it the moment it is no longer needed. ## Steps ### 1. Open the API Keys surface In HuliPractice, go to **Practice Settings**, open the **Integrations** section, and select **API Keys**. This page lists every key on your organization with its name, its scopes, its status, and when it was last used. If the section is absent, you are not signed in as an admin — switch to an admin login before continuing. Select **Create API key** to start a new one. ### 2. Name the key and choose its scopes Give the key a name that identifies the partner and the purpose, not the person who created it. A name like `San Rafael — Lab results sync` survives staff turnover; `Maria's key` does not. You will read this name later when you decide what to rotate or revoke, so make it self-explanatory. Then select the scopes. Each scope is `system/.`, where the permission letters are `r` (read), `s` (search), `c` (create), and `u` (update). Two combinations cover almost every integration: - Read plus search — the `.rs` form, e.g. . Grant this when the partner only pulls data out. - Full write — the `.cru` form, e.g. . Grant this when the partner also creates and updates records. It includes read. Which resources you can grant depends on whether they are writable in v1: | Resource | Available scopes | | ----------------- | -------------------------------------------------------------------------------------------------------- | | Patient | · | | Appointment | · | | Encounter | · | | Observation | · | | Composition | · (BAA-gated) | | DocumentReference | · (BAA-gated) | | Practitioner | (read-only) | | Organization | (read-only) | | Provenance | · | Practitioner and Organization are read-only in v1, so only the `.rs` form exists. Provenance is the audit-trail resource a partner POSTs alongside a write; it offers read, search, and create (`.rs` and `.c`), and no update. (Encounters, observations, medication/service requests, compositions, and document references all sit under one BAA-gated "Clinical information" card; the webhook Subscription scopes are their own "Outbound webhooks" card — see the [Scopes reference](/v1/scopes).) Grant the narrowest set that does the job. A lab-results integration that only reads patients and writes observations needs and — not write access to appointments or encounters. Every extra scope widens what a leaked key exposes. You can mint a second, separate key for a second partner rather than over-scoping one shared key. **Clinical scopes require a signed BAA.** The sensitive **Clinical information** card (Encounter, Observation, MedicationRequest, ServiceRequest, Composition, DocumentReference) exposes protected health information, so the surface makes you attest to a Business Associate Agreement (or equivalent) before it will mint a key that carries any of those scopes. Demographic and scheduling scopes (Patient, Appointment, Practitioner, Organization) are not gated. Only enable a clinical card for a partner you have a BAA with. ### 3. Capture the secret — it is shown once On confirmation, the surface displays the full secret token one time. This is the only moment the secret is ever visible — Practice Settings stores a hash, not the token, so it cannot show the value again on any later visit. Copy the secret immediately and place it in your secrets manager before you leave or close the screen. If you navigate away without copying it, the key still exists but its secret is unrecoverable; your only path is to delete that key and mint a new one (steps 1–3). After you have stored the secret, close the reveal. The list now shows the key as active, with its name and scopes, but never the secret again. ### 4. Share it with your integration partner Hand the secret to the partner through the secure channel you prepared — a password manager share or a secrets vault, scoped to just the people who operate the integration. Send the partner three things: - The secret token itself (through the secure channel, never inline in a message). - The base URL, https://api.huli.ai, with the FHIR R4 path under /fhir/R4/. - The list of scopes you granted, so the partner builds against exactly what the key allows and is not surprised by a on a resource you withheld. The partner authenticates by sending the token as an HTTP `Authorization: Bearer` header on every request. They do not call the token endpoint and they do not need your private keys — the admin bearer token is the credential as-is. Do not paste the secret into email, chat, a ticket, a shared spreadsheet, or a code repository, and do not screenshot the reveal screen into any of those. A token in a chat log is a token in everyone's search history. If it lands in one of those places even once, treat it as compromised and rotate it (step 6). ### 5. Verify the key works Run one authenticated request to confirm the key is live and correctly scoped. This example searches patients by name, so it needs a key with (or ). You can run it yourself or hand it to the partner. ```bash export HULI_API_KEY="" curl "https://api.huli.ai/fhir/R4/Patient?name=Fern%C3%A1ndez&_count=20" \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Accept: application/fhir+json" ``` A working key returns with a FHIR `Bundle` of type `searchset`. Encode the accent in the name (`á` becomes `%C3%A1`) so the search matches — this query looks up Doctora María Fernández's patients whose name starts with `Fernández`. ### 6. Rotate and revoke Rotation replaces a key's secret without an outage: mint a new key (steps 1–3), share it with the partner (step 4), let them cut over and verify (step 5), then revoke the old one. Two keys are valid at once during the overlap, so nothing breaks mid-cutover. Rotate on a schedule — a calendar reminder per partner — and immediately if a secret is ever exposed. To revoke, return to **Practice Settings → Integrations → API Keys**, find the key by the name you gave it in step 2, and revoke it. A revoked key stops authenticating right away; every request with it then returns with `HPB-00106` — see What can go wrong. Revoke a key the moment a partnership ends or a key is no longer in use — an unused active key is pure risk with no upside. The name you chose in step 2 is what makes rotation and revocation safe. When you hold three keys for three partners, `San Rafael — Lab results sync` tells you exactly which one to revoke; `key 3` does not. This is why the naming convention is worth the few extra seconds up front. ## What to verify - The new key appears in **Practice Settings → Integrations → API Keys** as active, with the name and scopes you intended. - You captured the secret on the one-time reveal and stored it in a secrets manager — not in email, chat, or a repository. - The verification request in step 5 returns with a `Bundle` of type `searchset`. - The granted scopes match the partner's stated need and no more. - You have a rotation reminder set and you know which list row to revoke when the partnership ends. ## What can go wrong The API returns errors as a FHIR `OperationOutcome`, not a bare string. The HTTP status and `issue[0].code` (the FHIR IssueType) classify the failure; the Huli code (`HPB-…`) is the prefix of `issue[0].diagnostics`, split on `": "`. There is no `details` object and no `text`. These are the failures you will see while standing up a key: `HPB-00106` — auth failed. The token is missing, malformed, or revoked. Confirm the header reads `Authorization: Bearer ` with a single space, that you pasted the full secret from the reveal screen, and that the key still shows as active in the list. After a revoke (step 6) this is the expected response for the old key. `HPB-00104` — insufficient scope. The token authenticated but lacks the scope the request needs — for the step 5 search, . The partner is calling a resource you did not grant. Re-mint the key with the right scopes, or confirm the partner is calling only what you granted. `HPB-00101` — validation error. A request parameter is malformed — most often an un-encoded accent in the `name` search. Encode `á` as `%C3%A1`. `HPB-00105` — rate limited. The key exceeded its request budget. The response carries a `Retry-After` header; wait that many seconds before retrying. A partner that trips this constantly is polling too aggressively — a workflow conversation, not a key problem. A representative `403` body: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "forbidden", "diagnostics": "HPB-00104: Insufficient scope" } ] } ``` A second `401`, `HPB-00107` (auth expired), applies to the short-lived access tokens issued by SMART Backend Services, not to an admin bearer token. If a partner sees it while using the key you minted here, they are sending a SMART access token instead of your admin bearer token — a sign they wired up the wrong auth mode. ## Next recipes - **Run your first authenticated Patient search** — read the `searchset` Bundle your verification request returns and recognize the four first-run failures in depth. - **Authenticate as a SMART Backend Services client** — when a partner needs short-lived, key-signed tokens (`client_credentials` + `private_key_jwt`, RS384) instead of a long-lived admin bearer token. - **Create and update a Patient** — move a partner from to and POST/PUT patient records. - **Write an Observation with a Provenance trail** — pair with so every write carries its audit record. ======================================================================== # Sync a daily patient list with the huli CLI # URL: https://developers.huli.ai/v1/recipes/daily-roster-sync-cli # Pull each day's patients and appointments with the huli CLI on a cron schedule — cursor-based pagination that resumes mid-walk on restart, plus 429 Retry-After backoff and id-level dedup at load. # Sync a daily patient list with the huli CLI Keep a daily mirror of your patients and appointments: schedule a nightly pull of every `Patient` and `Appointment` for one organization, write each page to durable storage, and resume from the exact page you left off when the host reboots mid-run. The loop is cursor-based, so a restart re-reads the last unfinished page and continues — it commits each page before advancing the checkpoint, and an id-level dedup at load time collapses any re-read page into a single row per resource. ## Audience You run server-to-server integrations and own a cron host. You have synced a paginated API before, you know what a checkpoint file buys you, and you want a patient-and-appointment mirror that survives a reboot — backed by an id-level dedup at load time, not a hope that the walk never repeats a page. ## You'll need - The huli CLI on the cron host. `huli version` should print a `1.x` build; confirm it speaks the v1 public API with `huli auth status` (below), which fails loudly against an older contract. - A SMART Backend Services client registered in HuliPractice (**Practice Settings → Integrations → API Keys**), holding these scopes: - — read plus search on Patient. - — read plus search on Appointment. - The client's RS384 private key on the cron host (file mode `0600`), plus the `client_id` issued at registration. - A writable checkpoint directory the cron user owns — this recipe uses `/var/lib/huli-sync`. SMART Backend Services issues a 5-minute access token from POST https://api.huli.ai/auth/token (`client_credentials` + `private_key_jwt`, RS384). The token endpoint is host-rooted — it sits at `/auth/token`, not under `/fhir`. The CLI mints and refreshes that token for you once `huli auth setup` has stored the client credentials; you never hand-build the assertion in the loop. This recipe uses huli auth setup (machine-to-machine, shipping today). Interactive browser login — huli auth login (Auth-Code + PKCE) — is forthcoming and not in the current CLI. Do not script against it yet. ## End state A cron job runs nightly at 02:15 America/Mexico_City. Each run walks every page of the `Patient` searchset, then every page of the `Appointment` searchset for Doctora María Fernández's organization at Clínica San Rafael, appending each page to a dated NDJSON file under `/var/lib/huli-sync`. A checkpoint file records the cursor of the last page committed. If the host reboots mid-run, the next start reads the checkpoint and resumes from that cursor — at worst re-reading the one page that was in flight. The step-5 load dedups on resource `id`, so the final mirror carries one row per `Patient` and `Appointment`. ## Steps ### 1. Store the machine-to-machine credentials once Register the client once. `huli auth setup` reads the private key, records the `client_id` and the token endpoint, and persists an encrypted profile under `~/.config/huli/`. Run it once as the cron user, not inside the cron job. huli auth setup --profile roster-sync --client-id $HULI_CLIENT_ID --private-key /etc/huli/roster-sync.pem --token-url https://api.huli.ai/auth/token Confirm the profile authenticates. `huli auth status` mints a throwaway access token against `/auth/token` and prints the resolved org and granted scopes without writing any data. huli auth status --profile roster-sync ```text profile: roster-sync client: roster-sync@clinica-san-rafael org: Clínica San Rafael (01965e2a-8c4d-7000-9001-0000000000b0) scopes: system/Patient.rs system/Appointment.rs token: valid, expires in 4m51s ``` If the scope line is missing `system/Appointment.rs`, the key was minted without it — re-mint in Practice Settings before scheduling. A scope gap surfaces at runtime as `HPB-00104`, not at setup. ### 2. Write the sync script The script walks one resource at a time. It reads the page cursor from a checkpoint file, calls the CLI for that page, appends the entries, then advances the checkpoint to the `next` cursor the page returned. Committing the output **before** advancing the checkpoint is what makes a restart safe: a crash between append and checkpoint-write re-reads the same page, and the loader in step 5 dedupes on resource `id`. Save this as `/usr/local/bin/huli-roster-sync.sh`. ```bash #!/usr/bin/env bash set -euo pipefail PROFILE="roster-sync" STATE_DIR="/var/lib/huli-sync" RUN_DATE="$(date +%F)" # e.g. 2026-06-02 OUT_DIR="${STATE_DIR}/${RUN_DATE}" mkdir -p "${OUT_DIR}" # Walk one FHIR resource type to exhaustion, resuming from a per-resource checkpoint. sync_resource() { local resource="$1" local ckpt="${STATE_DIR}/${resource}.cursor" local out="${OUT_DIR}/${resource}.ndjson" # Resume: a non-empty checkpoint means a prior run stopped mid-roster. local cursor="" if [[ -s "${ckpt}" ]]; then cursor="$(cat "${ckpt}")" echo "[$(date -Iseconds)] ${resource}: resuming from cursor ${cursor:0:16}…" else echo "[$(date -Iseconds)] ${resource}: starting fresh" fi while :; do # `huli fhir search` emits one JSON object per line: # {"page":[...entries...], "next":"|"} # --cursor "" requests the first page. The CLI handles token refresh, # 429 Retry-After backoff, and 5xx retries internally. local page_json page_json="$(huli fhir search "${resource}" \ --profile "${PROFILE}" \ --count 100 \ --cursor "${cursor}" \ --output ndjson-page)" # Append this page's entries, then advance the checkpoint. Output is # committed BEFORE the cursor moves, so a crash here re-reads this page. jq -c '.page[]' <<<"${page_json}" >>"${out}" cursor="$(jq -r '.next' <<<"${page_json}")" if [[ -z "${cursor}" || "${cursor}" == "null" ]]; then : >"${ckpt}" # roster exhausted: clear checkpoint echo "[$(date -Iseconds)] ${resource}: complete" break fi printf '%s' "${cursor}" >"${ckpt}" # durable resume point done } sync_resource Patient sync_resource Appointment ``` Make it executable. chmod 0755 /usr/local/bin/huli-roster-sync.sh The CLI absorbs rate-limit and transient-server retries so the loop stays linear. On `HPB-00105` it reads the `Retry-After` header, sleeps that many seconds, and re-issues the same page — the cursor does not advance, so no entries are skipped or doubled. On `5xx` it retries with exponential backoff. It surfaces (`HPB-00104` insufficient scope) and (`HPB-00106` auth failed) as a non-zero exit immediately, because those will not clear on retry. ### 3. Pin the cursor semantics Three invariants make the resume correct: - **The cursor is opaque and stable.** Treat `next` as a base64 token — never parse, truncate, or regenerate it. Passing yesterday's cursor into today's run resumes exactly where that token points; passing an empty string starts a fresh full walk. - **Append, then advance.** The script writes the page to NDJSON before it overwrites the checkpoint. A power loss between those two lines costs you one re-read of a single page on restart, never a gap. Reversing the order — advancing the checkpoint first — is the one change that can drop a page; do not do it. - **The raw NDJSON can repeat a page.** A resume re-reads the in-flight page, and because the cursor orders by a server-side sort key, rows created mid-walk can re-surface on a later page. The walk itself does not promise a duplicate-free file — the step-5 id-level dedup is what makes the final mirror one row per resource. Treat the NDJSON as an at-least-once stream, not exactly-once. ### 4. Schedule the cron entry Run nightly at 02:15 in the clinic's timezone. The `CRON_TZ` prefix pins the schedule to America/Mexico_City regardless of the host clock. A flock guard stops a long run from overlapping the next night's trigger. Install it as the `huli-sync` user's per-user crontab. A per-user crontab line has **no username field** — the schedule goes straight to the command: ```cron CRON_TZ=America/Mexico_City 15 2 * * * /usr/bin/flock -n /var/lib/huli-sync/.lock /usr/local/bin/huli-roster-sync.sh >> /var/log/huli-roster-sync.log 2>&1 ``` Save that as `/etc/huli/roster-sync.crontab` (owned by the cron user) and install it for the `huli-sync` service user: crontab -u huli-sync /etc/huli/roster-sync.crontab A per-user crontab and `/etc/cron.d/` are two different mechanisms — do not mix their syntax. A `/etc/cron.d/` system-crontab line carries a username field (`15 2 * * * huli-sync /usr/bin/flock …`) and you install it by dropping the file into `/etc/cron.d/` with no `crontab` command. A per-user crontab (the form above) omits the username field and is installed with `crontab -u`. Add a username field to a per-user crontab and `crontab` misparses `huli-sync` as the command. `flock -n` makes the job idempotent against overlap: if a run is still going when the next 02:15 fires, the second invocation exits without starting a parallel walk. Combined with the per-resource checkpoint, an overrun simply continues on the following night from where it stopped. ### 5. Load with id-level dedup A resume re-reads at most the in-flight page, and a mid-walk write can re-surface a row on a later page. Dedup on the FHIR resource `id` at load time so any repeat collapses to a single row. This `sort -u`-then-upsert pattern keeps the loader idempotent no matter how many times a page was re-read. ```bash for resource in Patient Appointment; do jq -r '[.id, (.|tojson)] | @tsv' \ "/var/lib/huli-sync/$(date +%F)/${resource}.ndjson" \ | sort -u -k1,1 \ | your-loader upsert --table "fhir_${resource,,}" --key id done ``` `sort -u -k1,1` keeps the first row per `id`; `your-loader upsert --key id` makes the database write a no-op when the row already exists. Either layer alone makes the load idempotent — running both is deliberate redundancy for a cron job you will not be watching. ## What to verify - `huli auth status --profile roster-sync` resolves the org and lists both `system/Patient.rs` and `system/Appointment.rs`. - After a full run, both `Patient.cursor` and `Appointment.cursor` are empty — a non-empty checkpoint means the walk stopped partway and will resume next start. - If the first page's searchset Bundle reports `total`, the NDJSON line count for each resource should approximate it, allowing for one duplicated page from a resume. Treat it as a sanity check, not an exact reconciliation — a searchset `total` is an estimate the server may omit or revise across pages. - After the step-5 load, row counts in `fhir_patient` / `fhir_appointment` equal the distinct `id` count in the NDJSON — no duplicates survived the dedup. - Kill the script mid-run (`Ctrl-C` during a page), re-run it, and confirm the output resumes from the saved cursor rather than restarting from page one. ## What can go wrong Every error is a FHIR `OperationOutcome`. Branch on the HTTP status and `issue[0].code` (the FHIR IssueType); the Huli code (`HPB-…`) is the prefix of `issue[0].diagnostics` — split on `": "` to read it. There is no `details` object and no `coding`. `HPB-00105` — rate limited. The CLI handles this for you: it reads `Retry-After`, sleeps, and re-issues the same page without advancing the cursor. If you ever drive the loop with raw `curl` instead, you must replicate that — honor `Retry-After` and retry the **same** cursor, never the next one. ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "throttled", "diagnostics": "HPB-00105: Rate limit exceeded" } ] } ``` `HPB-00104` — insufficient scope. The token authenticated but lacks `system/Appointment.rs` (or `system/Patient.rs`). The CLI exits non-zero immediately rather than retrying. Re-mint the key in Practice Settings with both `.rs` scopes selected. `HPB-00106` — auth failed. The `client_assertion` was rejected — usually a `client_id`/private-key mismatch or a clock skew that pushed the assertion's `iat`/`exp` out of tolerance. Re-run `huli auth status` to isolate setup from the loop, and verify the cron host's clock is NTP-synced. `HPB-00107` — auth expired. The 5-minute access token lapsed mid-walk on a slow page. The CLI refreshes automatically before each request; you only see this if you pinned a stale token by hand. Let the CLI manage the token — never cache the bearer across pages yourself. `HPB-00101` — validation error. A search parameter or the `_cursor` is malformed — most often a checkpoint file that was hand-edited or truncated. Treat the cursor as opaque: if a checkpoint is corrupt, clear it (`: > Patient.cursor`) to restart that resource's walk from page one rather than patching the token. A corrupt or partially-written checkpoint produces `HPB-00101`, not a silent wrong-page resume. If a crash truncates the checkpoint mid-write, the next run rejects the malformed cursor loudly. Recover by clearing the file and re-walking from the start — the step-5 id-level dedup absorbs the full re-read without leaving duplicate rows in the mirror. ## Next recipes - **Authenticate as a SMART Backend Service** — the `client_credentials` + `private_key_jwt` (RS384) handshake the CLI performs under `huli auth setup`, end to end, for when you need to drive `/auth/token` yourself. - **Incremental sync with `_lastUpdated`** — narrow the nightly walk to records changed since the last run instead of a full pull. - **Paginate a large patient list** — the cursor / `link[rel=next]` mechanics this loop rides on, walked by hand against a single searchset. - **Mirror Encounters and Observations** — extend the same checkpointed loop to the clinical resources, adding `system/Encounter.rs` and `system/Observation.rs`. ======================================================================== # Debugging a failed FHIR search # URL: https://developers.huli.ai/v1/recipes/debugging-a-failed-fhir-search # Read the OperationOutcome a failed FHIR R4 search returns, map each status to its HPB code and fix, and trace the correlation id to the audit record Huli support needs. # Debugging a failed FHIR search A FHIR search that returns anything other than `200 OK` hands you a structured `OperationOutcome`. Read it correctly and you resolve most failures yourself in one pass: the HTTP status names the category, the body carries the Huli code, and the response headers carry the correlation id you hand to support when the failure is on our side. ## Audience You integrate against the Huli FHIR API, you already authenticate (admin bearer token or SMART Backend Services), and you have a search returning a non-`200` status. You read JSON without a viewer and you want a repeatable triage path instead of a guess. ## You'll need - A request that reproduces the failure — the exact URL, method, and the token you sent. - `curl`, or a Go, Python, or Node HTTP client if you prefer a language client. - The ability to capture **response headers**, not just the body. `curl -i` or `curl -D -` prints them; most language clients expose them on the response object. Every error from the FHIR API is a FHIR `OperationOutcome`, never a bare string and never the `{ "error": { "code", "message" } }` shape used by Huli's internal APIs. Decode the body as JSON and read `issue[0]` — that is where the machine-readable signal lives. ## End state You can take any failed search, classify it from `issue[0].code` and the `HPB-…` prefix in `issue[0].diagnostics`, apply the documented fix for that class, and — when the cause is not on your side — extract the correlation id from the response headers and give support the four facts they need to find the matching audit record. ## Steps ### 1. Capture the response with its headers Replay the failing search with headers visible. The `-i` flag prints the status line and all response headers ahead of the body. ```bash curl -i "https://api.huli.ai/fhir/R4/Patient?name=Fern%C3%A1ndez&_count=20" \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Accept: application/fhir+json" ``` A failing response looks like this on the wire — status line, headers (correlation id included), then the `OperationOutcome` body: ```http HTTP/2 403 content-type: application/fhir+json x-correlation-id: 01965e9f-2a17-7000-9007-0000000000c4 { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "forbidden", "diagnostics": "HPB-00104: Insufficient scope" } ] } ``` ### 2. Read the OperationOutcome The body is always this shape — `resourceType`, then an `issue` array. Each issue carries exactly three fields. There is no `details`, no `coding`, no `text`. ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "forbidden", "diagnostics": "HPB-00104: Insufficient scope" } ] } ``` Two fields drive your triage: - `issue[0].code` — the FHIR `IssueType`, derived from the HTTP status: `400` → `invalid`, `401` → `security`, `403` → `forbidden`, `404` → `not-found`, `409` → `conflict`, `429` → `throttled`, `5xx` → `exception`. - `issue[0].diagnostics` — a human-readable message prefixed with the Huli code. The `HPB-…` code is the prefix; split on `": "` to extract it. Pull both apart programmatically rather than substring-matching the whole sentence. Split on the first `": "`: the head is the `HPB-…` code, the tail is the message. If the string carries no `": "`, treat that as a non-conforming body (see "What can go wrong") rather than assuming the whole sentence is the code. :::CodeGroup ```typescript const url = new URL('https://api.huli.ai/fhir/R4/Patient'); url.searchParams.set('name', 'Fernández'); url.searchParams.set('_count', '20'); const resp = await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}`, Accept: 'application/fhir+json', }, }); if (resp.status !== 200) { const outcome = (await resp.json()) as { issue: { code: string; diagnostics: string }[]; }; const issue = outcome.issue[0]; const fhirCode = issue.code; // e.g. "forbidden" const diagnostics = issue.diagnostics; // "HPB-00104: Insufficient scope" const sep = diagnostics.indexOf(': '); // No "HPB-...: " prefix — unexpected/non-conforming body. const hpbCode = sep === -1 ? '' : diagnostics.slice(0, sep); const message = sep === -1 ? diagnostics : diagnostics.slice(sep + 2); const correlationId = resp.headers.get('X-Correlation-Id'); const retryAfter = resp.headers.get('Retry-After'); // set only on 429 console.log(resp.status, fhirCode, hpbCode, message); console.log('correlation id:', correlationId); if (retryAfter) { console.log('retry after (s):', retryAfter); } } ``` ```python import requests resp = requests.get( "https://api.huli.ai/fhir/R4/Patient", params={"name": "Fernández", "_count": 20}, headers={ "Authorization": f"Bearer {API_KEY}", "Accept": "application/fhir+json", }, timeout=30, ) if resp.status_code != 200: outcome = resp.json() issue = outcome["issue"][0] fhir_code = issue["code"] # e.g. "forbidden" diagnostics = issue["diagnostics"] # "HPB-00104: Insufficient scope" hpb_code, sep, message = diagnostics.partition(": ") if not sep: # No "HPB-...: " prefix — unexpected/non-conforming body. hpb_code, message = "", diagnostics correlation_id = resp.headers.get("X-Correlation-Id") retry_after = resp.headers.get("Retry-After") # set only on 429 print(resp.status_code, fhir_code, hpb_code, message) print("correlation id:", correlation_id) if retry_after: print("retry after (s):", retry_after) ``` ```java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; public class ClassifyFhirSearch { public static void main(String[] args) throws Exception { // Let the client percent-encode the accent — never pre-encode here. String name = URLEncoder.encode("Fernández", StandardCharsets.UTF_8); URI uri = URI.create( "https://api.huli.ai/fhir/R4/Patient?name=" + name + "&_count=20"); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(uri) .header("Authorization", "Bearer " + System.getenv("HULI_API_KEY")) .header("Accept", "application/fhir+json") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { String body = response.body(); // Hand-parse the two fields we triage on; a real client would // use a JSON library to read issue[0].code / .diagnostics. String fhirCode = extract(body, "code"); // e.g. "forbidden" String diagnostics = extract(body, "diagnostics"); // "HPB-00104: Insufficient scope" int sep = diagnostics.indexOf(": "); // No "HPB-...: " prefix — unexpected/non-conforming body. String hpbCode = sep == -1 ? "" : diagnostics.substring(0, sep); String message = sep == -1 ? diagnostics : diagnostics.substring(sep + 2); String correlationId = response.headers().firstValue("X-Correlation-Id").orElse(null); String retryAfter = // set only on 429 response.headers().firstValue("Retry-After").orElse(null); System.out.println(response.statusCode() + " " + fhirCode + " " + hpbCode + " " + message); System.out.println("correlation id: " + correlationId); if (retryAfter != null) { System.out.println("retry after (s): " + retryAfter); } } } // Minimal value lookup for a flat "key":"value" — illustrative only. static String extract(String json, String key) { String needle = "\"" + key + "\":\""; int start = json.indexOf(needle); if (start == -1) { return ""; } start += needle.length(); int end = json.indexOf('"', start); return end == -1 ? "" : json.substring(start, end); } } ``` ```go package main import ( "encoding/json" "fmt" "io" "net/http" "strings" ) type operationOutcome struct { ResourceType string `json:"resourceType"` Issue []struct { Severity string `json:"severity"` Code string `json:"code"` Diagnostics string `json:"diagnostics"` } `json:"issue"` } func classify(resp *http.Response) { body, _ := io.ReadAll(resp.Body) defer resp.Body.Close() var oo operationOutcome if err := json.Unmarshal(body, &oo); err != nil || len(oo.Issue) == 0 { fmt.Printf("non-OperationOutcome body (status %d): %s\n", resp.StatusCode, body) return } issue := oo.Issue[0] hpbCode, message, found := strings.Cut(issue.Diagnostics, ": ") if !found { // No "HPB-...: " prefix — unexpected/non-conforming body. hpbCode, message = "", issue.Diagnostics } fmt.Printf("status=%d fhirCode=%s hpb=%s message=%q\n", resp.StatusCode, issue.Code, hpbCode, message) fmt.Printf("correlation-id=%s\n", resp.Header.Get("X-Correlation-Id")) if ra := resp.Header.Get("Retry-After"); ra != "" { fmt.Printf("retry-after=%s\n", ra) } } ``` ::: ### 3. Map the failure to its fix Match on the status plus the `HPB-…` code, then apply the fix. These four cover the overwhelming majority of failed searches. `security` / `HPB-00106` — auth failed. The token is missing, malformed, or revoked. Confirm the header reads `Authorization: Bearer ` with a single space, and that the variable holding it is actually populated in this shell. A second `401`, `HPB-00107` (auth expired), means a SMART Backend Services access token has passed its 5-minute TTL — mint a fresh one and retry. An admin bearer token does not expire on a timer, so `HPB-00107` against an admin key usually means a SMART access token is being sent on a request you intended to authenticate with the admin key. `forbidden` / `HPB-00104` — insufficient scope. The token authenticated but lacks the scope this search needs. A `Patient` name search needs ; a write needs . Re-mint the key in **Practice Settings → Integrations → API Keys** with the read+search (`rs`) scope selected for every resource you query. `invalid` / `HPB-00101` — validation error. A search parameter is malformed or unknown — most often an un-encoded accent or a typo'd parameter name. URL-encode `á` as `%C3%A1`, and check each parameter against the search reference for that resource. Note that hand-built `curl` URLs need the literal `%C3%A1`, while language clients percent-encode for you — passing a pre-encoded value into a client library double-encodes it and the search matches nothing. `throttled` / `HPB-00105` — rate limited. You exceeded the per-key request budget. Read the `Retry-After` response header (in seconds) and back off for that long before retrying. Add jitter if several workers share one key. ### 4. Surface the correlation id When the failure is on Huli's side — a `5xx` `exception`, or a `4xx` whose fix you have already applied and which still fails — the correlation id ties your request to our server-side record. It is on the response, not the body: ```bash # Print only the correlation id from a failing request curl -s -o /dev/null -D - \ "https://api.huli.ai/fhir/R4/Patient?name=Fern%C3%A1ndez" \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Accept: application/fhir+json" \ | grep -i '^x-correlation-id:' ``` In a language client, read the `X-Correlation-Id` response header (shown in the Python and Go samples in step 2). Capture and log it on every non-`200` response as a matter of course — it is the single fastest way for support to locate your request, and it is gone once the response is discarded. ### 5. Hand support the audit facts Each FHIR search Huli serves writes an audit record server-side, keyed by the same correlation id you captured. Support locates the matching record from the facts you provide — there is no client-facing audit-lookup endpoint, so give them everything needed to find it on the first try: - The **correlation id** from `X-Correlation-Id`. - The **UTC timestamp** of the request, ISO-8601 with offset (e.g. `2026-06-02T14:22:09-06:00`). - The **exact URL** you called, including query parameters (redact nothing — the audit record stores the search parameters and they must match). - The **`client_id`** of the credential you authenticated with (the API key's client identifier, not the secret). Never send your bearer token, client secret, or private key to support. The `client_id` and correlation id are sufficient to locate the record; the secret material is not needed and must not leave your environment. ## What to verify - The response body parses as JSON and `resourceType` is `OperationOutcome`. - `issue[0].code` matches the HTTP status per the mapping in step 2. - The `HPB-…` prefix you split out of `issue[0].diagnostics` matches the status (`HPB-00101`/400, `HPB-00104`/403, `HPB-00105`/429, `HPB-00106`/401, `HPB-00107`/401). - On a `429`, a `Retry-After` header is present and you honored it before retrying. - You captured `X-Correlation-Id` before discarding the response. ## What can go wrong - **Substring-matching the whole `diagnostics` sentence.** The message text can change; the `HPB-…` prefix and `issue[0].code` are the stable contract. Split on `": "` and branch on the code, not the prose. - **A `diagnostics` string with no `HPB-…: ` prefix.** Every conforming error leads with `HPB-…: `. If your split finds no `": "` separator, you are looking at a non-conforming or unexpected body (a proxy error page, a truncated response) — fall back to the HTTP status and the correlation id rather than treating the whole string as a code. - **Looking for `issue.details` or a `coding` array.** Neither exists on this API. The only fields on an issue are `severity`, `code`, and `diagnostics`. - **Reading the correlation id from the request instead of the response.** The id is assigned server-side and returned on the `X-Correlation-Id` _response_ header. If you only logged the request, you have nothing to give support. - **Retrying a `429` immediately.** Without honoring `Retry-After` you compound the throttle. Back off for the advertised seconds, then retry. - **Treating `HPB-00107` (auth expired) as `HPB-00106` (auth failed).** Expired means the credential was valid and timed out — refresh the SMART access token rather than re-checking the key. The fixes differ. - **Mixing up the token and discovery hosts.** The token endpoint is host-rooted at `POST https://api.huli.ai/auth/token`, while SMART discovery and JWKS are issuer-rooted under `/fhir` (`https://api.huli.ai/fhir/.well-known/smart-configuration` and `https://api.huli.ai/fhir/.well-known/jwks.json`). A `401` that resists every credential fix is often a request sent to the wrong path. ## Next recipes - **Run your first authenticated Patient search** — the green-path single request these failures are the inverse of. - **Authenticate as a SMART Backend Service** — `client_credentials` + `private_key_jwt` (RS384) and the 5-minute token lifecycle behind `HPB-00107`. - **Paginate a large patient list** — follow the `Bundle.link` entry with `relation: "next"` once your search returns `200`. ======================================================================== # Fetching a patient's full record # URL: https://developers.huli.ai/v1/recipes/fetching-a-patient-record # Pull a patient's whole clinical record — encounters, observations, notes, documents, medications, and orders — in one Patient/$everything Bundle, scope-filtered, with date and type narrowing. # Fetching a patient's full record Pull everything you are allowed to see about one patient in a single read. The `Patient/$everything` operation aggregates the patient's clinical record — `Encounter`, `Observation`, `Composition`, `DocumentReference`, `MedicationRequest`, and `ServiceRequest`, plus the `Patient` itself — into one `searchset` `Bundle`. It is **read-only** and **scope-filtered**: each resource type appears only if your token carries read scope for it, and the Bundle tells you, in-band, which types it withheld. This is the fastest way to hydrate a record without orchestrating six separate searches. The trade-off to understand up front: the result is shaped by your scopes. A token that can read encounters but not documents gets the encounters and a machine-readable note that documents were held back — never a silent omission. ## Audience You build a record-sync, a care-summary view, or a migration that ingests a patient's full chart. You read a `Bundle` without a viewer, you know what a FHIR reference is, and you want one call that returns as much of a patient's record as your token is entitled to. ## You'll need - A bearer token from HuliPractice (**Practice Settings → Integrations → API Keys**), or a SMART Backend Services access token. See [Bearer Tokens](/v1/auth/bearer) and [`POST /auth/token`](/v1/auth). - system/Patient.rs at minimum — the operation gates on patient read. Then add a read scope for **each** type you want included: - and (the BAA-gated **Clinical information** card), plus and . - and (the BAA-gated **Clinical information** card). A type whose read scope is absent is **withheld**, not an error — the call still succeeds. - The `id` of the patient. Resolve it with [a Patient search](/v1/recipes/getting-started-patient-search) if you only hold a name. - `curl`, or Node, Python, Java, or Go. `$everything` only ever **reads**. A search-only token (`.s` without `.r`) does not satisfy the per-type instance-read gate — disclosing a type's instances in the aggregate requires an instance read grant (`.r`/`.rs`/`.cru`/`.crud`), so a type you can only search is reported as withheld. ## End state You hold a `200 OK` whose body is a `searchset` `Bundle`. The patient is the first `match` entry; each clinical resource you are scoped for follows as further `match` entries. If any type was withheld for lack of scope, the Bundle carries a `meta.tag` of `scope-filtered` and an `OperationOutcome` entry naming the withheld types. ## Steps ### 1. Export the token and the patient id ```bash export HULI_TOKEN="" export PATIENT_ID="01965e2a-8c4d-7000-9001-0000000000a2" ``` ### 2. Call $everything `GET` the operation on the patient instance. Optional parameters narrow the result: - `start` / `end` — bound the clinical resources to a date window (`YYYY-MM-DD`). - `_type` — a comma-separated list to include only specific types (e.g. `_type=Encounter,Observation`). Omit it to include every type you are scoped for. - `_count` — the per-type page cap (default 50). When a type has more rows than the cap, the Bundle flags it as truncated. :::CodeGroup ```bash curl "https://api.huli.ai/fhir/R4/Patient/01965e2a-8c4d-7000-9001-0000000000a2/\$everything?start=2026-01-01&_count=50" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` ```typescript const id = process.env.PATIENT_ID!; const params = new URLSearchParams({ start: '2026-01-01', _count: '50' }); const resp = await fetch(`https://api.huli.ai/fhir/R4/Patient/${id}/$everything?${params}`, { headers: { Authorization: `Bearer ${process.env.HULI_TOKEN}`, Accept: 'application/fhir+json', }, }); const bundle = (await resp.json()) as { entry?: { resource: { resourceType: string }; search?: { mode: string } }[]; meta?: { tag?: { code: string }[] }; }; // match entries are the record; outcome entries carry withheld/truncation notices. const matches = bundle.entry?.filter((e) => e.search?.mode === 'match') ?? []; const notices = bundle.entry?.filter((e) => e.search?.mode === 'outcome') ?? []; console.log(matches.map((e) => e.resource.resourceType)); if (bundle.meta?.tag?.some((t) => t.code === 'scope-filtered')) { console.log('some types withheld:', notices); } ``` ```python import os import requests patient_id = os.environ["PATIENT_ID"] resp = requests.get( f"https://api.huli.ai/fhir/R4/Patient/{patient_id}/$everything", params={"start": "2026-01-01", "_count": 50}, headers={ "Authorization": f"Bearer {os.environ['HULI_TOKEN']}", "Accept": "application/fhir+json", }, timeout=30, ) bundle = resp.json() matches = [e for e in bundle.get("entry", []) if e.get("search", {}).get("mode") == "match"] notices = [e for e in bundle.get("entry", []) if e.get("search", {}).get("mode") == "outcome"] print([e["resource"]["resourceType"] for e in matches]) if any(t.get("code") == "scope-filtered" for t in bundle.get("meta", {}).get("tag", [])): print("some types withheld", notices) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class PatientEverything { public static void main(String[] args) throws Exception { String id = System.getenv("PATIENT_ID"); String url = "https://api.huli.ai/fhir/R4/Patient/" + id + "/$everything?start=2026-01-01&_count=50"; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("Authorization", "Bearer " + System.getenv("HULI_TOKEN")) .header("Accept", "application/fhir+json") .GET() .build(); HttpResponse response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); // Parse with a JSON library: match entries are the record, outcome entries // (search.mode=outcome) carry the withheld/truncation notices. System.out.println(response.statusCode()); System.out.println(response.body()); } } ``` ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { id := os.Getenv("PATIENT_ID") url := "https://api.huli.ai/fhir/R4/Patient/" + id + "/$everything?start=2026-01-01&_count=50" req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("HULI_TOKEN")) req.Header.Set("Accept", "application/fhir+json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { panic(err) } // entry[].search.mode is "match" (the record) or "outcome" (withheld/truncation notices). fmt.Printf("%d\n%s\n", resp.StatusCode, body) } ``` ::: First, resolve a real patient id in your sandbox: Then run `$everything` against it: ### 3. Read the Bundle — matches, and the in-band notices Every record resource is an `entry` with `search.mode: "match"`; the `total` counts only those. Any notice — withheld types, truncation — rides as an extra `entry` with `search.mode: "outcome"` carrying an `OperationOutcome`, and does **not** count toward `total`. ```json { "resourceType": "Bundle", "type": "searchset", "total": 3, "meta": { "tag": [ { "system": "https://fhir.huli.ai/r4/CodeSystem/bundle-tags", "code": "scope-filtered" } ] }, "link": [ { "relation": "self", "url": "https://api.huli.ai/fhir/R4/Patient/01965e2a-8c4d-7000-9001-0000000000a2/$everything?start=2026-01-01&_count=50" } ], "entry": [ { "fullUrl": "https://api.huli.ai/fhir/R4/Patient/01965e2a-8c4d-7000-9001-0000000000a2", "resource": { "resourceType": "Patient", "id": "01965e2a-8c4d-7000-9001-0000000000a2" }, "search": { "mode": "match" } }, { "fullUrl": "https://api.huli.ai/fhir/R4/Encounter/01965e2a-8c4d-7000-9060-0000000000e9", "resource": { "resourceType": "Encounter", "id": "01965e2a-8c4d-7000-9060-0000000000e9" }, "search": { "mode": "match" } }, { "fullUrl": "https://api.huli.ai/fhir/R4/Observation/01965e2a-8c4d-7000-9080-0000000000a7", "resource": { "resourceType": "Observation", "id": "01965e2a-8c4d-7000-9080-0000000000a7" }, "search": { "mode": "match" } }, { "resource": { "resourceType": "OperationOutcome", "issue": [ { "severity": "information", "code": "informational", "diagnostics": "The following resource types were withheld because the access token lacks read scope for them: DocumentReference, MedicationRequest." } ] }, "search": { "mode": "outcome" } } ] } ``` Three things to lift from the Bundle: - **The `meta.tag` of `scope-filtered`** is a fast machine signal that the result is incomplete by scope — check it before treating the Bundle as the whole record. - **The `information` outcome** names the **withheld** types (type names only — never PHI). Add the missing read scopes to the key if you need them. - **A `warning` outcome** (not shown above) names **truncated** types — a type had more rows than `_count`. Narrow `start`/`end` or raise `_count`, then re-read for the rest. A separate `warning` flags any type that failed to read (partial results), so a single type's outage never fails the whole call. ## What to verify - HTTP status is `200`. `resourceType` is `Bundle`, `type` is `searchset`. - The first `match` entry is the `Patient`, and `total` equals the number of `match` entries. - Every type you hold read scope for is present (within your date window and `_count`). - If `meta.tag` is `scope-filtered`, the `information` outcome lists exactly the types you did not scope for. ## What can go wrong All errors return a FHIR `OperationOutcome` — `{severity, code, diagnostics}`, no `details` object. Branch on the HTTP status and `issue[0].code`; the Huli code is the prefix of `issue[0].diagnostics`, split on `": "`. `HPB-00102` — **patient not found.** The id does not name a patient in your organization (a patient outside your organization is invisible, not forbidden). Confirm the id and the token's organization. `HPB-00104` — **insufficient scope.** The token lacks the baseline system/Patient.rs the operation gates on. (Lacking a _per-type_ read scope does not cause a `403` — that type is withheld in-band instead.) `HPB-00101` — **validation error.** The patient id is malformed, or a parameter is invalid. Use a well-formed UUID and `YYYY-MM-DD` dates. Withheld and truncated types are **not** errors — the call returns `200` with the partial record and the in-band notices. Treat the `scope-filtered` tag and the `outcome` entries as the contract for "what is missing and why", rather than inferring completeness from the absence of an error. ## Next recipes - **[Writing and amending a clinical note](/v1/recipes/writing-a-clinical-note)** — read or amend the `Composition` notes that appear in the aggregate. - **[Uploading a document](/v1/recipes/uploading-a-document)** — add the `DocumentReference` attachments the aggregate surfaces. - **[Wiring a read-only partner](/v1/recipes/wiring-a-read-only-partner)** — the per-resource search alternative when you want to page one type at a time instead of one aggregate read. ======================================================================== # Run your first authenticated Patient search # URL: https://developers.huli.ai/v1/recipes/getting-started-patient-search # Send a FHIR R4 Patient name search with an admin bearer token — request, searchset Bundle, and the four errors you'll hit first. # Run your first authenticated Patient search Run one authenticated request against the FHIR API, read the `searchset` Bundle it returns, and recognize the four failures that account for most first-run support tickets. One request, one round-trip — the whole loop fits in a single terminal session. Want to try this without a production credential? The [playground](/playground) runs this exact search against a sandbox organization with fabricated patients — get a key via the [Sandbox quickstart](/v1/recipes/sandbox-quickstart). ## Audience You integrate clinical systems and have called a FHIR R4 server before. You know what a `Bundle` is, you read JSON without a viewer, and you want a single green request before you wire up the rest of your integration. ## You'll need - An admin bearer token from HuliPractice (**Settings → Integrations → API Keys**). An admin-role user on your organization mints it; the token is shown once. - The system/Patient.rs scope on that token. `rs` grants read plus search, which is what this request uses. system/Patient.cru also works. - `curl`, or one of Node 18+ / Python 3.9+ / JDK 11+ / Go 1.22+ if you prefer a language client. The admin bearer token is a long-lived credential scoped to one organization. It does not expire on a timer the way SMART Backend Services tokens do. Treat it as a secret: keep it in an environment variable or a secrets manager, never in source control or a client bundle. ## End state You hold a `200 OK` whose body is a FHIR `Bundle` of type `searchset` containing the `Patient` resources whose name matches your query — for this recipe, Doctora María Fernández's patients at Clínica San Rafael that match `Fernández`. ## Steps ### 1. Export the token ```bash export HULI_API_KEY="" ``` Confirm it is set: ```bash echo $HULI_API_KEY ``` ### 2. Run the search The `name` parameter does a case- and accent-insensitive prefix match across the patient's name parts. URL-encode the accent (`á` → `%C3%A1`). :::CodeGroup ```bash curl "https://api.huli.ai/fhir/R4/Patient?name=Fern%C3%A1ndez&_count=20" \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Accept: application/fhir+json" ``` ```typescript const params = new URLSearchParams({ name: 'Fernández', _count: '20' }); const resp = await fetch(`https://api.huli.ai/fhir/R4/Patient?${params}`, { headers: { Authorization: `Bearer ${process.env.HULI_API_KEY}`, Accept: 'application/fhir+json', }, }); console.log(resp.status); console.log(await resp.json()); ``` ```python import os import requests resp = requests.get( "https://api.huli.ai/fhir/R4/Patient", params={"name": "Fernández", "_count": 20}, headers={ "Authorization": f"Bearer {os.environ['HULI_API_KEY']}", "Accept": "application/fhir+json", }, timeout=30, ) print(resp.status_code) print(resp.json()) ``` ```java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; public class PatientSearch { public static void main(String[] args) throws Exception { String query = "name=" + URLEncoder.encode("Fernández", StandardCharsets.UTF_8) + "&_count=" + URLEncoder.encode("20", StandardCharsets.UTF_8); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Patient?" + query)) .header("Authorization", "Bearer " + System.getenv("HULI_API_KEY")) .header("Accept", "application/fhir+json") .GET() .build(); HttpResponse response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.statusCode()); System.out.println(response.body()); } } ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" ) func main() { endpoint, err := url.Parse("https://api.huli.ai/fhir/R4/Patient") if err != nil { panic(err) } q := endpoint.Query() q.Set("name", "Fernández") q.Set("_count", "20") endpoint.RawQuery = q.Encode() req, err := http.NewRequest(http.MethodGet, endpoint.String(), nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("HULI_API_KEY")) req.Header.Set("Accept", "application/fhir+json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { panic(err) } // Production code branches on status and decodes the OperationOutcome on // the error paths. The "What can go wrong" section maps each code. switch resp.StatusCode { case http.StatusOK: fmt.Printf("200 OK\n%s\n", body) case http.StatusUnauthorized: // HPB-00106 auth failed / HPB-00107 auth expired fmt.Printf("401 unauthorized\n%s\n", body) case http.StatusForbidden: // HPB-00104 insufficient scope fmt.Printf("403 forbidden\n%s\n", body) case http.StatusBadRequest: // HPB-00101 validation error fmt.Printf("400 bad request\n%s\n", body) case http.StatusTooManyRequests: // HPB-00105 rate limited fmt.Printf("429 rate limited (Retry-After: %s)\n%s\n", resp.Header.Get("Retry-After"), body) default: fmt.Printf("%d\n%s\n", resp.StatusCode, body) } } ``` ::: No Fernández in your sandbox? Drop the `name` filter entirely (`/fhir/R4/Patient?_count=20`) — every sandbox has *some* patients, just not necessarily this one. See [your sandbox patients](/playground/roster) for a curated list of real ids to search by. The TypeScript, Python, Java, and Go clients percent-encode query values for you — pass the raw accented string (`Fernández`) and let the library encode it. Only hand-built URLs, like the `curl` above, need the literal `%C3%A1`. Setting `name` to a pre-encoded `Fern%C3%A1ndez` in a client library double-encodes it to `%25C3%25A1` and the search matches nothing. ### 3. Read the searchset Bundle A `200 OK` returns a `Bundle` of type `searchset`. The patients live under `entry[].resource` and `total` is the match count. When the result set spans more than one page, the `Bundle` carries a `link` entry with `relation: "next"` holding the cursor; this two-match example fits on one page, so no `next` link appears. ```json { "resourceType": "Bundle", "id": "01965e2a-8c4d-7000-9001-0000000000a1", "meta": { "lastUpdated": "2026-06-01T09:12:44.000-06:00" }, "type": "searchset", "total": 2, "link": [ { "relation": "self", "url": "https://api.huli.ai/fhir/R4/Patient?name=Fern%C3%A1ndez&_count=20" } ], "entry": [ { "fullUrl": "https://api.huli.ai/fhir/R4/Patient/01965e2a-8c4d-7000-9001-0000000000a2", "resource": { "resourceType": "Patient", "id": "01965e2a-8c4d-7000-9001-0000000000a2", "meta": { "versionId": "4", "lastUpdated": "2026-05-28T16:03:09.000-06:00", "profile": ["https://fhir.huli.ai/r4/StructureDefinition/HuliPatient"] }, "active": true, "name": [ { "use": "official", "family": "Fernández", "given": ["Ana", "Lucía"], "extension": [ { "url": "https://fhir.huli.ai/r4/StructureDefinition/second-lastname", "valueString": "Ramírez" } ] } ], "gender": "female", "birthDate": "1985-09-22", "telecom": [ { "system": "phone", "value": "+52 33 2145 8890", "use": "mobile" } ], "address": [ { "use": "home", "line": ["Calle Morelos 408, Col. Americana"], "city": "Guadalajara", "state": "Jalisco", "postalCode": "44160", "country": "MX" } ], "managingOrganization": { "reference": "Organization/01965e2a-8c4d-7000-9001-0000000000b0" } } }, { "fullUrl": "https://api.huli.ai/fhir/R4/Patient/01965e2a-8c4d-7000-9001-0000000000a3", "resource": { "resourceType": "Patient", "id": "01965e2a-8c4d-7000-9001-0000000000a3", "active": true, "name": [ { "use": "official", "family": "Fernández", "given": ["Carlos"], "extension": [ { "url": "https://fhir.huli.ai/r4/StructureDefinition/second-lastname", "valueString": "Ortega" } ] } ], "gender": "male", "birthDate": "1991-02-11" } } ] } ``` The second entry (Carlos) is trimmed for brevity to the fields that differ from the first — `meta`, `telecom`, `address`, and `managingOrganization` are omitted here, not absent on the wire. The API returns the same resource shape for every `Patient`; only populated fields appear. Two name-handling notes for LATAM data: - The first surname sits in `name.family`. The second surname rides in the `second-lastname` extension on the same `name` element — read both to reconstruct the full apellido. - `gender` is the FHIR value (`male` / `female` / `other`), mapped from Huli's internal `M` / `F` / `I`. Match on the FHIR token here, not the Huli letter. ## What to verify - HTTP status is `200`. - `resourceType` is `Bundle` and `type` is `searchset`. - `total` matches the number of `entry` items you expected for `Fernández`. - Each `entry.resource.resourceType` is `Patient`. - For a two-match query like this one there is no `next` link, so you have the full result set. Larger queries paginate — see the pagination recipe below. ## What can go wrong All errors return a FHIR `OperationOutcome`, not a bare string. Branch on the HTTP status code and `issue[0].code` (the FHIR IssueType) for machine-readable classification. The Huli-specific code (`HPB-…`) is available as the prefix of `issue[0].diagnostics` — split on `": "` to extract it. There is no `details` object. These four cover most first-run failures: `HPB-00106` — auth failed. The token is missing, malformed, or revoked. Confirm the header reads `Authorization: Bearer ` with a single space, and that `$HULI_API_KEY` is actually exported in this shell. `HPB-00104` — insufficient scope. The token authenticated but lacks system/Patient.rs. Re-mint it in Practice Settings with `Patient.rs` (or `Patient.cru`) selected. `HPB-00101` — validation error. A search parameter is malformed — most often an un-encoded accent or an unknown parameter. Encode `á` as `%C3%A1` and check the parameter name against the Patient search reference. `HPB-00105` — rate limited. You exceeded the per-key request budget. Read the `Retry-After` response header and back off for that many seconds before retrying. A representative `403` body: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "forbidden", "diagnostics": "HPB-00104: Insufficient scope" } ] } ``` There is a second `401`, `HPB-00107` (auth expired), that you will not hit with an admin bearer token — it applies to the time-limited tokens issued by SMART Backend Services. If you see it here, you are sending a SMART access token rather than the admin key. ## Next recipes - **Paginate a large patient list** — follow the `link[rel=next]` cursor to walk every page of a `searchset`. - **Search NOM-024 LATAM identifiers** — query the `identifier` parameter for CURP, RFC, NSS, and INE, with the identifier system URIs defined authoritatively there. - **Authenticate as a SMART Backend Service** — swap the admin bearer token for `client_credentials` + `private_key_jwt` (RS384) when you ship a server-to-server integration. - **Create and update a Patient** — move from `system/Patient.rs` to `system/Patient.cru` and `POST` / `PUT` patient records. ======================================================================== # Send lab results to the chart # URL: https://developers.huli.ai/v1/recipes/posting-lab-observations-lis # POST a LOINC-coded, UCUM-quantified Observation from your lab system — link it to the Patient and decode the validation errors that block most first writes. # Send lab results to the chart Push one result from your lab system into the patient's chart in HuliPractice as a FHIR R4 `Observation` — LOINC-coded, UCUM-quantified, anchored to a `Patient`. The write either lands a `201 Created` with a server-assigned ID or returns a FHIR `OperationOutcome` you can map back to your lab system's queue. This recipe covers both ends. Standalone Observation writes are **patient-scoped and out-of-encounter**. `Observation.encounter` is read-only on the Public API: a create or update that carries it is rejected with `400`. Encounter-bound results are recorded through the encounter save flow, not as standalone Observation POSTs — see the note in step 1. The write surface also takes a single `valueQuantity`; `referenceRange` and `component` are **not read on write** (they are silently dropped and do not round-trip). ## Audience You run the interface side of a clinical laboratory in Latin America. You speak HL7 v2 or ASTM on the analyzer side, you map LOINC to your local test catalog, and you carry UCUM units on every numeric result. You want the exact FHIR shape Huli accepts on write and the rejections that account for most first-integration failures. ## You'll need - A token carrying . `cru` grants create, read, and update — a write needs the `c`. An admin bearer token minted in **Practice Settings → Integrations → API Keys** works, as does a SMART Backend Services access token (`client_credentials` + `private_key_jwt`, RS384, 5-minute TTL). - on the same token if your lab system resolves the `Patient` UUID by search before writing. This recipe assumes you already hold it from the order message. ( is only needed to _read_ encounter-bound observations back — a write must not carry an encounter; see step 1.) - A LOINC code for every test you post. The lab's analytical result maps to a LOINC `code` — Huli validates it against its observation catalog on write. - A numeric value and its unit. The write reads `valueQuantity.value` and `valueQuantity.unit`; the LOINC catalog resolves the canonical unit and display server-side. Send a human unit label (e.g. `mg/dL`) in `valueQuantity.unit`. - `curl`, or a Go, Python, or Node HTTP client. LOINC is the `code`; the value is the `valueQuantity`. `code.coding[].system` must be exactly `http://loinc.org` and the `code` must be one the catalog recognizes — an unknown LOINC code is a `400` (`HPB-00101`, catalog lookup). The write reads `valueQuantity.value` and `valueQuantity.unit`; it does **not** validate `valueQuantity.code` (UCUM), so a missing or mismatched UCUM token does not by itself fail the write. The canonical unit is resolved from the LOINC catalog regardless of what you send. ## End state You hold a `201 Created` whose body is the stored `Observation`, now carrying a server-assigned `id`. The resource references Doctora María Fernández's patient at Clínica San Rafael, and it round-trips on a follow-up `GET /fhir/R4/Observation/{id}`. ## Steps ### 1. Export the token and the patient reference ```bash export HULI_TOKEN="" export PATIENT_ID="01965e2a-8c4d-7000-9001-0000000000a2" ``` Do **not** put an `Encounter` reference on the write. `Observation.encounter` is read-only on the Public API — a write that carries it is **rejected with `400`**. Encounter-bound observations are recorded through the encounter save flow (managed atomically with the encounter's clinical record), not as standalone Observation POSTs. A standalone lab result is patient-scoped and out-of-encounter; the server still emits `Observation.encounter` on `GET`/search for observations that _were_ captured during an encounter. ### 2. Build the Observation body Write a LOINC-coded serum glucose result of `126 mg/dL` for the patient. ```json { "resourceType": "Observation", "status": "final", "category": [ { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "laboratory", "display": "Laboratory" } ] } ], "code": { "coding": [ { "system": "http://loinc.org", "code": "2339-0", "display": "Glucose [Mass/volume] in Blood" } ] }, "subject": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2" }, "effectiveDateTime": "2026-06-01T07:42:00-06:00", "valueQuantity": { "value": 126, "unit": "mg/dL", "system": "http://unitsofmeasure.org", "code": "mg/dL" } } ``` Field-level rules the server enforces on this body: - `code.coding[]` must carry a LOINC entry — `system` exactly `http://loinc.org` and a `code` the observation catalog recognizes. This is required; a `code` with no LOINC coding, or an unknown LOINC code, is a `400` (`HPB-00101`). - The write reads `valueQuantity.value` and `valueQuantity.unit`. The LOINC catalog resolves the canonical unit and display, so `valueQuantity.code` (UCUM) is not validated on write — send `system`/`code` for round-trip fidelity if you like, but they do not gate the write. - `status` is required, and the write surface accepts only a **subset** of the FHIR value set: a create persists `final` only — any other status (`registered`, `preliminary`, `amended`, `corrected`, `cancelled`) is rejected with `422` on `Observation.status`. To void a stored result, `PUT` it with `entered-in-error` (see the next recipe). - The numeric value is range-checked against the catalog's validation range for that LOINC code; an out-of-range value is rejected with `400` (`HPB-02907`). The write surface takes a **single** `valueQuantity` per Observation. `referenceRange` and `component` are not read by the write decoder — if you send them they are silently dropped and will not round-trip. Multi-component vitals (e.g. a blood-pressure panel with separate systolic/diastolic components) are not supported as a single standalone Observation write. ### 3. POST the Observation Save the body from step 2 to `observation.json`, then: :::CodeGroup ```bash curl -i -X POST https://api.huli.ai/fhir/R4/Observation \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Content-Type: application/fhir+json" \ -H "Accept: application/fhir+json" \ -d @observation.json ``` ```typescript const UCUM_SYSTEM = 'http://unitsofmeasure.org'; function ucum(value: number, unitCode: string) { return { value, unit: unitCode, system: UCUM_SYSTEM, code: unitCode }; } const unit = 'mg/dL'; const observation = { resourceType: 'Observation', status: 'final', category: [ { coding: [ { system: 'http://terminology.hl7.org/CodeSystem/observation-category', code: 'laboratory', display: 'Laboratory', }, ], }, ], code: { coding: [ { system: 'http://loinc.org', code: '2339-0', display: 'Glucose [Mass/volume] in Blood', }, ], }, subject: { reference: `Patient/${process.env.PATIENT_ID}` }, effectiveDateTime: '2026-06-01T07:42:00-06:00', valueQuantity: ucum(126, unit), }; const resp = await fetch('https://api.huli.ai/fhir/R4/Observation', { method: 'POST', headers: { Authorization: `Bearer ${process.env.HULI_TOKEN}`, 'Content-Type': 'application/fhir+json', Accept: 'application/fhir+json', }, body: JSON.stringify(observation), }); if (resp.status === 201) { const created = (await resp.json()) as { id: string }; console.log(created.id); } else { const outcome = (await resp.json()) as { issue: { diagnostics: string }[]; }; // HPB code is the prefix of issue[0].diagnostics, split on ': ' const [hpbCode] = outcome.issue[0].diagnostics.split(': ', 1); console.log(resp.status, hpbCode, outcome.issue[0].diagnostics); } ``` ```python import os import requests UCUM_SYSTEM = "http://unitsofmeasure.org" def ucum(value: float, unit_code: str) -> dict: """Build a UCUM-coded quantity in one canonical unit.""" return {"value": value, "unit": unit_code, "system": UCUM_SYSTEM, "code": unit_code} unit = "mg/dL" observation = { "resourceType": "Observation", "status": "final", "category": [ { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "laboratory", "display": "Laboratory", } ] } ], "code": { "coding": [ { "system": "http://loinc.org", "code": "2339-0", "display": "Glucose [Mass/volume] in Blood", } ] }, "subject": {"reference": f"Patient/{os.environ['PATIENT_ID']}"}, "effectiveDateTime": "2026-06-01T07:42:00-06:00", "valueQuantity": ucum(126, unit), } resp = requests.post( "https://api.huli.ai/fhir/R4/Observation", headers={ "Authorization": f"Bearer {os.environ['HULI_TOKEN']}", "Content-Type": "application/fhir+json", "Accept": "application/fhir+json", }, json=observation, timeout=30, ) if resp.status_code == 201: print(resp.json()["id"]) else: outcome = resp.json() # HPB code is the prefix of issue[0].diagnostics, split on ": " code = outcome["issue"][0]["diagnostics"].split(": ", 1)[0] print(resp.status_code, code, outcome["issue"][0]["diagnostics"]) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class PostObservation { static final String UCUM_SYSTEM = "http://unitsofmeasure.org"; // ucum builds a UCUM-coded quantity. The write reads value + unit; the // system/code round-trip but are not validated. Hand-built JSON keeps this // dependency-free; a real LIS would use a JSON library. static String ucum(double value, String unitCode) { return String.format( "{\"value\":%s,\"unit\":\"%s\",\"system\":\"%s\",\"code\":\"%s\"}", value, unitCode, UCUM_SYSTEM, unitCode); } public static void main(String[] args) throws Exception { String unit = "mg/dL"; String patientId = System.getenv("PATIENT_ID"); String observation = "{" + "\"resourceType\":\"Observation\"," + "\"status\":\"final\"," + "\"category\":[{\"coding\":[{" + "\"system\":\"http://terminology.hl7.org/CodeSystem/observation-category\"," + "\"code\":\"laboratory\",\"display\":\"Laboratory\"}]}]," + "\"code\":{\"coding\":[{" + "\"system\":\"http://loinc.org\"," + "\"code\":\"2339-0\",\"display\":\"Glucose [Mass/volume] in Blood\"}]}," + "\"subject\":{\"reference\":\"Patient/" + patientId + "\"}," + "\"effectiveDateTime\":\"2026-06-01T07:42:00-06:00\"," + "\"valueQuantity\":" + ucum(126, unit) + "}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Observation")) .header("Authorization", "Bearer " + System.getenv("HULI_TOKEN")) .header("Content-Type", "application/fhir+json") .header("Accept", "application/fhir+json") .POST(HttpRequest.BodyPublishers.ofString(observation)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); switch (response.statusCode()) { case 201 -> // ack the lab system's message System.out.println("201 created\n" + response.body()); case 400 -> // HPB-00101 validation — unknown LOINC code, or value out of catalog range System.out.println("400 validation\n" + response.body()); // dead-letter, do not retry case 409 -> // HPB-00103 conflict — dead-letter like a 400; do not retry blindly System.out.println("409 conflict\n" + response.body()); case 403 -> // HPB-00104 insufficient scope — token lacks Observation.cru System.out.println("403 forbidden\n" + response.body()); case 404 -> // HPB-00102 — subject (Patient) reference does not resolve System.out.println("404 not found\n" + response.body()); case 401 -> // HPB-00106 auth failed / HPB-00107 auth expired System.out.println("401 unauthorized\n" + response.body()); case 429 -> // HPB-00105 rate limited — requeue after the header's seconds System.out.println("429 rate limited (Retry-After: " + response.headers().firstValue("Retry-After").orElse("") + ")\n" + response.body()); default -> System.out.println(response.statusCode() + "\n" + response.body()); } } } ``` ```go package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" ) type quantity struct { Value float64 `json:"value"` Unit string `json:"unit"` System string `json:"system"` Code string `json:"code"` } const ucumSystem = "http://unitsofmeasure.org" // ucum builds a UCUM-coded quantity. The write reads value + unit; the // system/code round-trip but are not validated on write. func ucum(value float64, unitCode string) quantity { return quantity{Value: value, Unit: unitCode, System: ucumSystem, Code: unitCode} } func main() { const unit = "mg/dL" obs := map[string]any{ "resourceType": "Observation", "status": "final", "category": []any{map[string]any{"coding": []any{map[string]any{ "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "laboratory", "display": "Laboratory", }}}}, "code": map[string]any{"coding": []any{map[string]any{ "system": "http://loinc.org", "code": "2339-0", "display": "Glucose [Mass/volume] in Blood", }}}, "subject": map[string]any{"reference": "Patient/" + os.Getenv("PATIENT_ID")}, "effectiveDateTime": "2026-06-01T07:42:00-06:00", "valueQuantity": ucum(126, unit), } payload, err := json.Marshal(obs) if err != nil { panic(err) } req, err := http.NewRequest(http.MethodPost, "https://api.huli.ai/fhir/R4/Observation", bytes.NewReader(payload)) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("HULI_TOKEN")) req.Header.Set("Content-Type", "application/fhir+json") req.Header.Set("Accept", "application/fhir+json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) switch resp.StatusCode { case http.StatusCreated: fmt.Printf("201 created\n%s\n", body) // ack the lab system's message case http.StatusBadRequest: // HPB-00101 validation — unknown LOINC code, or value out of catalog range fmt.Printf("400 validation\n%s\n", body) // dead-letter, do not retry case http.StatusConflict: // HPB-00103 conflict — dead-letter like a 400; do not retry blindly fmt.Printf("409 conflict\n%s\n", body) case http.StatusForbidden: // HPB-00104 insufficient scope — token lacks Observation.cru fmt.Printf("403 forbidden\n%s\n", body) case http.StatusNotFound: // HPB-00102 — subject (Patient) reference does not resolve fmt.Printf("404 not found\n%s\n", body) case http.StatusUnauthorized: // HPB-00106 auth failed / HPB-00107 auth expired fmt.Printf("401 unauthorized\n%s\n", body) case http.StatusTooManyRequests: // HPB-00105 rate limited fmt.Printf("429 rate limited (Retry-After: %s)\n%s\n", resp.Header.Get("Retry-After"), body) // requeue after the header's seconds default: fmt.Printf("%d\n%s\n", resp.StatusCode, body) } } ``` ::: ### 4. Confirm the stored resource The `201` response body is the stored `Observation` with its assigned `id`. Re-read it to confirm it persisted: ```bash curl https://api.huli.ai/fhir/R4/Observation/ \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` ## What to verify - HTTP status is `201`. - The response body has a server-assigned `id` — a UUID you did not send — and that `id` round-trips on the follow-up `GET /fhir/R4/Observation/{id}`. - `code.coding[0].system` is `http://loinc.org` and the LOINC `code` round-trips unchanged. - `valueQuantity.value` round-trips and `valueQuantity.system` is `http://unitsofmeasure.org`. The `unit` reflects the catalog's canonical unit for the LOINC code, which may differ from the label you sent. - `subject.reference` resolves to your `PATIENT_ID`. The body carries **no** `encounter` (a standalone write is out-of-encounter), and no `referenceRange` or `component` (the write surface does not read them). - `status` is `final`. There is no `referenceRange`/`component` on the stored resource for a standalone lab write. ## What can go wrong Every failure returns a FHIR `OperationOutcome` — never a bare string. Branch on the HTTP status and on `issue[0].code` (the FHIR IssueType) for machine classification; lift the Huli code (`HPB-…`) from the prefix of `issue[0].diagnostics`, split on `": "`. There is no `issue.details`, no `coding`, no `text` — only `severity`, `code`, `diagnostics`, and (on structural field errors such as the `status` 422) an `expression` FHIRPath pointing at the offending element. `HPB-00101` — validation. The body broke a write rule. Two lab-integration shapes hit this: - **Missing / unknown LOINC** (`HPB-02908`): `code` has no coding under `http://loinc.org`, or the LOINC `code` is not in the observation catalog. Map your local test catalog to a valid LOINC before posting. - **Value out of range** (`HPB-02907`): the numeric value falls outside the catalog's validation range for that LOINC code. Confirm the value and that you mapped to the right code. Note: a missing or mismatched UCUM `valueQuantity.code` is **not** a write error — the write reads `value` + `unit` and the catalog resolves the canonical unit. A representative `400` body: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "diagnostics": "HPB-00101: code.coding must include a LOINC code (system http://loinc.org)" } ] } ``` — unsupported `status`. A create accepts only `final`; any other status (`registered`, `preliminary`, `amended`, `corrected`, `cancelled`) is rejected at `Observation.status`. On an update, only `final` (a value edit) or `entered-in-error` (the void) are accepted. Post released lab results as `final`. ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "value", "expression": ["Observation.status"], "diagnostics": "Observation.status not supported on this operation: create accepts \"final\"; update accepts \"final\" or \"entered-in-error\"" } ] } ``` — also: `Observation.encounter` present on a write. The encounter is read-only on this surface; remove it (see step 1). `HPB-00102` — not found. An unresolvable `subject` reference is rejected. Resolve the patient against `Patient` search before writing, and confirm the token's organization owns it. ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "not-found", "diagnostics": "HPB-00102: referenced resource not found" } ] } ``` `HPB-00104` — insufficient scope. The token authenticated but lacks the `c` in . Re-mint it with `Observation.cru` selected; `Observation.rs` is read-only and cannot write. `HPB-00106` (auth failed) / `HPB-00107` (auth expired). The token is missing, malformed, or — on a SMART Backend Services token — past its 5-minute TTL. For `HPB-00107`, exchange a fresh access token at and retry. An admin bearer token does not expire on a timer, so `HPB-00107` against one usually means a SMART access token is being sent on a request you intended to authenticate with the admin key. `HPB-00105` — rate limited. You exceeded the per-key request budget — common when a lab system flushes a backlog. Read the `Retry-After` response header and requeue the message for that many seconds. Do not tight-loop the retry. ## Next recipes - **Search and void a prior result** — query `Observation` by `patient` + `code` + `date`, then `PUT` `entered-in-error` to void a result an analyzer re-run supersedes. - **Resolve Patient references by search** — turn an order message's identifiers into the patient UUID this recipe assumes you already hold. - **Authenticate as a SMART Backend Service** — swap the admin bearer token for `client_credentials` + `private_key_jwt` (RS384) for an unattended lab-system interface. ======================================================================== # Receive webhooks # URL: https://developers.huli.ai/v1/recipes/receiving-webhooks # React to appointments and clinical events as they happen — register a rest-hook Subscription, verify the HMAC signature on every delivery, dedupe on the event id, and recover missed events with $replay. # Receive webhooks React to appointments and clinical events as they happen, without polling. Stand up an endpoint that receives Huli's outbound webhooks: register a FHIR R4 `Subscription`, verify the HMAC signature on each delivery, dedupe on the stable event id, and pull the full resource from the FHIR API. By the end you have a receiver that is safe against forged, duplicated, and out-of-order deliveries. For the full model — lifecycle, delivery semantics, and every header — read the [Webhooks concept](/v1/concepts/webhooks) first. This recipe is the working end-to-end path. ## Audience You run an integration that needs to react to appointments and clinical events (a new or rescheduled appointment, a finalized encounter) without polling. You can host an HTTPS endpoint and you hold a machine (API key) credential minted with a BAA (Business Associate Agreement) attestation and the system/Subscription.crud scope. ## You'll need - A publicly reachable **HTTPS** endpoint. Private, link-local, and metadata IPs are refused at dial time. - A machine bearer token with system/Subscription.crud (create) and, for the observability calls, system/Subscription.rs. - A read grant on the resource type you subscribe to — this recipe's `criteria: "Encounter"` needs system/Encounter.rs on the same credential ("subscribe only to what you can read"; otherwise the create is refused with `403 HPB-00104`). - A place to store the signing secret returned once at create time. ## End state A live `Subscription` in `status: active`, an endpoint that verifies `X-Huli-Signature` and dedupes on `X-Huli-Event-Id`, and a tested `$replay` path for catching up after downtime. ## Steps ### 1. Create the subscription Notify on finalized (and other) `Encounter` events. `criteria` is the resource **type** only — filter on the event type in your receiver. :::CodeGroup ```bash curl -X POST "https://api.huli.ai/fhir/R4/Subscription" \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Subscription", "status": "requested", "reason": "Sync encounters into our EHR", "criteria": "Encounter", "channel": { "type": "rest-hook", "endpoint": "https://hooks.example.com/huli", "payload": "application/fhir+json" } }' ``` ```python import os import requests resp = requests.post( "https://api.huli.ai/fhir/R4/Subscription", headers={ "Authorization": f"Bearer {os.environ['HULI_API_KEY']}", "Content-Type": "application/fhir+json", }, json={ "resourceType": "Subscription", "status": "requested", "reason": "Sync encounters into our EHR", "criteria": "Encounter", "channel": { "type": "rest-hook", "endpoint": "https://hooks.example.com/huli", "payload": "application/fhir+json", }, }, timeout=30, ) print(resp.status_code) # 201 ``` ::: The response is with `Cache-Control: no-store`. Its body is the stored `Subscription` (now `status: active`) with the **signing secret in an extension** and a non-blocking BAA reminder in `contained[]`. The signing secret is returned **exactly once**. Read it out of this `201` and store it in your secrets manager now — no endpoint re-reveals it. If you lose it, delete the subscription and create a new one. ### 2. Verify the signature on every delivery Each delivery is a `POST` of a `Bundle` (`type: "history"`) with these headers: `X-Huli-Signature`, `X-Huli-Event-Id`, `X-Huli-Delivery-Id`, `X-Huli-Event-Type`, `X-Huli-Occurred-At`, and — on replays — `X-Huli-Replay: true`. Recompute the HMAC over the **raw body bytes** and constant-time-compare against `X-Huli-Signature`. Capture the raw request body **before** any JSON middleware parses it. Re-serializing the JSON changes the bytes and the signature will never match. In Express, use `express.raw()`; in Flask, read `request.get_data()`. :::CodeGroup ```javascript import express from 'express'; import crypto from 'node:crypto'; const app = express(); const SECRET = process.env.HULI_WEBHOOK_SECRET; const seen = new Set(); // swap for a durable, persistent store in production // Raw body — do NOT use express.json() on this route. app.post('/huli', express.raw({ type: '*/*' }), (req, res) => { const sig = req.get('X-Huli-Signature') ?? ''; const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(sig); if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) { return res.status(401).end(); } const eventId = req.get('X-Huli-Event-Id'); if (seen.has(eventId)) return res.status(200).end(); // duplicate — ack and skip seen.add(eventId); const bundle = JSON.parse(req.body.toString('utf8')); const entry = bundle.entry[0]; // entry.request.method: POST=created, PUT=updated, DELETE=deleted/cancelled. // Id-level notification — GET the full resource from the FHIR API here. console.log(req.get('X-Huli-Event-Type'), entry.request.method, entry.request.url); res.status(200).end(); // 2xx = delivered }); app.listen(8080); ``` ```python import hashlib import hmac import os from flask import Flask, request app = Flask(__name__) SECRET = os.environ["HULI_WEBHOOK_SECRET"].encode() seen = set() # swap for a durable, persistent store in production @app.post("/huli") def huli(): raw = request.get_data() # raw bytes, before JSON parsing digest = hmac.new(SECRET, raw, hashlib.sha256).hexdigest() expected = f"sha256={digest}" if not hmac.compare_digest(expected, request.headers.get("X-Huli-Signature", "")): return "", 401 event_id = request.headers.get("X-Huli-Event-Id") if event_id in seen: # duplicate — ack and skip return "", 200 seen.add(event_id) bundle = request.get_json() entry = bundle["entry"][0] # entry["request"]["method"]: POST=created, PUT=updated, DELETE=deleted/cancelled. # Id-level notification — GET the full resource from the FHIR API here. print(request.headers.get("X-Huli-Event-Type"), entry["request"]["method"]) return "", 200 # 2xx = delivered ``` ::: Dedupe on `X-Huli-Event-Id`, not `X-Huli-Delivery-Id`. The event id is stable across retries and replays; the delivery id is fresh on every attempt. Deliveries are at-least-once, so a durable dedupe store (not the in-memory `Set` above) is required in production. ### 3. Return the right status - Return any (or other `2xx`) once you have durably accepted the event. That marks the delivery delivered. - Return a `4xx` for a signature that does not verify — the delivery is dropped. - If your handler throws or times out, let it surface as a `5xx`: Huli retries with exponential backoff, up to 5 attempts, then dead-letters. - Do **not** answer with a redirect. A `3xx` is refused and counts as a failure. After 100 consecutive failures the subscription is auto-paused to `status: error` and stops receiving events. Once your endpoint is healthy, resume it with a `PUT` setting `status: "active"`, then [replay](#4-replay-missed-events-after-an-outage) the gap. ### 4. Replay missed events after an outage Re-enqueue events whose `occurred_at` falls in a time window. Only an `active` subscription may replay; the window is clamped to the 30-day retention horizon and one call re-enqueues at most 500 events. ```bash curl -X POST "https://api.huli.ai/fhir/R4/Subscription/$SUB_ID/\$replay" \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "from", "valueInstant": "2026-07-01T00:00:00Z" }, { "name": "to", "valueInstant": "2026-07-02T00:00:00Z" } ] }' ``` The response summarizes the run: ```json { "resourceType": "Parameters", "parameter": [ { "name": "deliveriesQueued", "valueInteger": 142 }, { "name": "truncated", "valueBoolean": false } ] } ``` Replayed deliveries carry `X-Huli-Replay: true` and the **original** `X-Huli-Event-Id`, so the dedupe from step 2 transparently absorbs any overlap. If `truncated` is `true`, call again with the same window — events with an in-flight replay are skipped, so each call advances to older events. Repeat until `truncated` is `false`. ### 5. Watch delivery health Read aggregate health with `$stats` and the recent per-attempt trail with `$deliveries` (both need system/Subscription.rs): ```bash curl "https://api.huli.ai/fhir/R4/Subscription/$SUB_ID/\$stats" \ -H "Authorization: Bearer $HULI_API_KEY" curl "https://api.huli.ai/fhir/R4/Subscription/$SUB_ID/\$deliveries?_count=50&status=failed" \ -H "Authorization: Bearer $HULI_API_KEY" ``` `$stats` reports `delivered`, `failed`, `pending`, `dead`, `deadLetterDepth`, `totalAttempts`, `successRate`, and delivery-latency percentiles. `$deliveries` returns one group per recent attempt (`id`, `status`, `attempts`, `replay`, `eventType`, `occurredAt`, `lastStatusCode`, …) and never exposes payloads, endpoints, or secrets. ## What can go wrong `HPB-00101` on create — a `criteria` with a query string (`Encounter?status=finished`), a `channel.type` other than `rest-hook`, a non-HTTPS endpoint, or a `channel.payload` that is not `application/fhir+json`. Send a bare resource type and the required channel fields. `HPB-00104` — the token lacks system/Subscription.crud, or was not minted with a BAA attestation. Re-mint the machine credential with the subscription scope and BAA. Signature mismatches in step 2 are almost always a **re-serialized body**: verify against the raw received bytes, not a parsed-and-re-encoded object. ## Related - [Webhooks](/v1/concepts/webhooks) — the full concept: lifecycle, event catalogue, headers, and delivery semantics. - [Rate limiting](/v1/concepts/rate-limiting) — per-subscription delivery throttling protects your endpoint from backlog floods. ======================================================================== # Registering a patient # URL: https://developers.huli.ai/v1/recipes/registering-a-patient # Onboard a patient from an external system into a Mexican clinic — discover the NOM-024 address codes via the terminology ValueSets, then POST a FHIR R4 Patient with CURP/RFC identifiers and the second-lastname extension. # Registering a patient Turn an external patient record into a stored `Patient`. You will authenticate, discover the Mexican address codes the NOM-024 model needs — country, municipality, locality, and the address-source provenance — through the terminology service, then `POST` the patient with a LATAM-shaped name, CURP/RFC identifiers, and the discovered address. One scope carries the whole flow: system/Patient.cru for the write, which also grants the read the MX terminology ValueSets and CodeSystem are gated behind. The address codes are the reason for the discovery steps. A Mexican organization rejects an address whose municipality or locality codes are inconsistent or absent, so the terminology expansions below hand you values that the write will accept. A non-MX organization can skip the MX address discovery entirely and send a plain address. ## Audience You integrate clinical systems and onboard patients from an external EHR, a registration portal, or a referral intake into a Mexican clinic. You have already run [your first authenticated search](/v1/recipes/getting-started-patient-search), you read a `Bundle` without a viewer, and you know what a FHIR reference and an extension are. You want to take a patient from an external record to a `201 Created`. ## You'll need - A bearer token from HuliPractice (**Practice Settings → Integrations → API Keys**), or a SMART Backend Services access token. See [Bearer Tokens](/v1/auth/bearer) for provisioning and [`POST /auth/token`](/v1/auth) for the token exchange — this recipe assumes you already hold one. - This one scope on that token: - — create `Patient` (`.cru` also grants read + search). The MX terminology resources — the `mx-country` / `mx-municipality` / `mx-locality` ValueSets and the `address-source` CodeSystem — enforce a per-url `system/Patient.rs` at the handler, which `.cru` includes. So this one grant covers discovery and the write. - `curl`, or Node, Python, Java, or Go if you prefer a language client. The terminology resources page on offset pagination (`_count` + `_offset`), not the keyset `_cursor` that Patient search uses. `_count` defaults to 20 and caps at 100; walk pages by adding `_offset` in multiples of `_count`. The `mx-municipality` expansion **requires** a `state` code, and `mx-locality` **requires** both `state` and `municipality` — they scope the catalog so you get back a usable list rather than the whole country. ## End state You hold a `201 Created` whose body is the stored `Patient` — with a server-assigned `id`, the patient's full apellido reconstructed from `family` plus the `second-lastname` extension, CURP/RFC identifiers under their national systems, and an address carrying the MX state code you discovered. The patient is then resolvable by name or identifier through Patient search. ## Steps ### 1. Export the token ```bash export HULI_TOKEN="" ``` A non-MX organization can skip to step 3 and send a plain address (`line`, `city`, `state`, `postalCode`, `country`) with no MX codes. ### 2. (MX path) Discover the address codes A Mexican address is built from catalog codes, not free text. Expand three ValueSets in order — each narrows the next — then read the address-source CodeSystem for the provenance code. #### Country The expansion returns `expansion.contains[]`, each a `{system, code, display}`. Pick the country code you need; for a Mexican address that is the code whose display is `México`. :::CodeGroup ```bash curl "https://api.huli.ai/fhir/R4/ValueSet/\$expand?url=https://fhir.huli.ai/r4/ValueSet/mx-country" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` ```typescript const url = new URL('https://api.huli.ai/fhir/R4/ValueSet/$expand'); url.searchParams.set('url', 'https://fhir.huli.ai/r4/ValueSet/mx-country'); const resp = await fetch(url, { headers: { Authorization: `Bearer ${process.env.HULI_TOKEN}`, Accept: 'application/fhir+json', }, }); const vs = await resp.json(); // expansion.contains[].code is the catalog code; .display is the human name. const country = vs.expansion?.contains?.find((c: { display: string }) => c.display === 'México'); console.log(country?.code, country?.display); ``` ```python import os import requests resp = requests.get( "https://api.huli.ai/fhir/R4/ValueSet/$expand", params={"url": "https://fhir.huli.ai/r4/ValueSet/mx-country"}, headers={ "Authorization": f"Bearer {os.environ['HULI_TOKEN']}", "Accept": "application/fhir+json", }, timeout=30, ) vs = resp.json() # expansion.contains[].code is the catalog code; .display is the human name. country = next(c for c in vs["expansion"]["contains"] if c["display"] == "México") print(country["code"], country["display"]) ``` ::: A representative country expansion: ```json { "resourceType": "ValueSet", "url": "https://fhir.huli.ai/r4/ValueSet/mx-country", "status": "active", "expansion": { "total": 1, "contains": [ { "system": "https://fhir.huli.ai/r4/CodeSystem/mx-country", "code": "1", "display": "México" } ] } } ``` #### Municipality The municipality expansion requires the `state` code. State codes follow the published Mexican catalog (for example Jalisco is `14`); pass the one your patient lives in. ```bash curl "https://api.huli.ai/fhir/R4/ValueSet/\$expand?url=https://fhir.huli.ai/r4/ValueSet/mx-municipality&state=14&_count=100" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` Omitting `state` is a `400` — the catalog is too large to expand unscoped. Carry the municipality `code` you pick into the next expansion. #### Locality The locality expansion requires both `state` and `municipality`. Use `filter` to prefix-match a locality name and keep the page small. ```bash curl "https://api.huli.ai/fhir/R4/ValueSet/\$expand?url=https://fhir.huli.ai/r4/ValueSet/mx-locality&state=14&municipality=39&filter=Guadalajara" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` #### Address source Read the address-source CodeSystem to obtain the provenance code that records the address came from the Mexican normative catalog. The CodeSystem exposes `read` only (no `$expand`). ```bash curl "https://api.huli.ai/fhir/R4/CodeSystem/address-source" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` ```json { "resourceType": "CodeSystem", "url": "https://fhir.huli.ai/r4/CodeSystem/address-source", "status": "active", "content": "complete", "concept": [ { "code": "mx-normativo-nom024", "display": "NOM-024 normative address source" } ] } ``` The single member `mx-normativo-nom024` is the provenance value. In the IG's address model the source is carried under the system `https://huli.io/fhir/CodeSystem/address-source` — note that this provenance system string differs from the `https://fhir.huli.ai/r4/CodeSystem/address-source` canonical you just read; the read endpoint resolves the code, the IG names the system the stored value uses. See the [FHIR Implementation Guide](https://developers.huli.ai/fhir/) for the exact address-source binding. ### 3. POST the Patient Assemble the discovered values into the create body. The name carries the first surname in `family` and the maternal/second surname in the `second-lastname` extension on the same name element. Identifiers go under their published national systems (see the note below). The address maps the discovered state code onto `address.state`, the municipality/locality onto `city` and `district`, and the rest of the street address onto `line`. The body below is the **comprehensive** form — every field the create decoder honors on a Patient write, not a minimal example. Required fields are flagged inline; everything else is optional. The **Full field reference** after the example lists each field, whether the decoder reads it on write, and what it maps to. A minimal write needs only `name[0].given[0]`; everything else enriches the record. The identifier system URLs are published authoritatively in the FHIR Implementation Guide — do not invent them. CURP is `http://www.renapo.gob.mx/curp` (RENAPO) and RFC is `http://www.sat.gob.mx/rfc` (SAT); the identifier `type.coding` uses `http://terminology.hl7.org/CodeSystem/v2-0203` (`CURP`, `RFC`). The [FHIR IG identifiers reference](https://developers.huli.ai/fhir/) is the source of truth for the full identifier system list. :::CodeGroup ```bash curl -i -X POST https://api.huli.ai/fhir/R4/Patient \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Content-Type: application/fhir+json" \ -H "Accept: application/fhir+json" \ -d '{ "resourceType": "Patient", "active": true, "name": [ { "use": "official", "family": "Hernández", "given": ["Carlos"], "extension": [ { "url": "https://fhir.huli.ai/r4/StructureDefinition/second-lastname", "valueString": "Ramírez" } ] } ], "gender": "male", "birthDate": "1985-07-20", "maritalStatus": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus", "code": "M" } ] }, "identifier": [ { "use": "official", "system": "http://www.renapo.gob.mx/curp", "value": "HERC850720HJCRMR04", "type": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/v2-0203", "code": "CURP" } ] } }, { "use": "official", "system": "http://www.sat.gob.mx/rfc", "value": "HERC850720AB1", "type": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/v2-0203", "code": "RFC" } ] } } ], "telecom": [ { "system": "phone", "value": "5533112244", "use": "mobile", "rank": 1 }, { "system": "email", "value": "carlos.hernandez@example.com", "use": "home" } ], "address": [ { "use": "home", "type": "physical", "line": ["Calle Morelos 408, Col. Americana"], "city": "Guadalajara", "district": "Guadalajara", "state": "14", "postalCode": "44160", "country": "MX" } ], "contact": [ { "relationship": [{ "text": "Madre" }], "name": { "given": ["María Ramírez"] }, "telecom": [{ "system": "phone", "value": "5599887766", "use": "mobile" }] } ], "extension": [ { "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-blood-type", "valueCode": "O+" }, { "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-private-insurance", "extension": [ { "url": "provider", "valueString": "Seguros Monterrey" }, { "url": "policyNumber", "valueString": "POL-99812" }, { "url": "certificateNumber", "valueString": "CERT-44120" } ] } ] }' ``` ```typescript const patient = { resourceType: 'Patient', active: true, // optional — false maps to an inactive record; absent defaults to active name: [ { use: 'official', family: 'Hernández', // first surname given: ['Carlos'], // given[0] is the only strictly required field extension: [ { // The maternal/second surname rides this extension on the name element. url: 'https://fhir.huli.ai/r4/StructureDefinition/second-lastname', valueString: 'Ramírez', }, ], }, ], gender: 'male', // male | female | other (unknown is accepted but stored as empty) birthDate: '1985-07-20', maritalStatus: { // coding[0].code is read verbatim (S/M/D/W/P/U/L); display is ignored coding: [{ system: 'http://terminology.hl7.org/CodeSystem/v3-MaritalStatus', code: 'M' }], }, identifier: [ { use: 'official', system: 'http://www.renapo.gob.mx/curp', // CURP — published in the FHIR IG value: 'HERC850720HJCRMR04', type: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/v2-0203', code: 'CURP' }] }, }, { use: 'official', system: 'http://www.sat.gob.mx/rfc', // RFC — published in the FHIR IG value: 'HERC850720AB1', type: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/v2-0203', code: 'RFC' }] }, }, ], telecom: [ // system + value read verbatim; use + rank optional { system: 'phone', value: '5533112244', use: 'mobile', rank: 1 }, { system: 'email', value: 'carlos.hernandez@example.com', use: 'home' }, ], address: [ { use: 'home', // optional type: 'physical', // optional line: ['Calle Morelos 408, Col. Americana'], city: 'Guadalajara', district: 'Guadalajara', state: '14', // the mx-country/state code discovered in step 2 postalCode: '44160', country: 'MX', }, ], contact: [ // emergency / guardian contact — only name.given[0], relationship[0].text, and telecom are read { relationship: [{ text: 'Madre' }], name: { given: ['María Ramírez'] }, telecom: [{ system: 'phone', value: '5599887766', use: 'mobile' }], }, ], extension: [ { // blood type — valueCode stored verbatim url: 'https://fhir.huli.ai/r4/StructureDefinition/huli-blood-type', valueCode: 'O+', }, { // private insurance — provider required, policyNumber / certificateNumber optional url: 'https://fhir.huli.ai/r4/StructureDefinition/huli-private-insurance', extension: [ { url: 'provider', valueString: 'Seguros Monterrey' }, { url: 'policyNumber', valueString: 'POL-99812' }, { url: 'certificateNumber', valueString: 'CERT-44120' }, ], }, ], }; const resp = await fetch('https://api.huli.ai/fhir/R4/Patient', { method: 'POST', headers: { Authorization: `Bearer ${process.env.HULI_TOKEN}`, 'Content-Type': 'application/fhir+json', Accept: 'application/fhir+json', }, body: JSON.stringify(patient), }); if (resp.status === 201) { const created = (await resp.json()) as { id: string }; console.log('registered', created.id); } else { const outcome = (await resp.json()) as { issue: { diagnostics: string }[] }; // The HPB- code is the prefix of issue[0].diagnostics — split on ': '. const [code] = outcome.issue[0].diagnostics.split(': ', 1); console.log(resp.status, code, outcome.issue[0].diagnostics); } ``` ```python import os import requests patient = { "resourceType": "Patient", "active": True, # optional — False maps to an inactive record "name": [ { "use": "official", "family": "Hernández", # first surname "given": ["Carlos"], # given[0] is the only strictly required field "extension": [ { # The maternal/second surname rides this extension on the name element. "url": "https://fhir.huli.ai/r4/StructureDefinition/second-lastname", "valueString": "Ramírez", } ], } ], "gender": "male", # male | female | other "birthDate": "1985-07-20", "maritalStatus": { # coding[0].code read verbatim (S/M/D/W/P/U/L); display ignored "coding": [{"system": "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus", "code": "M"}] }, "identifier": [ { "use": "official", "system": "http://www.renapo.gob.mx/curp", # CURP — published in the FHIR IG "value": "HERC850720HJCRMR04", "type": {"coding": [{"system": "http://terminology.hl7.org/CodeSystem/v2-0203", "code": "CURP"}]}, }, { "use": "official", "system": "http://www.sat.gob.mx/rfc", # RFC — published in the FHIR IG "value": "HERC850720AB1", "type": {"coding": [{"system": "http://terminology.hl7.org/CodeSystem/v2-0203", "code": "RFC"}]}, }, ], "telecom": [ # system + value read verbatim; use + rank optional {"system": "phone", "value": "5533112244", "use": "mobile", "rank": 1}, {"system": "email", "value": "carlos.hernandez@example.com", "use": "home"}, ], "address": [ { "use": "home", # optional "type": "physical", # optional "line": ["Calle Morelos 408, Col. Americana"], "city": "Guadalajara", "district": "Guadalajara", "state": "14", # the mx-country/state code discovered in step 2 "postalCode": "44160", "country": "MX", } ], "contact": [ # emergency / guardian contact — only name.given[0], relationship[0].text, telecom read { "relationship": [{"text": "Madre"}], "name": {"given": ["María Ramírez"]}, "telecom": [{"system": "phone", "value": "5599887766", "use": "mobile"}], } ], "extension": [ { # blood type — valueCode stored verbatim "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-blood-type", "valueCode": "O+", }, { # private insurance — provider required, policyNumber / certificateNumber optional "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-private-insurance", "extension": [ {"url": "provider", "valueString": "Seguros Monterrey"}, {"url": "policyNumber", "valueString": "POL-99812"}, {"url": "certificateNumber", "valueString": "CERT-44120"}, ], }, ], } resp = requests.post( "https://api.huli.ai/fhir/R4/Patient", headers={ "Authorization": f"Bearer {os.environ['HULI_TOKEN']}", "Content-Type": "application/fhir+json", "Accept": "application/fhir+json", }, json=patient, timeout=30, ) if resp.status_code == 201: print("registered", resp.json()["id"]) else: outcome = resp.json() # The HPB- code is the prefix of issue[0].diagnostics — split on ": ". code = outcome["issue"][0]["diagnostics"].split(": ", 1)[0] print(resp.status_code, code, outcome["issue"][0]["diagnostics"]) ``` ```go package main import ( "bytes" "fmt" "io" "net/http" "os" ) func main() { // family carries the first surname; the second-lastname extension carries // the maternal surname. Identifier systems are the IG-published CURP/RFC URLs; // address.state is the mx-country/state code discovered in step 2. Only // name.given[0] is strictly required — every other field below is optional // enrichment the create decoder honors. body := []byte(`{ "resourceType": "Patient", "active": true, "name": [{ "use": "official", "family": "Hernández", "given": ["Carlos"], "extension": [{ "url": "https://fhir.huli.ai/r4/StructureDefinition/second-lastname", "valueString": "Ramírez" }] }], "gender": "male", "birthDate": "1985-07-20", "maritalStatus": { "coding": [{"system": "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus", "code": "M"}] }, "identifier": [ { "use": "official", "system": "http://www.renapo.gob.mx/curp", "value": "HERC850720HJCRMR04", "type": {"coding": [{"system": "http://terminology.hl7.org/CodeSystem/v2-0203", "code": "CURP"}]} }, { "use": "official", "system": "http://www.sat.gob.mx/rfc", "value": "HERC850720AB1", "type": {"coding": [{"system": "http://terminology.hl7.org/CodeSystem/v2-0203", "code": "RFC"}]} } ], "telecom": [ {"system": "phone", "value": "5533112244", "use": "mobile", "rank": 1}, {"system": "email", "value": "carlos.hernandez@example.com", "use": "home"} ], "address": [{ "use": "home", "type": "physical", "line": ["Calle Morelos 408, Col. Americana"], "city": "Guadalajara", "district": "Guadalajara", "state": "14", "postalCode": "44160", "country": "MX" }], "contact": [{ "relationship": [{"text": "Madre"}], "name": {"given": ["María Ramírez"]}, "telecom": [{"system": "phone", "value": "5599887766", "use": "mobile"}] }], "extension": [ { "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-blood-type", "valueCode": "O+" }, { "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-private-insurance", "extension": [ {"url": "provider", "valueString": "Seguros Monterrey"}, {"url": "policyNumber", "valueString": "POL-99812"}, {"url": "certificateNumber", "valueString": "CERT-44120"} ] } ] }`) req, err := http.NewRequest(http.MethodPost, "https://api.huli.ai/fhir/R4/Patient", bytes.NewReader(body)) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("HULI_TOKEN")) req.Header.Set("Content-Type", "application/fhir+json") req.Header.Set("Accept", "application/fhir+json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, err := io.ReadAll(resp.Body) if err != nil { panic(err) } switch resp.StatusCode { case http.StatusCreated: fmt.Printf("201 registered\n%s\n", out) case http.StatusBadRequest: // HPB-00101 structural / CURP-composition / MX locality coherence fmt.Printf("400 validation\n%s\n", out) case http.StatusUnprocessableEntity: // CURP needs in-app confirmation (no FHIR channel) fmt.Printf("422 needs in-app confirmation\n%s\n", out) default: fmt.Printf("%d\n%s\n", resp.StatusCode, out) } } ``` ::: Run a **minimal** patient write against your sandbox — a create needs only `name[0].given[0]`, so this is the smallest body the decoder accepts: A `201 Created` returns the stored `Patient` with a server-assigned `id`. The `family` plus the `second-lastname` extension round-trip, the identifiers round-trip under their systems, and the address carries the state code you sent. #### Full field reference Every field the Patient create decoder reads on write. "Honored" means the create decoder maps the field into the stored record; fields not listed (or marked **ignored**) are accepted but not persisted from your input. Only `name[0].given[0]` is required. | Field | Req? | Honored on write | Notes | | ------------------------------------------------------- | ------------ | ----------------- | ------------------------------------------------------------------------------------------------------------- | | `name[0].given[0]` | **required** | yes | First given name — the only strictly required field (`ValidatePatient`). | | `name[0].family` | optional | yes | First (paternal) surname. | | `name[0].extension[]` `…/second-lastname` `valueString` | optional | yes | Maternal / second surname on the same name element. | | `name[0].use` | optional | ignored | Read endpoint always emits `official`. | | `active` | optional | yes | `true` → active record, `false` → inactive; absent defaults to active. | | `gender` | optional | yes | `male`/`female`/`other` map to M/F/I. `unknown` validates but stores empty. | | `birthDate` | optional | yes | `YYYY-MM-DD`. | | `maritalStatus.coding[0].code` | optional | yes | One of S/M/D/W/P/U/L (v3-MaritalStatus). `display` is ignored. | | `identifier[].system` | optional | yes | Resolved against the published system list (CURP/RFC/etc.). An unrecognized system is a `400`. | | `identifier[].value` | optional | yes | Required when an `identifier` entry is present. | | `identifier[].type` / `use` | optional | ignored | Type is re-derived from the resolved system on read. | | `telecom[].system` | optional | yes | `phone` / `email` / etc. | | `telecom[].value` | optional | yes | The number or address. | | `telecom[].use` | optional | yes | `home`/`mobile`/`work`. | | `telecom[].rank` | optional | yes | Preference order (integer). | | `address[].line[]` | optional | yes | Street address lines. | | `address[].city` | optional | yes | Carries the locality for an MX address. | | `address[].district` | optional | yes | Carries the municipality for an MX address. | | `address[].state` | optional | yes | The MX state code discovered in step 2. | | `address[].postalCode` | optional | yes | | | `address[].country` | optional | yes | | | `address[].use` / `type` | optional | yes | `home`/`work`; `physical`/`postal`. | | `contact[].name.given[0]` | optional | yes | Emergency / guardian contact name. | | `contact[].relationship[0].text` | optional | yes | Free-text relationship label. | | `contact[].telecom[]` | optional | yes | `system`/`value`/`use` per `telecom` above. | | `extension[]` `…/huli-blood-type` `valueCode` | optional | yes | Blood type, e.g. `O+`. | | `extension[]` `…/huli-private-insurance` | optional | yes | Nested `provider` (required within the block), `policyNumber`, `certificateNumber`. | | `managingOrganization` | optional | ignored | Server stamps the token's organization; a supplied reference is validated as a UUID but not used to reassign. | | `deceasedDateTime` | optional | ignored on create | Deceased status is set through the dedicated deceased flow, not the create decoder. | ## What to verify - HTTP status is `201`. - The response body's `resourceType` is `Patient` and it carries a server-assigned `id` — a UUID you did not send. - `name[0].family` is the first surname and the `second-lastname` extension on the same name element carries the maternal surname — read both to reconstruct the full apellido. - Each `identifier[].system` round-trips unchanged (`http://www.renapo.gob.mx/curp` for CURP), proof the system was recognized, not dropped. - `address[0].state` is the MX state code you discovered, and `gender` is the FHIR token (`male`/`female`/`other`) you sent. ## What can go wrong All errors return a FHIR `OperationOutcome`, never a bare string — `{severity, code, diagnostics}`, with no `details` object. Branch on the HTTP status and `issue[0].code` (the FHIR IssueType); the Huli code is the prefix of `issue[0].diagnostics`, split on `": "` to extract it. Structural problems — a missing required field, malformed JSON, a bad date — surface as `HPB-00101`. `HPB-00101` — **structural validation.** A required field is missing or malformed: no `given` name, a `birthDate` that is not `YYYY-MM-DD`, a `gender` outside `male`/`female`/`other`, or an identifier under an unrecognized `system`. Send a valid `given[0]`, a recognized identifier system (the IG-published CURP/RFC URLs), and a well-formed date. **CURP composition validation.** A CURP whose internal composition is inconsistent — the date segment, the sex letter, or the check digit not matching the rest — is rejected even when the string is the right length. Compose the CURP correctly from the patient's own data, or omit it and add it once verified. **inconsistent MX locality codes.** A Mexican organization rejects an address whose municipality or locality is incoherent with the state — a municipality that does not belong to the state you sent, or a locality outside the municipality — validated against the national municipality/country catalogs. This is why step 2 discovers the codes against the catalog; re-run the step 2 expansions to pick a consistent state → municipality → locality chain. **CURP needs in-app confirmation.** A CURP that the app would accept only after an "inappropriate-word" confirmation is rejected until the patient is created through the app — the FHIR surface has no confirmation channel. Create the patient in HuliPractice, or omit the CURP and add it there. A representative `400` body: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "diagnostics": "HPB-00101: Validation error" } ] } ``` A non-MX organization does not need the MX address codes. Drop the `state`/`district` catalog codes and send a plain address (`line`, `city`, `state` as text, `postalCode`, `country`); the write still succeeds. The MX discovery in step 2 exists to satisfy the Mexican address-consistency check, not as a universal requirement. ## Next recipes - **[Booking an appointment end-to-end](/v1/recipes/booking-an-appointment)** — with the patient registered, discover a service, practitioner, room, and free slot, then `POST` the Appointment for them. - **[Run your first authenticated Patient search](/v1/recipes/getting-started-patient-search)** — resolve the patient you just created by name or identifier (CURP/RFC) to confirm it persisted and to fetch its `id` later. - **Authenticate as a SMART Backend Service** — swap the admin bearer token for `client_credentials` + `private_key_jwt` (RS384, 5-minute access token) when you ship the onboarding flow server-to-server. ======================================================================== # Sandbox quickstart # URL: https://developers.huli.ai/v1/recipes/sandbox-quickstart # Get a sandbox organization pre-seeded with fake FHIR data from a Huli admin, receive your bearer credential through a one-time link, and make your first authenticated call. # Sandbox quickstart Most recipes in this section assume you already have a Huli organization and an admin who can mint you a bearer token in Practice Settings. A **sandbox** is the same idea, pointed at synthetic data: a dedicated organization pre-seeded with fabricated patients, appointments, encounters, and observations, so you can build and test your integration before it touches anything that looks like production. A sandbox is **created for you by someone with a Huli account** — an org admin at the clinic you're integrating with, or your Huli contact. There is no anonymous self-serve provisioning endpoint: the person creating the sandbox does it from inside HuliPractice, and hands you the credential through a one-time link. Your side of the flow needs nothing but that link and an HTTP client. ## Audience You're an external developer evaluating the Huli Public FHIR API, building a proof of concept, or developing an integration that isn't ready for real patient data. You have a contact at Huli or at a clinic who can create the sandbox for you. ## You'll need - Someone with an **admin-role HuliPractice login** willing to create the sandbox — see [the admin's side](#sandbox-quickstart.the-admins-side-creating-a-sandbox) below, which you can forward to them verbatim. - The **one-time share link** they send you after creating it. - `curl` (or any HTTP client). The [`huli` CLI](/v1/cli) works too — the credential is a plain bearer token, so `--token` is all it needs. ## End state A dedicated sandbox organization seeded with fabricated clinical data; a bearer API key bound to that org and to nothing else, valid for **14 days**; and one successful `GET` against the same FHIR R4 surface every production integration uses. ## The admin's side: creating a sandbox If you're the org admin or Huli operator creating the sandbox for a developer: a sandbox is always attached to **one production API key**, from **Practice Settings → Integrations**. 1. Creating a **new** integration? Leave **"Generar un entorno de pruebas (recomendado)"** checked in the create wizard — the production key and its paired sandbox are minted together and disclosed in one share link. For an **existing** key, open the key row's menu and choose **"Gestionar entorno de pruebas"** (Manage sandbox), then create the sandbox from there. 2. The sandbox's identity is derived from the production key itself — there is no email or name to fill in. Managing the sandbox again for the same key reuses the existing sandbox organization (and its data); refreshing just mints a fresh key — it never creates a duplicate org. 3. On success you get a **one-time share link** (valid for **24 hours**, single use) containing the developer's bearer credential. Send that link — not a pasted token — to the developer over a reasonably private channel. The secret itself is only ever revealed on the share page, exactly once. The sandbox organization is created and seeded at that moment — fabricated patients, practitioners, appointments, encounters, and observations, referentially consistent, zero real data. The minted key is bound to the sandbox org and can never reach any other organization's data: the credential _is_ the boundary. ## The developer's side ### 1. Open the share link and capture the token The link your contact sends looks like `https:///api-keys/share/…`. It works **once**, then self-destructs; the same page shows the API base URL. Store the bearer token in a secrets manager or environment variable immediately — nobody, including the admin who created it, can view it again. If you lose it, ask your contact to refresh the sandbox from the key's **Gestionar entorno de pruebas** dialog: you'll get a new key on the same organization. ### 2. Make your first call The sandbox key is an ordinary bearer credential on the same FHIR R4 surface as production — no separate sandbox auth mode, no token-exchange handshake: ```bash export HULI_SANDBOX_TOKEN="" curl "https://api.huli.ai/fhir/R4/Patient?_count=5" \ -H "Authorization: Bearer ${HULI_SANDBOX_TOKEN}" \ -H "Accept: application/fhir+json" ``` A `200 OK` returns the same FHIR `Bundle` shape as [Start](/v1/start) — just backed by fabricated data instead of a clinic's real records. Or run it right here — paste your sandbox token into the [playground](/playground) token bar above the recipes nav (or on this page's Run button, the first time you use one) and press Run: With the [`huli` CLI](/v1/cli), pass the token directly: ```bash huli --token "${HULI_SANDBOX_TOKEN}" fhir patient search --count 5 ``` ## The sandbox indicator There's no request-level "sandbox mode" flag to remember or forget — the mode lives in the credential. A sandbox key can only ever resolve to its own sandbox org, so there's no header or query param that could accidentally point it at real data. Every response served by a sandbox key carries two identifying signals — one on the transport, one inside the payload: ```http X-Huli-Mode: sandbox ``` ```json { "resourceType": "Patient", "meta": { "tag": [ { "system": "https://huli.io/tags", "code": "sandbox", "display": "Synthetic sandbox data" } ] } } ``` The `meta.tag` is stamped on **every** resource a sandbox key reads or writes — single resources, search `Bundle`s, and each Bundle entry alike — so even data copied out of a response (into a fixture, a demo, a bug report) stays self-identifying as synthetic. Use both as belt-and-suspenders checks in your own logs or tests: neither should ever appear on a response your production key receives, and both should always appear on a sandbox key's responses. ## Limits and expiry Sandbox keys carry ceilings a production key doesn't: | Ceiling | Default | Enforcement | | --------------------- | --------------- | ----------------------------------------------------------------------------- | | Rate limit | 60 req/min | Same 1-minute sliding window as every other key. | | Volume cap (lifetime) | 10,000 requests | Counted per request; never resets on its own. | | Key expiry | 14 days | Stamped at minting; an expired key is rejected. The org and its data persist. | | Keys per sandbox | 5 | Each sandbox-key refresh mints a new key on the same org, up to this cap. | The volume cap is a lifetime counter, not a per-minute rate — it does not reset on a timer the way the request-rate limit does. Once you hit it, every further request on that key returns `HPB-00121`. Resetting a sandbox key's volume counter and extending a key's expiry are **operator-only** actions — reach out to your Huli contact if you need a clean run or more time. Only the **credential** expires — the sandbox organization and its seeded data are never deleted. When a key lapses (or you simply want a fresh one), your contact opens the production key's **Gestionar entorno de pruebas** dialog and refreshes the sandbox key: same org, same data, new key, new one-time link. ## What to verify - The share link opened exactly once and showed a bearer token plus the base URL. - `GET /fhir/R4/Patient?_count=5` with the token returns a `200` with a `Bundle`, not an `OperationOutcome`. - The response headers include `X-Huli-Mode: sandbox`, and each resource carries the `meta.tag` with `system: "https://huli.io/tags"` and `code: "sandbox"`. ## What can go wrong — the token is wrong, or the key **expired** (14 days after minting). Ask your contact to refresh the sandbox key from the production key's Manage-sandbox dialog — you'll get a fresh key on the same organization. `HPB-00105` — the per-key request-rate limit (see [Rate Limiting](/v1/concepts/rate-limiting)); read `Retry-After`. `HPB-00121` — the lifetime volume cap is exhausted, not the per-minute rate. `Retry-After` will not help here; resetting a sandbox key's volume counter is operator-only — reach out to your Huli contact. **The share link says it was already used or expired** — share links are single-use with a 24-hour window. Ask your contact to refresh the sandbox key from the production key's Manage-sandbox dialog; a new key and a new link are minted, and the existing org and data are reused. ## Graduating to production When your integration is ready for real patient data, the path is the standard partner one: the clinic's admin mints you a **production** key from the same Practice Settings surface — see [Creating and sharing an API key](/v1/recipes/creating-and-sharing-an-api-key) — or your Huli contact walks you through the commercial onboarding. Your sandbox keeps working alongside it; it's a separate organization, so nothing you built against it needs to change. ## Next recipes - **[Run your first authenticated Patient search](/v1/recipes/getting-started-patient-search)** — the same request shape, against a real (non-sandbox) organization. - **[Creating and sharing an API key as a clinic admin](/v1/recipes/creating-and-sharing-an-api-key)** — the production-key flow this sandbox flow mirrors. - **[Rate Limiting](/v1/concepts/rate-limiting)** — the per-key/per-org limits every key has, sandbox or not. ======================================================================== # Scheduling an administrative meeting # URL: https://developers.huli.ai/v1/recipes/scheduling-an-administrative-meeting # Book an internal meeting as a FHIR R4 Appointment with no patient — a required title, optional all-day flag, and external email invitees — against a service whose appointment type is administrative. # Scheduling an administrative meeting Book an internal meeting — a staff huddle, a vendor call, a blocked planning hour — as a FHIR R4 `Appointment` that has **no patient**. An administrative meeting is an appointment against a service whose appointment type is `administrative`; it carries a **title**, may run **all day**, and may invite **external email attendees** who are not Huli users. The scopes are the booking ones: system/Appointment.cru for the write and system/Practitioner.rs for the practitioner wiring. The shape differs from a clinical booking in three load-bearing ways, and the server enforces all three: an administrative meeting **requires** a title, **rejects** a patient participant, and **may** carry external invitees — while a clinical appointment rejects the title and invitees and expects a patient. Pick the right service and the rest follows. ## Audience You build an internal scheduling tool, a calendar sync, or an operations integration that books non-clinical time on a practitioner's calendar. You have already [booked a clinical appointment](/v1/recipes/booking-an-appointment) — this recipe reuses that recipe's discovery (service, practitioner, room, slot) and changes only the create body. ## You'll need - A bearer token from HuliPractice (**Practice Settings → Integrations → API Keys**), or a SMART Backend Services access token. See [Bearer Tokens](/v1/auth/bearer) and [`POST /auth/token`](/v1/auth). - These two scopes on that token: - — create `Appointment` (and the gated discovery resources `HealthcareService`, `Location`, `Schedule`, `Slot`). - — read + search `Practitioner` and `PractitionerRole`. - A **service configured as an administrative meeting type**, plus a practitioner and a free slot — discovered exactly as in [Booking an appointment end-to-end](/v1/recipes/booking-an-appointment) (steps 2–5 there). The one difference is the service: pick one whose appointment type is `administrative`. A room (`Location`) participant is **optional** for administrative meetings — clinical bookings require one, administrative meetings do not. - `curl`, or Node, Python, Java, or Go. The appointment type is **derived from the service**, not sent by you. To find which service is administrative, search existing meetings with `GET /fhir/R4/Appointment?appointment-type=administrative` and read the `serviceType` off one, or confirm with the clinic admin which catalog entry is the meeting type. A `huli-appointment-title` sent against a *clinical* service is rejected, and a clinical booking with **no** patient against an *administrative* service is rejected for a missing title — the two kinds are mutually exclusive. ## End state You hold a `201 Created` whose body is the stored `Appointment` — no patient participant, a `huli-appointment-title` extension carrying the meeting name, the practitioner participant (plus a room participant if you sent one), and (if you sent them) `huli-appointment-external-attendee` extensions for the email invitees. It is searchable with `appointment-type=administrative`. **Invitees are emailed automatically.** Creating this meeting emails every `huli-appointment-external-attendee` a calendar (`ICS`) **REQUEST**; cancelling it sends a **CANCEL**, and editing it diffs the attendee set (added invitees get a REQUEST, removed ones a CANCEL, and a time change re-invites the rest). It's the same dispatch the in-app app uses, sent post-commit and at-least-once — you don't need to send your own invites. ## Steps ### 1. Export the token ```bash export HULI_TOKEN="" ``` ### 2. Find the administrative service and a slot Discover the bookable values exactly as in [Booking an appointment end-to-end](/v1/recipes/booking-an-appointment): the `serviceType` coding (from `HealthcareService.type`), a practitioner and their room (`PractitionerRole`), and a free `Slot`. Pick a service whose appointment type is `administrative`. To confirm a service is the administrative one, list existing administrative meetings — the `appointment-type` token filters on the service's derived type: ```bash curl "https://api.huli.ai/fhir/R4/Appointment?appointment-type=administrative&_count=20" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` Each match carries its `serviceType` coding and its `huli-appointment-title` extension — copy the `serviceType` of one to book another meeting against the same service. ### 3. POST the administrative Appointment Assemble the discovered `serviceType`, the slot's `start`/`end`, and the practitioner + room participants — **with no `Patient` participant**. Add the required `huli-appointment-title` extension, an optional `huli-appointment-all-day` boolean, and one `huli-appointment-external-attendee` extension per email invitee (each nests a required `email` and an optional `displayName`; up to 30). :::CodeGroup ```bash curl -i -X POST https://api.huli.ai/fhir/R4/Appointment \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Content-Type: application/fhir+json" \ -H "Accept: application/fhir+json" \ -d '{ "resourceType": "Appointment", "status": "booked", "description": "Revisión mensual de operaciones", "serviceType": [ { "coding": [ { "system": "https://fhir.huli.ai/r4/CodeSystem/org-service", "code": "01965e2a-8c4d-7000-9010-0000000000fa", "display": "Reunión administrativa" } ] } ], "start": "2026-06-18T15:00:00.000-06:00", "end": "2026-06-18T16:00:00.000-06:00", "participant": [ { "actor": { "reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1" }, "status": "accepted" }, { "actor": { "reference": "Location/01965e2a-8c4d-7000-9020-0000000000a1" }, "status": "accepted" } ], "extension": [ { "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-appointment-title", "valueString": "Revisión mensual de operaciones" }, { "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-appointment-all-day", "valueBoolean": false }, { "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-appointment-external-attendee", "extension": [ { "url": "email", "valueString": "proveedor@ejemplo.com" }, { "url": "displayName", "valueString": "Proveedor Externo" } ] } ] }' ``` ```typescript const ext = 'https://fhir.huli.ai/r4/StructureDefinition'; const meeting = { resourceType: 'Appointment', status: 'booked', description: 'Revisión mensual de operaciones', serviceType: [ { coding: [ { system: 'https://fhir.huli.ai/r4/CodeSystem/org-service', code: '01965e2a-8c4d-7000-9010-0000000000fa', // an administrative-type service display: 'Reunión administrativa', }, ], }, ], start: '2026-06-18T15:00:00.000-06:00', end: '2026-06-18T16:00:00.000-06:00', participant: [ // No Patient participant — an administrative meeting rejects one. { actor: { reference: 'Practitioner/01965e2a-8c4d-7000-9001-0000000000c1' }, status: 'accepted', }, { actor: { reference: 'Location/01965e2a-8c4d-7000-9020-0000000000a1' }, status: 'accepted' }, ], extension: [ { url: `${ext}/huli-appointment-title`, valueString: 'Revisión mensual de operaciones' }, // required { url: `${ext}/huli-appointment-all-day`, valueBoolean: false }, // optional { // optional — repeat per invitee, up to 30 url: `${ext}/huli-appointment-external-attendee`, extension: [ { url: 'email', valueString: 'proveedor@ejemplo.com' }, // required within the block { url: 'displayName', valueString: 'Proveedor Externo' }, // optional ], }, ], }; const resp = await fetch('https://api.huli.ai/fhir/R4/Appointment', { method: 'POST', headers: { Authorization: `Bearer ${process.env.HULI_TOKEN}`, 'Content-Type': 'application/fhir+json', Accept: 'application/fhir+json', }, body: JSON.stringify(meeting), }); if (resp.status === 201) { console.log('booked', (await resp.json()).id); } else { const outcome = (await resp.json()) as { issue: { diagnostics: string }[] }; console.log(resp.status, outcome.issue[0].diagnostics); } ``` ```python import os import requests ext = "https://fhir.huli.ai/r4/StructureDefinition" meeting = { "resourceType": "Appointment", "status": "booked", "description": "Revisión mensual de operaciones", "serviceType": [ { "coding": [ { "system": "https://fhir.huli.ai/r4/CodeSystem/org-service", "code": "01965e2a-8c4d-7000-9010-0000000000fa", # an administrative-type service "display": "Reunión administrativa", } ] } ], "start": "2026-06-18T15:00:00.000-06:00", "end": "2026-06-18T16:00:00.000-06:00", "participant": [ # No Patient participant — an administrative meeting rejects one. {"actor": {"reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1"}, "status": "accepted"}, {"actor": {"reference": "Location/01965e2a-8c4d-7000-9020-0000000000a1"}, "status": "accepted"}, ], "extension": [ {"url": f"{ext}/huli-appointment-title", "valueString": "Revisión mensual de operaciones"}, # required {"url": f"{ext}/huli-appointment-all-day", "valueBoolean": False}, # optional { # optional — repeat per invitee, up to 30 "url": f"{ext}/huli-appointment-external-attendee", "extension": [ {"url": "email", "valueString": "proveedor@ejemplo.com"}, # required within the block {"url": "displayName", "valueString": "Proveedor Externo"}, # optional ], }, ], } resp = requests.post( "https://api.huli.ai/fhir/R4/Appointment", headers={ "Authorization": f"Bearer {os.environ['HULI_TOKEN']}", "Content-Type": "application/fhir+json", "Accept": "application/fhir+json", }, json=meeting, timeout=30, ) print(resp.status_code, resp.json().get("id") or resp.json()["issue"][0]["diagnostics"]) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class AdminMeeting { public static void main(String[] args) throws Exception { // No Patient participant; the title extension is required; all-day and // external-attendee extensions are optional. Hand-built JSON keeps this // dependency-free; a real client would use a JSON library. String ext = "https://fhir.huli.ai/r4/StructureDefinition/"; String body = "{" + "\"resourceType\":\"Appointment\",\"status\":\"booked\"," + "\"description\":\"Revisión mensual de operaciones\"," + "\"serviceType\":[{\"coding\":[{" + "\"system\":\"https://fhir.huli.ai/r4/CodeSystem/org-service\"," + "\"code\":\"01965e2a-8c4d-7000-9010-0000000000fa\"}]}]," + "\"start\":\"2026-06-18T15:00:00.000-06:00\"," + "\"end\":\"2026-06-18T16:00:00.000-06:00\"," + "\"participant\":[" + "{\"actor\":{\"reference\":\"Practitioner/01965e2a-8c4d-7000-9001-0000000000c1\"},\"status\":\"accepted\"}," + "{\"actor\":{\"reference\":\"Location/01965e2a-8c4d-7000-9020-0000000000a1\"},\"status\":\"accepted\"}]," + "\"extension\":[" + "{\"url\":\"" + ext + "huli-appointment-title\",\"valueString\":\"Revisión mensual de operaciones\"}," + "{\"url\":\"" + ext + "huli-appointment-external-attendee\",\"extension\":[" + "{\"url\":\"email\",\"valueString\":\"proveedor@ejemplo.com\"}," + "{\"url\":\"displayName\",\"valueString\":\"Proveedor Externo\"}]}]}"; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Appointment")) .header("Authorization", "Bearer " + System.getenv("HULI_TOKEN")) .header("Content-Type", "application/fhir+json") .header("Accept", "application/fhir+json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.statusCode()); System.out.println(response.body()); } } ``` ```go package main import ( "bytes" "fmt" "io" "net/http" "os" ) func main() { // No Patient participant; the title extension is required; all-day and // external-attendee extensions are optional. body := []byte(`{ "resourceType": "Appointment", "status": "booked", "description": "Revisión mensual de operaciones", "serviceType": [{"coding": [{ "system": "https://fhir.huli.ai/r4/CodeSystem/org-service", "code": "01965e2a-8c4d-7000-9010-0000000000fa" }]}], "start": "2026-06-18T15:00:00.000-06:00", "end": "2026-06-18T16:00:00.000-06:00", "participant": [ {"actor": {"reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1"}, "status": "accepted"}, {"actor": {"reference": "Location/01965e2a-8c4d-7000-9020-0000000000a1"}, "status": "accepted"} ], "extension": [ {"url": "https://fhir.huli.ai/r4/StructureDefinition/huli-appointment-title", "valueString": "Revisión mensual de operaciones"}, {"url": "https://fhir.huli.ai/r4/StructureDefinition/huli-appointment-all-day", "valueBoolean": false}, {"url": "https://fhir.huli.ai/r4/StructureDefinition/huli-appointment-external-attendee", "extension": [ {"url": "email", "valueString": "proveedor@ejemplo.com"}, {"url": "displayName", "valueString": "Proveedor Externo"} ]} ] }`) req, err := http.NewRequest(http.MethodPost, "https://api.huli.ai/fhir/R4/Appointment", bytes.NewReader(body)) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("HULI_TOKEN")) req.Header.Set("Content-Type", "application/fhir+json") req.Header.Set("Accept", "application/fhir+json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, err := io.ReadAll(resp.Body) if err != nil { panic(err) } fmt.Printf("%d\n%s\n", resp.StatusCode, out) } ``` ::: A `201 Created` returns the stored meeting. The `huli-appointment-title` and (when `true`) `huli-appointment-all-day` extensions round-trip on every read; the `huli-appointment-external-attendee` extensions round-trip on a single-resource `GET`, create, and update — but **never on a search**, because invitee emails are PII held off the search keyset path. An **all-day** meeting (`huli-appointment-all-day: true`) must span whole local days — the service enforces midnight-to-midnight bounds, so set `start`/`end` to the local day boundaries. A partial- day window with the all-day flag set is rejected. ### 4. Edit invitees and flip a meeting later A `PUT` is a full replace. Supplying `huli-appointment-external-attendee` extension(s) **replaces** the invitee set; omitting them leaves it unchanged. Because a `PUT` replaces everything, you can flip a meeting between administrative and clinical: omit the title (clears it) and add a `Patient` participant to turn a meeting into a clinical appointment against a clinical service, or the reverse. ## What to verify - HTTP status is `201`. The response `resourceType` is `Appointment` with a server-assigned `id`. - There is **no** `Patient` participant, and the practitioner + room participants are present. - The `huli-appointment-title` extension round-trips with your meeting name. - The meeting appears in `GET /fhir/R4/Appointment?appointment-type=administrative`. - A single-resource `GET` shows the `huli-appointment-external-attendee` extensions; the same resource in a search result does not. ## What can go wrong All errors return a FHIR `OperationOutcome` — `{severity, code, diagnostics}`, no `details` object. Structural problems surface as `HPB-00101`; the booking preconditions (practitioner, room, slot conflict) surface the practice-layer `HP-008xx` codes documented in [Booking an appointment end-to-end](/v1/recipes/booking-an-appointment). **Missing title on an administrative meeting.** A meeting against an administrative service requires `huli-appointment-title`; omitting it is rejected with `issue[0].code` `required` and an expression of `Appointment.extension(huli-appointment-title)`. Add the title extension. **Title or invitees on a clinical appointment.** A `huli-appointment-title` or `huli-appointment-external-attendee` against a _clinical_ service is rejected with a `value` issue — those fields belong only to administrative meetings. Either drop them or book against an administrative service. **Patient participant on an administrative meeting.** An administrative meeting must not name a `Patient` participant. Remove it (administrative meetings are internal — the external-attendee extensions carry guests instead). **Booking precondition / specialty.** The same guards as a clinical booking apply: the practitioner must have the room in their assigned locations, the slot must be free (`409` `HP-00803`), and a multi-specialty service still needs an `Appointment.specialty` selection (`HPB-00115`/`HPB-00116`). See the booking recipe's [What can go wrong](/v1/recipes/booking-an-appointment). ## Next recipes - **[Booking an appointment end-to-end](/v1/recipes/booking-an-appointment)** — the clinical counterpart, with the full discovery flow this recipe reuses. - **[Wiring a read-only partner](/v1/recipes/wiring-a-read-only-partner)** — consume the appointment feed read-only, filtering administrative vs clinical with `appointment-type`. ======================================================================== # Uploading a document # URL: https://developers.huli.ai/v1/recipes/uploading-a-document # Attach a lab PDF, scan, or image to a patient as a FHIR R4 DocumentReference — multipart $upload or inline base64 — then read it back via a 30-minute signed URL. Uses the BAA-gated system/DocumentReference.cru scope. # Uploading a document Attach a binary — a lab result PDF, a scanned referral, an imaging file — to a patient as a FHIR R4 `DocumentReference`, then read it back through a short-lived signed download URL. You will upload by two routes (multipart for a real file on disk, inline base64 for a small payload you already hold in memory), read the document, search a patient's documents, and soft-delete a mistake. One scope carries the flow: system/DocumentReference.cru — create, read, and update. The binary is never stored in or returned from the resource body. On read, the document's bytes are served via a **30-minute signed URL** on `content[0].attachment.url`; the resource itself only carries metadata. That split is the thing to internalize: you `$upload` bytes once, then every later read hands you a fresh, expiring URL to fetch them. ## Audience You integrate a lab, an imaging system, or a document pipeline that pushes files into a patient's chart. You read a `Bundle` without a viewer, you can build a `multipart/form-data` request or base64-encode a file, and you want a document from disk to a stored, retrievable `DocumentReference`. ## You'll need - A bearer token from HuliPractice (**Practice Settings → Integrations → API Keys**), or a SMART Backend Services access token. See [Bearer Tokens](/v1/auth/bearer) for provisioning and [`POST /auth/token`](/v1/auth) for the token exchange. - The system/DocumentReference.cru scope on that token. It sits under the **Clinical information** card, which is **BAA-gated** — the clinic admin must attest to a Business Associate Agreement before a key carrying it can be minted. `.cru` grants upload + read + update; system/DocumentReference.rs alone grants read + search. - The `id` of the patient the document belongs to. Resolve it with [a Patient search](/v1/recipes/getting-started-patient-search) if you only hold a name. - A file to upload. Allowed types are **PDF, JPEG, PNG, WEBP, and DICOM**, up to **25 MB**. - `curl`, or Node or Python if you prefer a language client. The server detects the real content type from the file's **magic bytes** and requires the filename extension to match — so the **filename is required** on every upload, and a `.pdf` whose bytes are actually a PNG is rejected. There is **no `DELETE`** verb: retire a document by `PUT`ing `status: "entered-in-error"`, which soft-deletes it. ## End state You hold a `201 Created` whose body is the stored `DocumentReference` — patient as `subject`, a server-detected `content[0].attachment.contentType`, the file size and SHA-256 hash, and (on a later read) a signed `url` you can `GET` to download the bytes for the next 30 minutes. ## Steps ### 1. Export the token and the patient id ```bash export HULI_TOKEN="" export PATIENT_ID="01965e2a-8c4d-7000-9001-0000000000a2" ``` ### 2. Upload the file (multipart) For a real file on disk, `POST` to the `$upload` operation as `multipart/form-data`. The form takes a `file` part (the binary), a `subject` field (a `Patient` reference or bare UUID), and an optional `encounter` field to link the document to a visit. :::CodeGroup ```bash curl -i -X POST https://api.huli.ai/fhir/R4/DocumentReference/\$upload \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" \ -F "file=@resultados-laboratorio.pdf;type=application/pdf" \ -F "subject=Patient/01965e2a-8c4d-7000-9001-0000000000a2" \ -F "encounter=Encounter/01965e2a-8c4d-7000-9060-0000000000e9" ``` ```typescript import { readFile } from 'node:fs/promises'; const bytes = await readFile('resultados-laboratorio.pdf'); const form = new FormData(); form.set('file', new Blob([bytes], { type: 'application/pdf' }), 'resultados-laboratorio.pdf'); form.set('subject', `Patient/${process.env.PATIENT_ID}`); form.set('encounter', 'Encounter/01965e2a-8c4d-7000-9060-0000000000e9'); // optional const resp = await fetch('https://api.huli.ai/fhir/R4/DocumentReference/$upload', { method: 'POST', headers: { Authorization: `Bearer ${process.env.HULI_TOKEN}`, Accept: 'application/fhir+json', // Do NOT set Content-Type — fetch sets the multipart boundary for you. }, body: form, }); if (resp.status === 201) { const created = (await resp.json()) as { id: string }; console.log('uploaded', created.id); } else { const outcome = (await resp.json()) as { issue: { diagnostics: string }[] }; console.log(resp.status, outcome.issue[0].diagnostics); } ``` ```python import os import requests with open("resultados-laboratorio.pdf", "rb") as fh: resp = requests.post( "https://api.huli.ai/fhir/R4/DocumentReference/$upload", headers={ "Authorization": f"Bearer {os.environ['HULI_TOKEN']}", "Accept": "application/fhir+json", }, files={"file": ("resultados-laboratorio.pdf", fh, "application/pdf")}, data={ "subject": f"Patient/{os.environ['PATIENT_ID']}", "encounter": "Encounter/01965e2a-8c4d-7000-9060-0000000000e9", # optional }, timeout=60, ) if resp.status_code == 201: print("uploaded", resp.json()["id"]) else: print(resp.status_code, resp.json()["issue"][0]["diagnostics"]) ``` ::: A `201 Created` returns the stored `DocumentReference` with a server-assigned `id` and a `Location` header. The bytes are referenced, not inlined — `content[0].attachment.url` holds a 30-minute signed URL on the create/read response: ```json { "resourceType": "DocumentReference", "id": "01965e2a-8c4d-7000-9070-0000000000f4", "meta": { "versionId": "1769472901000000000", "lastUpdated": "2026-06-26T10:15:01.000-06:00", "profile": ["https://fhir.huli.ai/r4/StructureDefinition/HuliDocumentReference"] }, "status": "current", "subject": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2", "type": "Patient" }, "author": [ { "reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1", "type": "Practitioner" } ], "date": "2026-06-26T10:15:01.000-06:00", "content": [ { "attachment": { "contentType": "application/pdf", "url": "https://storage.googleapis.com/huli-prod-documents/...&X-Goog-Expires=1800&...", "size": 248913, "hash": "k3m2Q9c0Vp9d1xQe3rJh8oH2bW5sQ0aZ7tC4uN6vY8=", "title": "resultados-laboratorio.pdf" } } ], "context": { "encounter": [ { "reference": "Encounter/01965e2a-8c4d-7000-9060-0000000000e9", "type": "Encounter" } ] } } ``` `contentType` is **server-detected from the bytes**, not echoed from your form — proof the magic- byte check ran. `hash` is the base64 of the file's SHA-256 digest. ### 3. Upload inline (base64) — the small-payload alternative When you already hold the bytes in memory, skip multipart and `POST` a JSON `DocumentReference` with the binary base64-encoded in `content[0].attachment.data`. This works on both `POST /DocumentReference` and the `$upload` operation. The `title` (filename) is **required** so the extension can be matched against the detected type. :::CodeGroup ```bash curl -i -X POST https://api.huli.ai/fhir/R4/DocumentReference \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Content-Type: application/fhir+json" \ -H "Accept: application/fhir+json" \ -d '{ "resourceType": "DocumentReference", "status": "current", "subject": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2" }, "context": { "encounter": [ { "reference": "Encounter/01965e2a-8c4d-7000-9060-0000000000e9" } ] }, "content": [ { "attachment": { "contentType": "application/pdf", "title": "resultados-laboratorio.pdf", "data": "JVBERi0xLjQKJ..." } } ] }' ``` ```python import base64 import os import requests with open("resultados-laboratorio.pdf", "rb") as fh: data = base64.standard_b64encode(fh.read()).decode("ascii") document = { "resourceType": "DocumentReference", "status": "current", "subject": {"reference": f"Patient/{os.environ['PATIENT_ID']}"}, # required "context": {"encounter": [{"reference": "Encounter/01965e2a-8c4d-7000-9060-0000000000e9"}]}, # optional "content": [ { "attachment": { "contentType": "application/pdf", "title": "resultados-laboratorio.pdf", # required — extension matched to detected type "data": data, # required — base64 of the bytes } } ], } resp = requests.post( "https://api.huli.ai/fhir/R4/DocumentReference", headers={ "Authorization": f"Bearer {os.environ['HULI_TOKEN']}", "Content-Type": "application/fhir+json", "Accept": "application/fhir+json", }, json=document, timeout=60, ) print(resp.status_code, resp.json().get("id") or resp.json()["issue"][0]["diagnostics"]) ``` ::: Prefer multipart for anything beyond a few hundred kilobytes: base64 inflates the payload ~33% and counts against the same 25 MB ceiling once decoded. Inline mode is convenient for small, in-memory payloads; multipart streams the file without the encoding overhead. ### 4. Read the document and download the bytes Read the `DocumentReference` by id to get a **fresh** signed URL, then `GET` that URL to download. The signed URL expires after 30 minutes — fetch it shortly after the read, and re-read for a new one rather than caching it. ```bash # 1. Read the resource to get a fresh signed URL. URL=$(curl -s "https://api.huli.ai/fhir/R4/DocumentReference/01965e2a-8c4d-7000-9070-0000000000f4" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" \ | jq -r '.content[0].attachment.url') # 2. Download the bytes (the signed URL needs no Authorization header). curl -s "$URL" -o resultados-laboratorio.pdf ``` ### 5. Search a patient's documents `DocumentReference` search is patient-scoped, so the `patient` parameter is **required**. Narrow with `category`, `type` (a LOINC code), or `date`. Search results carry the metadata but **no signed URL** — read the individual document (step 4) when you need the bytes. ```bash curl "https://api.huli.ai/fhir/R4/DocumentReference?patient=01965e2a-8c4d-7000-9001-0000000000a2&category=laboratory&_count=20" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` `category` codes come from the `document-category` CodeSystem — `laboratory`, `imaging`, `clinical_note`, `prescription`, `administrative`, `consent`, `growth_booklet`, `other`. Search pages with `_count` + `_cursor` (follow the `next` link); the `category` filter repaginates with an exact total, while `type` and `date` filter the current page. ### 6. Correct mistakes — metadata edit and soft-delete A `PUT` either updates the document's metadata (`category`, `description`) or — when the body sets `status: "entered-in-error"` — soft-deletes it. The binary itself is immutable on this surface; to replace the file, upload a new document. ```bash # Soft-delete a document uploaded against the wrong patient. curl -i -X PUT https://api.huli.ai/fhir/R4/DocumentReference/01965e2a-8c4d-7000-9070-0000000000f4 \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Content-Type: application/fhir+json" \ -H "Accept: application/fhir+json" \ -d '{ "resourceType": "DocumentReference", "status": "entered-in-error" }' ``` A `200 OK` returns the document with `status: "entered-in-error"`; it then drops out of reads and searches. ## What to verify - The upload is `201`. The response `resourceType` is `DocumentReference` with a server-assigned `id` and a `Location` header. - `content[0].attachment.contentType` is the **detected** type (e.g. `application/pdf`) and `size`/`hash` are populated. - A read returns a fresh `content[0].attachment.url`, and a `GET` on that URL downloads the bytes. - The document appears in a `patient`-scoped search; after an `entered-in-error` `PUT`, it no longer does. ## What can go wrong All errors return a FHIR `OperationOutcome` — `{severity, code, diagnostics}`, no `details` object. Branch on the HTTP status and `issue[0].code`; the Huli code is the prefix of `issue[0].diagnostics`, split on `": "`. `HPB-00119` — **document too large.** The file exceeds the 25 MB ceiling. Compress or split it; the limit is enforced on the decoded bytes, so a base64 inline payload hits it sooner than its wire size suggests. `HPB-00120` — **content invalid or type mismatch.** The bytes are not one of PDF/JPEG/PNG/WEBP/DICOM, or the filename extension does not match the detected type (a `.pdf` whose magic bytes are a PNG). Send the true file with its real extension. `HPB-00101` — **validation error.** A required field is missing — the multipart `file` or `subject`, or, inline, `content[0].attachment.data` or `.title` (filename). Add the missing field. `HPB-00104` — **insufficient scope.** The token lacks system/DocumentReference.cru (or `.rs` for a read). Because the **Clinical information** card is BAA-gated, confirm the key was minted with a BAA attestation. `HPB-00118` — **DocumentReference not found.** The id does not name a document in your organization, or it was soft-deleted. Confirm the id and the token's organization. **Document storage not configured.** This server has no document storage wired; the surface fails closed rather than accepting an upload it cannot persist. This is an operator-side gap, not a request error. A representative `413` body: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "processing", "diagnostics": "HPB-00119: Document exceeds the maximum allowed size" } ] } ``` ## Next recipes - **[Writing and amending a clinical note](/v1/recipes/writing-a-clinical-note)** — the `Composition` narrative that sits alongside these documents under the Clinical information card. - **[Fetching a patient's full record](/v1/recipes/fetching-a-patient-record)** — pull a patient's documents together with their encounters, observations, and notes in one `$everything` Bundle. - **[Creating a clinical encounter](/v1/recipes/creating-an-encounter)** — create the visit you link a document to via `context.encounter`. ======================================================================== # Wire a read-only partner — appointments and encounters # URL: https://developers.huli.ai/v1/recipes/wiring-a-read-only-partner # Give an analytics or reporting tool least-privilege read access to the Appointment + Encounter feed with system/Appointment.rs and system/Encounter.rs — search, read, paginate, and resolve the Practitioner/Organization references they point at. Request only .rs scopes. # Wire a read-only partner — appointments and encounters Give a partner that only needs to read — an analytics dashboard, a reporting tool, a referral network — least-privilege access to the FHIR R4 appointment and encounter feed. Search both resources, read a single resource by id, walk a multi-page `searchset` cursor, and resolve the `Practitioner` and `Organization` references those resources point at. Two scopes carry the whole integration — system/Appointment.rs and system/Encounter.rs — plus plain reads of the `Practitioner` and `Organization` resources those records point at. This integration only reads. The v1 surface for a read-only partner is `Appointment`, `Encounter`, `Practitioner`, and `Organization` — request only `.rs` scopes. You will not `POST` and you will not `PUT`: a token minted with only `.rs` scopes cannot mutate the source system even by accident, which is exactly the posture you want for a feed consumer. ## Audience You build an analytics or reporting product — or a referral network — that ingests an appointment and encounter feed from partner clinics. You have called a FHIR R4 server before, you read a `Bundle` without a viewer, and you want a read-only pull integration wired against the v1 surface — nothing reaches back and mutates the source system. ## You'll need - An admin bearer token from HuliPractice (**Practice Settings → Integrations → API Keys**). An admin-role user on the partner organization mints it; the token is shown once, so copy it immediately. - These two scopes on that token: - — read plus search on `Appointment`. - — read plus search on `Encounter`. `r` is read-by-id, `s` is search; `rs` grants both. The `Practitioner` and `Organization` references inside those resources are read-only resources you resolve with a plain read — no separate write scope exists for them in v1. - `curl`, or Node, Python, Java, or Go if you prefer a language client. The admin bearer token is a long-lived credential scoped to one organization. It does not expire on a timer the way SMART Backend Services access tokens do. Treat it as a secret: keep it in an environment variable or a secrets manager, never in source control or a client bundle. When you ship server-to-server, swap it for SMART Backend Services (`client_credentials` + `private_key_jwt`, RS384, 5-minute access token) — the scopes and request shapes in this recipe are identical. This partner is read-only in v1. Request only `.rs` scopes — never `.c`, `.u`, or `.cru` on any resource. A token minted with only `.rs` scopes cannot write even by accident, which is the posture you want for a feed consumer. ## End state You hold two `searchset` `Bundle`s — one of `Appointment` resources, one of `Encounter` resources — for Doctora María Fernández's schedule at Clínica San Rafael over a date window. You can read any single resource by id, follow the `link[rel=next]` cursor across pages, and dereference the `Practitioner` and `Organization` each resource points at. ## Steps ### 1. Export the token ```bash export HULI_API_KEY="" ``` Confirm it is set: ```bash echo $HULI_API_KEY ``` ### 2. Search appointments over a date window `Appointment` search accepts `patient`, `practitioner`, `date`, and `status`. The `date` parameter takes a FHIR prefix (`eq`, `gt`, `ge`, `lt`, `le`); pass it twice to bound a window — `date=ge2026-06-01` and `date=le2026-06-30` for the month of June. Timestamps are ISO-8601 with offset on the wire. The full parameter list per resource lives in the [FHIR Implementation Guide](https://developers.huli.ai/fhir/). :::CodeGroup ```bash curl "https://api.huli.ai/fhir/R4/Appointment?practitioner=01965e2a-8c4d-7000-9001-0000000000c1&date=ge2026-06-01&date=le2026-06-30&status=booked&_count=50" \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Accept: application/fhir+json" ``` ```typescript const params = new URLSearchParams(); params.set('practitioner', '01965e2a-8c4d-7000-9001-0000000000c1'); params.append('date', 'ge2026-06-01'); params.append('date', 'le2026-06-30'); params.set('status', 'booked'); params.set('_count', '50'); const resp = await fetch(`https://api.huli.ai/fhir/R4/Appointment?${params}`, { headers: { Authorization: `Bearer ${process.env.HULI_API_KEY}`, Accept: 'application/fhir+json', }, }); console.log(resp.status); console.log(await resp.json()); ``` ```python import os import requests resp = requests.get( "https://api.huli.ai/fhir/R4/Appointment", params=[ ("practitioner", "01965e2a-8c4d-7000-9001-0000000000c1"), ("date", "ge2026-06-01"), ("date", "le2026-06-30"), ("status", "booked"), ("_count", "50"), ], headers={ "Authorization": f"Bearer {os.environ['HULI_API_KEY']}", "Accept": "application/fhir+json", }, timeout=30, ) print(resp.status_code) print(resp.json()) ``` ```java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; public class AppointmentSearch { public static void main(String[] args) throws Exception { // A repeated parameter (date) is two key=value pairs joined by &. StringBuilder query = new StringBuilder(); query.append("practitioner=").append(enc("01965e2a-8c4d-7000-9001-0000000000c1")); query.append("&date=").append(enc("ge2026-06-01")); query.append("&date=").append(enc("le2026-06-30")); query.append("&status=").append(enc("booked")); query.append("&_count=").append(enc("50")); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Appointment?" + query)) .header("Authorization", "Bearer " + System.getenv("HULI_API_KEY")) .header("Accept", "application/fhir+json") .GET() .build(); HttpResponse response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.statusCode()); System.out.println(response.body()); } private static String enc(String value) { return URLEncoder.encode(value, StandardCharsets.UTF_8); } } ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" ) func main() { endpoint, err := url.Parse("https://api.huli.ai/fhir/R4/Appointment") if err != nil { panic(err) } q := endpoint.Query() q.Set("practitioner", "01965e2a-8c4d-7000-9001-0000000000c1") q.Add("date", "ge2026-06-01") q.Add("date", "le2026-06-30") q.Set("status", "booked") q.Set("_count", "50") endpoint.RawQuery = q.Encode() req, err := http.NewRequest(http.MethodGet, endpoint.String(), nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("HULI_API_KEY")) req.Header.Set("Accept", "application/fhir+json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { panic(err) } // Production code branches on status and decodes the OperationOutcome on the // error paths. The "What can go wrong" section maps each code. switch resp.StatusCode { case http.StatusOK: fmt.Printf("200 OK\n%s\n", body) case http.StatusUnauthorized: // HPB-00106 auth failed / HPB-00107 auth expired fmt.Printf("401 unauthorized\n%s\n", body) case http.StatusForbidden: // HPB-00104 insufficient scope fmt.Printf("403 forbidden\n%s\n", body) case http.StatusBadRequest: // HPB-00101 validation error fmt.Printf("400 bad request\n%s\n", body) case http.StatusTooManyRequests: // HPB-00105 rate limited fmt.Printf("429 rate limited (Retry-After: %s)\n%s\n", resp.Header.Get("Retry-After"), body) default: fmt.Printf("%d\n%s\n", resp.StatusCode, body) } } ``` ::: A repeated parameter like `date` is a list of tuples in Python's `requests`, `params.append` in TypeScript's `URLSearchParams`, two `&date=` pairs in a hand-built Java query string, and `q.Add` in Go. Using `params.set` / `q.Set` twice overwrites the first value and you lose one bound of the window. A `200 OK` returns a `Bundle` of type `searchset`. Resources live under `entry[].resource`; `total` is the match count. ```json { "resourceType": "Bundle", "id": "01965e2a-8c4d-7000-9002-0000000000d0", "meta": { "lastUpdated": "2026-06-02T08:30:00.000-06:00" }, "type": "searchset", "total": 1, "link": [ { "relation": "self", "url": "https://api.huli.ai/fhir/R4/Appointment?practitioner=01965e2a-8c4d-7000-9001-0000000000c1&date=ge2026-06-01&date=le2026-06-30&status=booked&_count=50" } ], "entry": [ { "fullUrl": "https://api.huli.ai/fhir/R4/Appointment/01965e2a-8c4d-7000-9002-0000000000d1", "resource": { "resourceType": "Appointment", "id": "01965e2a-8c4d-7000-9002-0000000000d1", "meta": { "versionId": "1", "lastUpdated": "2026-05-30T11:02:18.000-06:00" }, "status": "booked", "start": "2026-06-12T09:00:00.000-06:00", "end": "2026-06-12T09:30:00.000-06:00", "participant": [ { "actor": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2", "display": "Ana Lucía Fernández Ramírez" }, "status": "accepted" }, { "actor": { "reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1", "display": "Doctora María Fernández" }, "status": "accepted" } ] } } ] } ``` ### 3. Search encounters for the same window `Encounter` search accepts `patient`, `date`, `status`, and `class`. The same `date`-prefix rule applies. To pull every encounter for one patient, pass `patient=` instead of (or alongside) the date window. ```bash curl "https://api.huli.ai/fhir/R4/Encounter?date=ge2026-06-01&date=le2026-06-30&status=finished&_count=50" \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Accept: application/fhir+json" ``` A representative `Encounter` resource inside the `searchset`: ```json { "resourceType": "Encounter", "id": "01965e2a-8c4d-7000-9003-0000000000e1", "meta": { "versionId": "2", "lastUpdated": "2026-06-12T10:14:55.000-06:00" }, "status": "finished", "class": { "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "AMB", "display": "ambulatory" }, "subject": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2", "display": "Ana Lucía Fernández Ramírez" }, "participant": [ { "individual": { "reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1", "display": "Doctora María Fernández" } } ], "period": { "start": "2026-06-12T09:02:11.000-06:00", "end": "2026-06-12T09:41:37.000-06:00" }, "serviceProvider": { "reference": "Organization/01965e2a-8c4d-7000-9001-0000000000b0", "display": "Clínica San Rafael" } } ``` ### 4. Read a single resource by id When you already hold an id — from a prior search, a webhook, or a referral payload — read it directly instead of searching. The `r` in `.rs` grants this. ```bash curl "https://api.huli.ai/fhir/R4/Encounter/01965e2a-8c4d-7000-9003-0000000000e1" \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Accept: application/fhir+json" ``` A read-by-id returns the bare resource (not a `Bundle`) on `200`, or `HPB-00102` if the id does not exist in your organization. ### 5. Resolve the Practitioner and Organization references Both `Appointment` and `Encounter` carry references to a `Practitioner` (`participant[].actor` / `participant[].individual`) and `Encounter` names an `Organization` under `serviceProvider`. Resolve a reference by reading the resource it names. `Practitioner` and `Organization` are read-only in v1 — a plain `GET` is all you get, and all you need. ```bash curl "https://api.huli.ai/fhir/R4/Practitioner/01965e2a-8c4d-7000-9001-0000000000c1" \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Accept: application/fhir+json" ``` ```json { "resourceType": "Practitioner", "id": "01965e2a-8c4d-7000-9001-0000000000c1", "active": true, "name": [ { "use": "official", "family": "Fernández", "given": ["María"], "prefix": ["Dra."] } ] } ``` ```bash curl "https://api.huli.ai/fhir/R4/Organization/01965e2a-8c4d-7000-9001-0000000000b0" \ -H "Authorization: Bearer $HULI_API_KEY" \ -H "Accept: application/fhir+json" ``` ```json { "resourceType": "Organization", "id": "01965e2a-8c4d-7000-9001-0000000000b0", "active": true, "name": "Clínica San Rafael" } ``` Resolving references with your `.rs` scopes covers the read of these two resource types — they share the organization scope of your token, so no extra grant is needed. Cache them: the same `Practitioner` and `Organization` recur across every appointment and encounter in the feed, so a per-id cache cuts your request volume against the rate limit. ### 6. Follow the cursor across pages When a `searchset` spans more than one `_count` page, the `Bundle` carries a `link` entry with `relation: "next"` whose `url` holds an opaque cursor. Follow it verbatim — do not parse, rebuild, or re-sort it. The last page omits the `next` link. ```json { "resourceType": "Bundle", "type": "searchset", "total": 138, "link": [ { "relation": "self", "url": "https://api.huli.ai/fhir/R4/Encounter?date=ge2026-06-01&date=le2026-06-30&status=finished&_count=50" }, { "relation": "next", "url": "https://api.huli.ai/fhir/R4/Encounter?date=ge2026-06-01&date=le2026-06-30&status=finished&_count=50&_cursor=eyJ0IjoiMjAyNi0wNi0xMlQwOTo0MTozNy0wNjowMCIsImlkIjoiMDE5NjVlMmEtOGM0ZC03MDAwLTkwMDMtMDAwMDAwMDAwMGUxIn0" } ], "entry": [] } ``` A loop that walks every page and accumulates entries: :::CodeGroup ```typescript const headers = { Authorization: `Bearer ${process.env.HULI_API_KEY}`, Accept: 'application/fhir+json', }; const first = new URLSearchParams(); first.append('date', 'ge2026-06-01'); first.append('date', 'le2026-06-30'); first.set('status', 'finished'); first.set('_count', '50'); let url: string | null = `https://api.huli.ai/fhir/R4/Encounter?${first}`; const encounters: unknown[] = []; while (url) { const resp = await fetch(url, { headers }); if (!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); const bundle = await resp.json(); for (const entry of bundle.entry ?? []) encounters.push(entry.resource); // The next link is fully formed — assign it verbatim. url = bundle.link?.find((l: { relation: string }) => l.relation === 'next')?.url ?? null; } console.log(`pulled ${encounters.length} encounters`); ``` ```python import os import requests session = requests.Session() session.headers.update({ "Authorization": f"Bearer {os.environ['HULI_API_KEY']}", "Accept": "application/fhir+json", }) url = "https://api.huli.ai/fhir/R4/Encounter" params = [ ("date", "ge2026-06-01"), ("date", "le2026-06-30"), ("status", "finished"), ("_count", "50"), ] encounters = [] while url: resp = session.get(url, params=params, timeout=30) resp.raise_for_status() bundle = resp.json() encounters.extend(e["resource"] for e in bundle.get("entry", [])) # The next link is already fully formed — follow it verbatim, params=None. url = next( (l["url"] for l in bundle.get("link", []) if l["relation"] == "next"), None, ) params = None print(f"pulled {len(encounters)} encounters") ``` ```java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.regex.Matcher; import java.util.regex.Pattern; public class EncounterWalk { public static void main(String[] args) throws Exception { String query = "date=" + enc("ge2026-06-01") + "&date=" + enc("le2026-06-30") + "&status=" + enc("finished") + "&_count=" + enc("50"); HttpClient client = HttpClient.newHttpClient(); String url = "https://api.huli.ai/fhir/R4/Encounter?" + query; int pulled = 0; while (url != null) { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("Authorization", "Bearer " + System.getenv("HULI_API_KEY")) .header("Accept", "application/fhir+json") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { throw new RuntimeException(response.statusCode() + " " + response.body()); } String body = response.body(); pulled += countMatches(body, "\"resource\""); // The next link is fully formed — follow it verbatim. url = nextLink(body); } System.out.println("pulled " + pulled + " encounters"); } private static String nextLink(String body) { Matcher m = Pattern.compile( "\\{[^{}]*\"relation\"\\s*:\\s*\"next\"[^{}]*\"url\"\\s*:\\s*\"([^\"]+)\"" + "|\\{[^{}]*\"url\"\\s*:\\s*\"([^\"]+)\"[^{}]*\"relation\"\\s*:\\s*\"next\"") .matcher(body); if (m.find()) { return m.group(1) != null ? m.group(1) : m.group(2); } return null; } private static int countMatches(String haystack, String needle) { int count = 0; for (int i = haystack.indexOf(needle); i >= 0; i = haystack.indexOf(needle, i + 1)) { count++; } return count; } private static String enc(String value) { return URLEncoder.encode(value, StandardCharsets.UTF_8); } } ``` ```go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" ) type bundle struct { Link []struct { Relation string `json:"relation"` URL string `json:"url"` } `json:"link"` Entry []struct { Resource json.RawMessage `json:"resource"` } `json:"entry"` } func main() { start, _ := url.Parse("https://api.huli.ai/fhir/R4/Encounter") q := start.Query() q.Add("date", "ge2026-06-01") q.Add("date", "le2026-06-30") q.Set("status", "finished") q.Set("_count", "50") start.RawQuery = q.Encode() next := start.String() var encounters []json.RawMessage for next != "" { req, _ := http.NewRequest(http.MethodGet, next, nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("HULI_API_KEY")) req.Header.Set("Accept", "application/fhir+json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } body, _ := io.ReadAll(resp.Body) resp.Body.Close() var b bundle if err := json.Unmarshal(body, &b); err != nil { panic(err) } for _, e := range b.Entry { encounters = append(encounters, e.Resource) } next = "" for _, l := range b.Link { if l.Relation == "next" { next = l.URL // already fully formed; follow verbatim } } } fmt.Printf("pulled %d encounters\n", len(encounters)) } ``` ::: ## What to verify - HTTP status is `200` for each search and read. - For searches, `resourceType` is `Bundle` and `type` is `searchset`; for a read-by-id, the body is the bare resource (`Appointment` / `Encounter` / `Practitioner` / `Organization`). - `total` matches the count of `entry` items you expected for the window. - Each appointment carries a `Practitioner` reference under `participant[].actor`; each encounter carries one under `participant[].individual` and an `Organization` under `serviceProvider`. - Resolving those references with a read-by-id returns `200`, confirming your `.rs` scopes cover the read-only `Practitioner` and `Organization`. - The cursor walk terminates — the final page has no `link[rel=next]`, and your accumulated count equals `total`. ## What can go wrong All errors return a FHIR `OperationOutcome`, never a bare string. Branch on the HTTP status and `issue[0].code` (the FHIR IssueType) for machine-readable classification. The Huli-specific code (`HPB-…`) is the prefix of `issue[0].diagnostics` — split on `": "` to extract it. There is no `details` object, no `coding`, no `text`. `HPB-00106` — auth failed. The token is missing, malformed, or revoked. Confirm the header reads `Authorization: Bearer ` with a single space and that `$HULI_API_KEY` is exported in this shell. `HPB-00107` (auth expired) applies only to the time-limited tokens from SMART Backend Services; if you see it with an admin bearer token, you are sending a SMART access token by mistake. `HPB-00104` — insufficient scope. The token authenticated but lacks the scope for the resource you hit — `system/Appointment.rs` for `Appointment`, `system/Encounter.rs` for `Encounter`. Re-mint the key in Practice Settings with both scopes selected. You also land here if you try to write: a `.rs` token has no `c` or `u` permission, so a `POST` or `PUT` returns `403`. `HPB-00101` — validation error. A search parameter is malformed — an unknown parameter, a bad `date` prefix (use `ge` / `le`, not `>=`), or a non-ISO-8601 timestamp. Check the parameter against the per-resource search reference in the [FHIR Implementation Guide](https://developers.huli.ai/fhir/). `HPB-00102` — not found. The id in a read-by-id does not exist within your organization. Searches never return `404` for an empty result — they return a `200` `searchset` with `total: 0`. `HPB-00105` — rate limited. You exceeded the per-key request budget — common when resolving references without caching. Read the `Retry-After` response header and back off for that many seconds before retrying. A representative `403` body: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "forbidden", "diagnostics": "HPB-00104: Insufficient scope" } ] } ``` This is a read-only partner in v1. A token carrying only `.rs` scopes returns `HPB-00104` on any write attempt — there is no create or update path for this integration. If your pipeline has a write step, gate it off here; the v1 feed is a pull-only source. ## Next recipes - **Paginate large result sets** — walk the `link[rel=next]` cursor with backoff and resume, the general pattern this recipe applies to the encounter feed. - **Authenticate as a SMART Backend Service** — swap the admin bearer token for `client_credentials` + `private_key_jwt` (RS384, 5-minute access token) when you ship the feed consumer server-to-server. Discover the endpoints at `https://api.huli.ai/fhir/.well-known/smart-configuration` and verify signatures against `https://api.huli.ai/fhir/.well-known/jwks.json`. - **Read Observations for an encounter** — add `system/Observation.rs` and pull vital-signs, laboratory, and exam results scoped to each `Encounter` you ingested here. ======================================================================== # Writing and amending a clinical note # URL: https://developers.huli.ai/v1/recipes/writing-a-clinical-note # Create a FHIR R4 Composition — the LOINC-sectioned clinical narrative of a visit — then amend it safely with If-Match optimistic concurrency. Uses the BAA-gated system/Composition.cru scope. # Writing and amending a clinical note Record a visit's clinical narrative as a FHIR R4 `Composition`, read it back, then amend it without clobbering a concurrent edit. A `Composition` is the **clinical-note projection of an encounter**: the same stored visit the `Encounter` resource renders, exposed as LOINC-coded narrative sections (chief complaint, history, findings, assessment, plan) instead of the visit envelope. One scope carries the flow: system/Composition.cru — create, read, and update, including the conditional `PUT` that makes amendments safe. The part most first writes get wrong is the amend. Two clients that both read a note and both `PUT` it will silently lose one edit unless the second write is rejected. The `If-Match` header and the weak `ETag` the API returns on every read close that window — send the version you read, and a stale write fails loudly with a `409` instead of overwriting fresher data. ## Audience You integrate an EHR or a scribe tool that writes clinical narrative back into HuliPractice. You have [created an Encounter](/v1/recipes/creating-an-encounter) before, you read a `Bundle` without a viewer, and you know what a FHIR reference is. You want to take a note from draft to a stored, amendable `Composition`. ## You'll need - A bearer token from HuliPractice (**Practice Settings → Integrations → API Keys**), or a SMART Backend Services access token. See [Bearer Tokens](/v1/auth/bearer) for provisioning and [`POST /auth/token`](/v1/auth) for the token exchange — this recipe assumes you already hold one. - The system/Composition.cru scope on that token. It sits under the **Medical records** card, which is **BAA-gated** — the clinic admin must attest to a Business Associate Agreement before a key carrying it can be minted. `.cru` grants create, read, and update; system/Composition.rs alone grants read + search. - The `id` of the patient the note is for, and the `id` of the authoring practitioner. Resolve them with [a Patient search](/v1/recipes/getting-started-patient-search) and a [Practitioner search](/v1/recipes/creating-an-encounter) if you only hold names. - `curl`, or Node, Python, Java, or Go if you prefer a language client. A `Composition` and an `Encounter` are two projections of **one** stored visit. Creating a `Composition` creates a new visit row; the same row is readable as an `Encounter`. The two own different fields — `Composition` maps the narrative sections, `Encounter` maps the visit envelope and the diagnoses — so a `Composition` write preserves whatever the `Encounter` surface set, and vice versa. There is **no `_history`, no `vread`, and no `DELETE`** on `Composition`; correct a note's lifecycle through `status` on a `PUT`. ## End state You hold a `201 Created` whose body is the stored `Composition` — with a server-assigned `id`, the patient as `subject`, the practitioner as `author`, the LOINC type `11488-4` (consultation note), and your narrative under `section[]`. You then read it back, capture its `ETag`, and land a `200 OK` amendment guarded by `If-Match`. ## Steps ### 1. Export the token and the references ```bash export HULI_TOKEN="" export PATIENT_ID="01965e2a-8c4d-7000-9001-0000000000a2" export PRACTITIONER_ID="01965e2a-8c4d-7000-9001-0000000000c1" ``` ### 2. Create the note `POST` a `Composition` with `status`, the patient `subject`, the practitioner `author`, and one LOINC-coded `section` per narrative field. The section code routes the text back to its field, so use the exact LOINC codes below — an unrecognized code is ignored on write. | Section | LOINC code | Maps to | | -------------------------- | ---------- | --------------------- | | Chief complaint | `10154-3` | reason for the visit | | History of present illness | `10164-2` | subjective history | | Physical findings | `29545-1` | objective findings | | Assessment | `51848-0` | diagnostic impression | | Plan of care | `18776-5` | care-plan narrative | `status` is the FHIR Composition document status and it drives the underlying visit state: `preliminary` → an in-progress visit, `final` → a completed visit, `entered-in-error` → a voided visit. `amended` keeps the visit completed. The **diagnosis** section (LOINC `29308-4`) is **read-only** here — it is emitted on read but ignored on write; set diagnoses through the [Encounter](/v1/recipes/creating-an-encounter) surface, and a `Composition` `PUT` preserves them. :::CodeGroup ```bash curl -i -X POST https://api.huli.ai/fhir/R4/Composition \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Content-Type: application/fhir+json" \ -H "Accept: application/fhir+json" \ -d '{ "resourceType": "Composition", "status": "preliminary", "type": { "coding": [ { "system": "http://loinc.org", "code": "11488-4", "display": "Consultation note" } ] }, "subject": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2" }, "author": [ { "reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1" } ], "section": [ { "title": "Chief complaint", "code": { "coding": [ { "system": "http://loinc.org", "code": "10154-3" } ] }, "text": { "status": "generated", "div": "
Cefalea de 3 días.
" } }, { "title": "Physical findings", "code": { "coding": [ { "system": "http://loinc.org", "code": "29545-1" } ] }, "text": { "status": "generated", "div": "
TA 120/80. Sin focalización.
" } }, { "title": "Plan of care", "code": { "coding": [ { "system": "http://loinc.org", "code": "18776-5" } ] }, "text": { "status": "generated", "div": "
Analgésico y control en 1 semana.
" } } ] }' ``` ```typescript const section = (code: string, title: string, text: string) => ({ title, code: { coding: [{ system: 'http://loinc.org', code }] }, text: { status: 'generated', div: `
${text}
`, }, }); const composition = { resourceType: 'Composition', status: 'preliminary', // required — preliminary|final|amended|entered-in-error type: { coding: [{ system: 'http://loinc.org', code: '11488-4', display: 'Consultation note' }] }, subject: { reference: `Patient/${process.env.PATIENT_ID}` }, // required author: [{ reference: `Practitioner/${process.env.PRACTITIONER_ID}` }], // required section: [ section('10154-3', 'Chief complaint', 'Cefalea de 3 días.'), section('29545-1', 'Physical findings', 'TA 120/80. Sin focalización.'), section('18776-5', 'Plan of care', 'Analgésico y control en 1 semana.'), ], }; const resp = await fetch('https://api.huli.ai/fhir/R4/Composition', { method: 'POST', headers: { Authorization: `Bearer ${process.env.HULI_TOKEN}`, 'Content-Type': 'application/fhir+json', Accept: 'application/fhir+json', }, body: JSON.stringify(composition), }); if (resp.status === 201) { const created = (await resp.json()) as { id: string }; // The weak ETag is your concurrency token for the amend in step 4. console.log('created', created.id, resp.headers.get('ETag')); } else { const outcome = (await resp.json()) as { issue: { diagnostics: string }[] }; console.log(resp.status, outcome.issue[0].diagnostics); } ``` ```python import os import requests def section(code: str, title: str, text: str) -> dict: return { "title": title, "code": {"coding": [{"system": "http://loinc.org", "code": code}]}, "text": { "status": "generated", "div": f'
{text}
', }, } composition = { "resourceType": "Composition", "status": "preliminary", # required — preliminary|final|amended|entered-in-error "type": {"coding": [{"system": "http://loinc.org", "code": "11488-4", "display": "Consultation note"}]}, "subject": {"reference": f"Patient/{os.environ['PATIENT_ID']}"}, # required "author": [{"reference": f"Practitioner/{os.environ['PRACTITIONER_ID']}"}], # required "section": [ section("10154-3", "Chief complaint", "Cefalea de 3 días."), section("29545-1", "Physical findings", "TA 120/80. Sin focalización."), section("18776-5", "Plan of care", "Analgésico y control en 1 semana."), ], } resp = requests.post( "https://api.huli.ai/fhir/R4/Composition", headers={ "Authorization": f"Bearer {os.environ['HULI_TOKEN']}", "Content-Type": "application/fhir+json", "Accept": "application/fhir+json", }, json=composition, timeout=30, ) if resp.status_code == 201: # resp.headers["ETag"] is the concurrency token for the amend in step 4. print("created", resp.json()["id"], resp.headers.get("ETag")) else: print(resp.status_code, resp.json()["issue"][0]["diagnostics"]) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WriteNote { public static void main(String[] args) throws Exception { // status/subject/author are required; each section's LOINC code routes its // narrative to a clinical field. Hand-built JSON keeps this dependency-free; // a real client would use a JSON library. String body = "{" + "\"resourceType\":\"Composition\"," + "\"status\":\"preliminary\"," + "\"type\":{\"coding\":[{\"system\":\"http://loinc.org\",\"code\":\"11488-4\",\"display\":\"Consultation note\"}]}," + "\"subject\":{\"reference\":\"Patient/" + System.getenv("PATIENT_ID") + "\"}," + "\"author\":[{\"reference\":\"Practitioner/" + System.getenv("PRACTITIONER_ID") + "\"}]," + "\"section\":[" + "{\"code\":{\"coding\":[{\"system\":\"http://loinc.org\",\"code\":\"10154-3\"}]}," + "\"text\":{\"status\":\"generated\",\"div\":\"
Cefalea de 3 días.
\"}}" + "]}"; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.huli.ai/fhir/R4/Composition")) .header("Authorization", "Bearer " + System.getenv("HULI_TOKEN")) .header("Content-Type", "application/fhir+json") .header("Accept", "application/fhir+json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); // Capture the ETag for the conditional amend in step 4. System.out.println(response.statusCode()); System.out.println(response.headers().firstValue("ETag").orElse("")); System.out.println(response.body()); } } ``` ```go package main import ( "bytes" "fmt" "io" "net/http" "os" ) func main() { // status/subject/author are required; each section's LOINC code routes its // narrative to a clinical field (10154-3 chief complaint, 29545-1 findings, // 18776-5 plan). The diagnosis section is read-only on write. body := []byte(`{ "resourceType": "Composition", "status": "preliminary", "type": {"coding": [{"system": "http://loinc.org", "code": "11488-4", "display": "Consultation note"}]}, "subject": {"reference": "Patient/` + os.Getenv("PATIENT_ID") + `"}, "author": [{"reference": "Practitioner/` + os.Getenv("PRACTITIONER_ID") + `"}], "section": [ { "code": {"coding": [{"system": "http://loinc.org", "code": "10154-3"}]}, "text": {"status": "generated", "div": "
Cefalea de 3 días.
"} } ] }`) req, err := http.NewRequest(http.MethodPost, "https://api.huli.ai/fhir/R4/Composition", bytes.NewReader(body)) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("HULI_TOKEN")) req.Header.Set("Content-Type", "application/fhir+json") req.Header.Set("Accept", "application/fhir+json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, err := io.ReadAll(resp.Body) if err != nil { panic(err) } // resp.Header.Get("ETag") is the concurrency token for the amend in step 4. fmt.Printf("%d %s\n%s\n", resp.StatusCode, resp.Header.Get("ETag"), out) } ``` ::: A `201 Created` returns the stored `Composition`. The response carries a weak `ETag` header — `W/""`, derived from the visit's last-modified time — and the same value in `meta.versionId`. Keep it: it is the token the amend in step 4 conditions on. ```json { "resourceType": "Composition", "id": "01965e2a-8c4d-7000-9060-0000000000e9", "meta": { "versionId": "1769472764000000000", "lastUpdated": "2026-06-26T10:12:44.000-06:00", "profile": ["https://fhir.huli.ai/r4/StructureDefinition/HuliComposition"] }, "status": "preliminary", "type": { "coding": [{ "system": "http://loinc.org", "code": "11488-4", "display": "Consultation note" }] }, "subject": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2", "type": "Patient" }, "encounter": { "reference": "Encounter/01965e2a-8c4d-7000-9060-0000000000e9", "type": "Encounter" }, "author": [ { "reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1", "type": "Practitioner" } ], "title": "Clinical note", "date": "2026-06-26T10:12:44.000-06:00", "section": [ { "title": "Chief complaint", "code": { "coding": [ { "system": "http://loinc.org", "code": "10154-3", "display": "Chief complaint" } ] }, "text": { "status": "generated", "div": "
Cefalea de 3 días.
" } } ] } ``` Note the `encounter` reference: it resolves to the **same id** as the Composition — proof the two are projections of one visit row. ### 3. Read the note and capture its version A single read returns the current note and stamps the weak `ETag` you condition the amend on. Search instead with `patient`, `date`, `type`, or `_id` to list a patient's notes. ```bash curl -i "https://api.huli.ai/fhir/R4/Composition/01965e2a-8c4d-7000-9060-0000000000e9" \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Accept: application/fhir+json" ``` Read the `ETag` response header — e.g. `ETag: W/"1769472764000000000"`. The `type` search parameter only matches the consultation-note code (`11488-4` or `http://loinc.org|11488-4`); any other value returns an empty bundle, since every Huli `Composition` is a consultation note. ### 4. Amend the note with If-Match A `Composition` `PUT` is a **full replace** of the narrative: send the complete section set you want stored, not just the changed one — a section you omit is cleared. Set the `If-Match` header to the `ETag` you captured so a concurrent edit cannot be lost. The server re-checks the version against the locked row inside the update transaction, so the guard holds even under a race. :::CodeGroup ```bash curl -i -X PUT https://api.huli.ai/fhir/R4/Composition/01965e2a-8c4d-7000-9060-0000000000e9 \ -H "Authorization: Bearer $HULI_TOKEN" \ -H "Content-Type: application/fhir+json" \ -H "Accept: application/fhir+json" \ -H 'If-Match: W/"1769472764000000000"' \ -d '{ "resourceType": "Composition", "status": "final", "subject": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2" }, "author": [ { "reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1" } ], "section": [ { "code": { "coding": [ { "system": "http://loinc.org", "code": "10154-3" } ] }, "text": { "status": "generated", "div": "
Cefalea de 3 días, ya resuelta.
" } }, { "code": { "coding": [ { "system": "http://loinc.org", "code": "18776-5" } ] }, "text": { "status": "generated", "div": "
Alta. Sin necesidad de control.
" } } ] }' ``` ```typescript const etag = 'W/"1769472764000000000"'; // captured from the read in step 3 const resp = await fetch( 'https://api.huli.ai/fhir/R4/Composition/01965e2a-8c4d-7000-9060-0000000000e9', { method: 'PUT', headers: { Authorization: `Bearer ${process.env.HULI_TOKEN}`, 'Content-Type': 'application/fhir+json', Accept: 'application/fhir+json', 'If-Match': etag, }, body: JSON.stringify({ resourceType: 'Composition', status: 'final', // completes the visit subject: { reference: `Patient/${process.env.PATIENT_ID}` }, author: [{ reference: `Practitioner/${process.env.PRACTITIONER_ID}` }], section: [ section('10154-3', 'Chief complaint', 'Cefalea de 3 días, ya resuelta.'), section('18776-5', 'Plan of care', 'Alta. Sin necesidad de control.'), ], }), }, ); if (resp.status === 200) { console.log('amended', resp.headers.get('ETag')); // a new version } else if (resp.status === 409) { // HPB-00103 — someone amended it since you read it. Re-read step 3, reapply, retry. console.log('stale — re-read and retry'); } ``` ::: A `200 OK` returns the updated `Composition` with a **new** `versionId`/`ETag`. Setting `status` to `final` completes the underlying visit. ## What to verify - The create is `201`. The response `resourceType` is `Composition` with a server-assigned `id`, and the `encounter` reference resolves to that same id. - The response carries an `ETag` header and a matching `meta.versionId`. - Your `section[]` round-trips: each LOINC code you sent comes back, with the narrative inside `text.div`. - The amend is `200` and its `ETag`/`versionId` differs from the one you sent in `If-Match`. - A re-read after an `entered-in-error` or `final` write reflects the new `status`. ## What can go wrong All errors return a FHIR `OperationOutcome` — `{severity, code, diagnostics}`, no `details` object. Branch on the HTTP status and `issue[0].code`; the Huli code is the prefix of `issue[0].diagnostics`, split on `": "`. `HPB-00103` — **version conflict.** Your `If-Match` did not match the stored version: the note changed between your read and your `PUT`. Re-read it (step 3), reapply your edit onto the fresh copy, and retry with the new `ETag`. Do not strip `If-Match` to force the write through — that is the exact lost-update you are guarding against. `HPB-00104` — **insufficient scope.** The token lacks system/Composition.cru (or `.rs` for a read). Because the **Clinical information** card is BAA-gated, confirm the key was minted with a BAA attestation in Practice Settings. `HPB-00117` — **Composition not found.** The id does not name a visit in your organization (or it was never created). Confirm the id and the token's organization. `HPB-00101` — **validation error.** A required field is missing (`status`, `subject`, or `author` on create) or the JSON is malformed. The `subject` must reference a patient, and `author[0]` a practitioner, that resolve in your organization. **Illegal status transition.** A note already `final` cannot be set to `entered-in-error` through this surface — a finalized visit cannot be voided, mirroring the encounter model. Void a `preliminary` note instead; for a finalized one, record a correcting note. A representative `409` body: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "conflict", "diagnostics": "HPB-00103: Version conflict" } ] } ``` ## Next recipes - **[Creating a clinical encounter](/v1/recipes/creating-an-encounter)** — set the visit envelope and diagnoses (the `Encounter` projection of this same row). - **[Uploading a document](/v1/recipes/uploading-a-document)** — attach a lab PDF or scanned file to the patient with `DocumentReference.$upload`, under the same Clinical information card. - **[Fetching a patient's full record](/v1/recipes/fetching-a-patient-record)** — pull every Composition, Encounter, Observation, and document for a patient in one `$everything` Bundle. ======================================================================== # Reference # URL: https://developers.huli.ai/v1/reference # OpenAPI specification, FHIR CapabilityStatement, and generated endpoint pages for the Huli Public API v1. # Reference Machine-readable contracts for the Huli Public API v1. ## OpenAPI specification The OpenAPI 3.0.3 specification covers the FHIR R4 endpoints (see the [API reference](/v1/api)). **Live spec:** ```bash curl https://api.huli.ai/openapi.json ``` The published `openapi.json` is the source of truth for the API contract — the endpoint reference under [API](/v1/api) is generated from it. The spec includes `x-huli-scope` extensions on each operation documenting the required scope. ## FHIR CapabilityStatement The live CapabilityStatement reflects what the server currently supports. It is generated from the same source as the OpenAPI spec — they do not diverge. ```bash curl https://api.huli.ai/fhir/R4/metadata \ -H "Accept: application/fhir+json" ``` This endpoint does not require authentication. It is safe to fetch on startup to validate that the server supports the operations your integration needs. ## Generated endpoint pages The pages under [`/v1/api/`](/v1/api) are auto-generated from the OpenAPI spec and the server FHIR CapabilityStatement at build time. Each resource page merges the endpoint reference (request/response schemas, required scopes, error codes, code samples) with the FHIR narrative (supported interactions, search parameters, custom extensions, and LATAM-specific identifiers). Browse them from the [API reference index](/v1/api), and use the R4 / R5 switch there to pick a FHIR release — see [Choosing R4 vs R5](/v1/api/fhir-versions). ## SMART discovery ```bash curl https://api.huli.ai/fhir/.well-known/smart-configuration ``` Returns the `token_endpoint`, `jwks_uri`, `scopes_supported`, and `grant_types_supported` needed to configure any SMART-compatible client library. ## JWKS The server's public key for verifying issued access tokens: ```bash curl https://api.huli.ai/fhir/.well-known/jwks.json ``` ======================================================================== # Scopes # URL: https://developers.huli.ai/v1/scopes # SMART on FHIR scope reference for the Huli Public API v1. # Scopes This page is hand-curated from the OpenAPI spec. Treat it as the authoritative reference — it matches the scopes advertised in the `/fhir/.well-known/smart-configuration` endpoint. Scopes follow the SMART on FHIR `system/.` format. Request only the scopes your integration requires. ## Permission letters - **r** — read a single resource by ID - **s** — search (query with parameters, paginated Bundle response) - **c** — create a new resource - **u** — update an existing resource - **d** — delete a resource ## Scope reference ### Patient #### system/Patient.rs **Read and search patients** Allows reading individual Patient records by ID and searching the patient list. Does not permit creating or modifying patient data. Permissions granted: **r** — read a single resource by ID, **s** — search (query with parameters, paginated Bundle response) #### system/Patient.cru **Create, read, and update patients** Allows creating new Patient records, reading existing ones, and updating demographic or contact information. Includes all operations of `system/Patient.rs`. Permissions granted: **c** — create a new resource, **r** — read a single resource by ID, **u** — update an existing resource ### Appointment #### system/Appointment.rs **Read and search appointments** Allows reading individual Appointment resources and searching the appointment list by patient, practitioner, date, or status. Permissions granted: **r** — read a single resource by ID, **s** — search (query with parameters, paginated Bundle response) #### system/Appointment.cru **Create, read, and update appointments** Allows booking new appointments, reading existing ones, and updating appointment status (e.g. cancellation). Includes all operations of `system/Appointment.rs`. Permissions granted: **c** — create a new resource, **r** — read a single resource by ID, **u** — update an existing resource ### Encounter #### system/Encounter.rs **Read and search encounters** Allows reading individual Encounter records (clinical consultations) and searching by patient, date, status, or class. Permissions granted: **r** — read a single resource by ID, **s** — search (query with parameters, paginated Bundle response) #### system/Encounter.cru **Create, read, and update encounters** Allows creating new Encounter records, reading existing ones, and updating encounter status or details. Includes all operations of `system/Encounter.rs`. Permissions granted: **c** — create a new resource, **r** — read a single resource by ID, **u** — update an existing resource ### Observation #### system/Observation.rs **Read and search observations** Allows reading individual Observation resources (vital signs, lab results, exam findings) and searching by patient, encounter, LOINC code, date, status, or category. Permissions granted: **r** — read a single resource by ID, **s** — search (query with parameters, paginated Bundle response) #### system/Observation.cru **Create, read, and update observations** Allows recording new Observation values, reading existing ones, and amending previously recorded observations. LOINC codes are validated on write. Permissions granted: **c** — create a new resource, **r** — read a single resource by ID, **u** — update an existing resource ### MedicationRequest #### system/MedicationRequest.rs **Read and search medication requests** Allows reading individual MedicationRequest resources (prescription orders) and searching by patient or encounter. Permissions granted: **r** — read a single resource by ID, **s** — search (query with parameters, paginated Bundle response) #### system/MedicationRequest.c **Create medication requests** Allows creating MedicationRequest resources (each auto-links to a draft prescription). Permissions granted: **c** — create a new resource #### system/MedicationRequest.cru **Create, read, and update medication requests** Allows creating, reading, and updating MedicationRequest resources. Updates are read-then-merge and preserve app-only fields; a signed or cancelled prescription cannot be modified. Permissions granted: **c** — create a new resource, **r** — read a single resource by ID, **u** — update an existing resource ### ServiceRequest #### system/ServiceRequest.rs **Read and search service requests** Allows reading individual ServiceRequest resources (study / procedure orders) and searching by patient or encounter. Permissions granted: **r** — read a single resource by ID, **s** — search (query with parameters, paginated Bundle response) #### system/ServiceRequest.c **Create service requests** Allows creating ServiceRequest resources (a single-item order). Permissions granted: **c** — create a new resource #### system/ServiceRequest.cru **Create, read, and update service requests** Allows creating, reading, and updating ServiceRequest resources. Updates are read-then-merge on draft orders and preserve app-only fields; a signed or cancelled order cannot be modified, and multi-item orders are read-only. Permissions granted: **c** — create a new resource, **r** — read a single resource by ID, **u** — update an existing resource ### Composition #### system/Composition.rs **Read and search clinical notes** Allows reading individual Composition resources (the clinical-note projection of an encounter) and searching by patient, date, or type. Permissions granted: **r** — read a single resource by ID, **s** — search (query with parameters, paginated Bundle response) #### system/Composition.cru **Create, read, and update clinical notes** Allows creating Composition resources, reading existing ones, and updating the clinical narrative (with optimistic concurrency via If-Match). Includes all operations of `system/Composition.rs`. Permissions granted: **c** — create a new resource, **r** — read a single resource by ID, **u** — update an existing resource ### DocumentReference #### system/DocumentReference.rs **Read and search document references** Allows reading individual DocumentReference resources (lab results, imaging, scanned files; the binary is served via a 30-minute signed URL) and searching by patient, category, type, or date. Permissions granted: **r** — read a single resource by ID, **s** — search (query with parameters, paginated Bundle response) #### system/DocumentReference.cru **Create, read, and update document references** Allows uploading document binaries ($upload — base64 inline or multipart, 25 MB ceiling), reading existing ones, and updating their metadata. Includes all operations of `system/DocumentReference.rs`. Permissions granted: **c** — create a new resource, **r** — read a single resource by ID, **u** — update an existing resource ### Practitioner #### system/Practitioner.rs **Read practitioners** Allows reading individual Practitioner resources (healthcare providers). Practitioner data is read-only in the public API. Permissions granted: **r** — read a single resource by ID, **s** — search (query with parameters, paginated Bundle response) ### Organization #### system/Organization.rs **Read organization** Allows reading the Organization resource that corresponds to your API key organization. Organization data is read-only in the public API. Permissions granted: **r** — read a single resource by ID, **s** — search (query with parameters, paginated Bundle response) ### Subscription #### system/Subscription.rs **Read and search webhook subscriptions** Allows reading a webhook Subscription by ID and listing the subscriptions registered for your API key organization. Permissions granted: **r** — read a single resource by ID, **s** — search (query with parameters, paginated Bundle response) #### system/Subscription.crud **Manage webhook subscriptions** Allows creating, reading, updating, and deleting outbound-webhook Subscriptions. Requires a signed BAA on the API key. Includes all operations of `system/Subscription.rs`. Permissions granted: **c** — create a new resource, **r** — read a single resource by ID, **u** — update an existing resource, **d** — delete a resource ## Requesting scopes Include the `scope` parameter in your token request. Separate multiple scopes with spaces: ```bash curl -X POST https://api.huli.ai/auth/token \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=${CLIENT_ASSERTION}" \ -d "scope=system/Patient.rs system/Appointment.rs" ``` If `scope` is omitted, all scopes registered for the API key are granted. The issued token includes the granted scopes in the `scope` response field. ======================================================================== # Start building # URL: https://developers.huli.ai/v1/start # Run a real FHIR search in your browser with no key, and learn to read the response — success and failure — with a guided walkthrough. # Start building The Huli Public API is a FHIR REST API — standard healthcare resources like `Patient`, `Appointment`, and `Observation` over plain HTTPS. Run the patient search below right now — no key, it uses sample data baked into the page — and the response comes back with a walkthrough attached. Flip on **Simulate an error** to see a failure explained the same way. ## Try it & read the response Every error is a FHIR `OperationOutcome` resource carrying a stable `HPB-` code, never an ad-hoc error blob. [Errors](/v1/errors) lists every code, and the [debugging recipe](/v1/recipes/debugging-a-failed-fhir-search) walks a failing search end-to-end. When you're ready for live responses, the playground runs real requests from your browser against a sandbox organization.