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.
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.
rsgrants 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.
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
export HULI_API_KEY="<paste your admin bearer token here>"
Confirm it is set:
echo $HULI_API_KEY
2. Run the search
/fhir/R4/Patient?name=Fern%C3%A1ndezThe name parameter does a case- and accent-insensitive prefix match across the patient's name parts. URL-encode the accent (á → %C3%A1).
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"
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());
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())
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<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
}
}
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)
}
}
/fhir/R4/Patient?name=Fern%C3%A1ndez&_count=20Set your sandbox token above to run this request.
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.
{
"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 thesecond-lastnameextension on the samenameelement — read both to reconstruct the full apellido. genderis the FHIR value (male/female/other), mapped from Huli's internalM/F/I. Match on the FHIR token here, not the Huli letter.
What to verify
- HTTP status is
200. 200 OK resourceTypeisBundleandtypeissearchset.totalmatches the number ofentryitems you expected forFernández.- Each
entry.resource.resourceTypeisPatient. - For a two-match query like this one there is no
nextlink, 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 <token> with a single space, and that $HULI_API_KEY is actually exported in this shell.
HPB-00104 — insufficient scope. The token authenticated butlacks system/Patient.rs. Re-mint it in Practice Settings with Patient.rs (or Patient.cru) selected.
HPB-00101 — validation error. A search parameter ismalformed — 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 requestbudget. Read the Retry-After response header and back off for that many seconds before retrying.
A representative 403 body:
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "forbidden",
"diagnostics": "HPB-00104: Insufficient scope"
}
]
}
Next recipes
- Paginate a large patient list — follow the
link[rel=next]cursor to walk every page of asearchset. - Search NOM-024 LATAM identifiers — query the
identifierparameter 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.rstosystem/Patient.cruandPOST/PUTpatient records.