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. 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.

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
GET/fhir/R4/Patient?name=Fern%C3%A1ndez

The 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)
	}
}
GET/fhir/R4/Patient?name=Fern%C3%A1ndez&_count=20

Set 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 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. 200 OK
  • 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:

401 Unauthorized 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.

403 Forbidden 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.

400 Bad Request 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.

429 Too Many Requests 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:

{
  "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 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.