---
title: Run your first authenticated Patient search
description: Send a FHIR R4 Patient name search with an admin bearer token — request, searchset Bundle, and the four errors you'll hit first.
nav: Recipes
order: 10
version: v1
source: handwritten
updated: 2026-08-12
---

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

<Callout variant="info">

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

</Callout>

## 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 <Scope>system/Patient.rs</Scope> scope on that token. `rs` grants read plus
  search, which is what this request uses. <Scope>system/Patient.cru</Scope> also works.
- `curl`, or one of Node 18+ / Python 3.9+ / JDK 11+ / Go 1.22+ if you prefer a language client.

<Callout variant="info">
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.
</Callout>

## 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="<paste your admin bearer token here>"
```

Confirm it is set:

```bash
echo $HULI_API_KEY
```

### 2. Run the search

<Endpoint method="GET" path="/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`).

:::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<String> 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)
	}
}
```

:::

<RunnableRequest method="GET" path="/fhir/R4/Patient?name=Fern%C3%A1ndez&_count=20" recipe="getting-started-patient-search" captures='[{"name":"patientId","path":"entry.0.resource.id"}]' />

<Callout variant="note">
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.
</Callout>

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`. <StatusBadge code="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:

<StatusBadge code="401" /> `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.

<StatusBadge code="403" /> `HPB-00104` — insufficient scope. The token authenticated but
lacks <Scope>system/Patient.rs</Scope>. Re-mint it in Practice Settings with `Patient.rs`
(or `Patient.cru`) selected.

<StatusBadge code="400" /> `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.

<StatusBadge code="429" /> `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"
    }
  ]
}
```

<Callout variant="note">
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.
</Callout>

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