---
title: Creating a clinical encounter
description: 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.
nav: Recipes
order: 35
version: v1
source: handwritten
updated: 2026-06-26
---

# 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:
<Scope>system/Encounter.cru</Scope> for the write, <Scope>system/Patient.rs</Scope> so the
subject resolves, and <Scope>system/Practitioner.rs</Scope> 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:
  - <Scope name="system/Encounter.cru" /> — create `Encounter` (`.cru` also grants read +
    search).
  - <Scope name="system/Patient.rs" /> — read + search `Patient`; the encounter's `subject`
    must reference a patient that resolves in your organization.
  - <Scope name="system/Practitioner.rs" /> — 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.

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

## 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="<paste your bearer token here>"
export PATIENT_ID="01965e2a-8c4d-7000-9001-0000000000a2"
```

Resolve a real patient id in your sandbox instead of the illustrative one above:

<RunnableRequest method="GET" path="/fhir/R4/Patient?_count=1" recipe="creating-an-encounter" captures='[{"name":"patientId","path":"entry.0.resource.id"}]' />

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

<Endpoint method="GET" path="/fhir/R4/Practitioner?name=Fern%C3%A1ndez" />

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/<entry.resource.id>.
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/<entry.resource.id>.
practitioner = bundle["entry"][0]["resource"]
print(f"Practitioner/{practitioner['id']}")
```

:::

<RunnableRequest method="GET" path="/fhir/R4/Practitioner?_count=1" recipe="creating-an-encounter" captures='[{"name":"practitionerId","path":"entry.0.resource.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`.

<Endpoint method="POST" path="/fhir/R4/Encounter" />

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

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

:::

<RunnableRequest method="POST" path="/fhir/R4/Encounter" recipe="creating-an-encounter" body='{"resourceType":"Encounter","status":"finished","class":{"system":"http://terminology.hl7.org/CodeSystem/v3-ActCode","code":"AMB","display":"ambulatory"},"subject":{"reference":"Patient/{{patientId}}"},"participant":[{"individual":{"reference":"Practitioner/{{practitionerId}}"}}]}' captures='[{"name":"encounterId","path":"id"}]' />

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

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/<uuid>` — must resolve in the organization (else `422`).                                                          |
| `participant[0].individual.reference` | **required** | yes              | `Practitioner/<uuid>` — 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/<uuid>` 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.                                                                                |

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

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

## What to verify

- HTTP status is `201`. <StatusBadge code="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.

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

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

<StatusBadge code="422" /> **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.

<StatusBadge code="409" /> **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.
