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, 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 for provisioning and POST /auth/token for the token exchange — this recipe assumes you already hold one.
  • These three scopes on that token:
    • system/Encounter.cru — create Encounter (.cru also grants read +search).
    • system/Patient.rs — read + search Patient; the encounter's subjectmust reference a patient that resolves in your organization.
    • system/Practitioner.rs — read + search Practitioner andPractitionerRole to discover the participant.
  • The id of the patient the visit is for. Resolve it with a Patient search if you only hold a name or identifier.
  • curl, or Node, Python, Java, or Go if you prefer a language client.

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

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:

GET/fhir/R4/Patient?_count=1

Set your sandbox token above to run this request.

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.

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

curl "https://api.huli.ai/fhir/R4/Practitioner?name=Fern%C3%A1ndez&_count=20" \
  -H "Authorization: Bearer $HULI_TOKEN" \
  -H "Accept: application/fhir+json"
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}`);
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']}")
GET/fhir/R4/Practitioner?_count=1

Set your sandbox token above to run this request.

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.

POST/fhir/R4/Encounter
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."
      }
    ]
  }'
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);
}
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"])
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)
	}
}
POST/fhir/R4/Encounter

Set your sandbox token above to run this request.

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.

FieldReq?Honored on writeNotes
statusrequiredyesFHIR status; send finished (maps to Huli completed) or in-progress.
subject.referencerequiredyesPatient/<uuid> — must resolve in the organization (else 422).
participant[0].individual.referencerequiredyesPractitioner/<uuid> — at least one participant is required; the first individual is read.
class.codeoptionalyesOne of AMB/EMER/IMP/VR; maps to ambulatory/emergency/inpatient/virtual. Absent defaults to AMB.
class.system / displayoptionalignoredRe-emitted from the code mapping on read.
appointment[0].referenceoptionalyesAppointment/<uuid> linking the visit to its scheduling appointment.
period.startoptionalyesVisit start.
period.endoptionalyesVisit end; omit for an in-progress visit.
reasonCode[0].textoptionalyesFree-text reason for the visit. Only the first entry's text is read.
contained[] ConditionoptionalyesICD-10 diagnosis: code.coding[0] (system/code/display) + code.text are read into the encounter's diagnoses.
contained[] ClinicalImpressionoptionalyessummary is read as the subjective note.
participant[].type / periodoptionalignoredOnly individual is consumed on write.
diagnosis[]optionalignored on writeBuilt on read from the contained Conditions — send diagnoses as contained Conditions, not as diagnosis[] references.
serviceProvideroptionalignoredThe server stamps the token's organization.

What to verify

  • HTTP status is 201. 201 Created
  • 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.

400 Bad Request 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].coderequired and a diagnostics of "at least one participant (practitioner) is required". Discover the practitioner in step 2 and build the participant before posting.

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

422 Unprocessable Entity 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.

409 Conflict 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:

{
  "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 — record the visit's vitals and lab results as Observation resources linked to this encounter.
  • 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.