---
title: Writing and amending a clinical note
description: Create a FHIR R4 Composition — the LOINC-sectioned clinical narrative of a visit — then amend it safely with If-Match optimistic concurrency. Uses the BAA-gated system/Composition.cru scope.
nav: Recipes
order: 36
version: v1
source: handwritten
updated: 2026-06-26
---

# Writing and amending a clinical note

Record a visit's clinical narrative as a FHIR R4 `Composition`, read it back, then amend it
without clobbering a concurrent edit. A `Composition` is the **clinical-note projection of an
encounter**: the same stored visit the `Encounter` resource renders, exposed as LOINC-coded
narrative sections (chief complaint, history, findings, assessment, plan) instead of the visit
envelope. One scope carries the flow: <Scope>system/Composition.cru</Scope> — create, read, and
update, including the conditional `PUT` that makes amendments safe.

The part most first writes get wrong is the amend. Two clients that both read a note and both
`PUT` it will silently lose one edit unless the second write is rejected. The `If-Match` header
and the weak `ETag` the API returns on every read close that window — send the version you read,
and a stale write fails loudly with a `409` instead of overwriting fresher data.

## Audience

You integrate an EHR or a scribe tool that writes clinical narrative back into HuliPractice. You
have [created an Encounter](/v1/recipes/creating-an-encounter) before, you read a `Bundle` without
a viewer, and you know what a FHIR reference is. You want to take a note from draft to a stored,
amendable `Composition`.

## 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.
- The <Scope>system/Composition.cru</Scope> scope on that token. It sits under the **Medical
  records** card, which is **BAA-gated** — the clinic admin must attest to a Business Associate
  Agreement before a key carrying it can be minted. `.cru` grants create, read, and update;
  <Scope>system/Composition.rs</Scope> alone grants read + search.
- The `id` of the patient the note is for, and the `id` of the authoring practitioner. Resolve
  them with [a Patient search](/v1/recipes/getting-started-patient-search) and a
  [Practitioner search](/v1/recipes/creating-an-encounter) if you only hold names.
- `curl`, or Node, Python, Java, or Go if you prefer a language client.

<Callout variant="info">
A `Composition` and an `Encounter` are two projections of **one** stored visit. Creating a
`Composition` creates a new visit row; the same row is readable as an `Encounter`. The two own
different fields — `Composition` maps the narrative sections, `Encounter` maps the visit
envelope and the diagnoses — so a `Composition` write preserves whatever the `Encounter` surface
set, and vice versa. There is **no `_history`, no `vread`, and no `DELETE`** on `Composition`;
correct a note's lifecycle through `status` on a `PUT`.
</Callout>

## End state

You hold a `201 Created` whose body is the stored `Composition` — with a server-assigned `id`,
the patient as `subject`, the practitioner as `author`, the LOINC type `11488-4` (consultation
note), and your narrative under `section[]`. You then read it back, capture its `ETag`, and land
a `200 OK` amendment guarded by `If-Match`.

## Steps

### 1. Export the token and the references

```bash
export HULI_TOKEN="<paste your bearer token here>"
export PATIENT_ID="01965e2a-8c4d-7000-9001-0000000000a2"
export PRACTITIONER_ID="01965e2a-8c4d-7000-9001-0000000000c1"
```

### 2. Create the note

`POST` a `Composition` with `status`, the patient `subject`, the practitioner `author`, and one
LOINC-coded `section` per narrative field. The section code routes the text back to its field, so
use the exact LOINC codes below — an unrecognized code is ignored on write.

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

| Section                    | LOINC code | Maps to               |
| -------------------------- | ---------- | --------------------- |
| Chief complaint            | `10154-3`  | reason for the visit  |
| History of present illness | `10164-2`  | subjective history    |
| Physical findings          | `29545-1`  | objective findings    |
| Assessment                 | `51848-0`  | diagnostic impression |
| Plan of care               | `18776-5`  | care-plan narrative   |

<Callout variant="info">
`status` is the FHIR Composition document status and it drives the underlying visit state:
`preliminary` → an in-progress visit, `final` → a completed visit, `entered-in-error` → a voided
visit. `amended` keeps the visit completed. The **diagnosis** section (LOINC `29308-4`) is
**read-only** here — it is emitted on read but ignored on write; set diagnoses through the
[Encounter](/v1/recipes/creating-an-encounter) surface, and a `Composition` `PUT` preserves them.
</Callout>

:::CodeGroup

```bash
curl -i -X POST https://api.huli.ai/fhir/R4/Composition \
  -H "Authorization: Bearer $HULI_TOKEN" \
  -H "Content-Type: application/fhir+json" \
  -H "Accept: application/fhir+json" \
  -d '{
    "resourceType": "Composition",
    "status": "preliminary",
    "type": {
      "coding": [
        { "system": "http://loinc.org", "code": "11488-4", "display": "Consultation note" }
      ]
    },
    "subject": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2" },
    "author": [ { "reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1" } ],
    "section": [
      {
        "title": "Chief complaint",
        "code": { "coding": [ { "system": "http://loinc.org", "code": "10154-3" } ] },
        "text": {
          "status": "generated",
          "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Cefalea de 3 días.</div>"
        }
      },
      {
        "title": "Physical findings",
        "code": { "coding": [ { "system": "http://loinc.org", "code": "29545-1" } ] },
        "text": {
          "status": "generated",
          "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">TA 120/80. Sin focalización.</div>"
        }
      },
      {
        "title": "Plan of care",
        "code": { "coding": [ { "system": "http://loinc.org", "code": "18776-5" } ] },
        "text": {
          "status": "generated",
          "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Analgésico y control en 1 semana.</div>"
        }
      }
    ]
  }'
```

```typescript
const section = (code: string, title: string, text: string) => ({
  title,
  code: { coding: [{ system: 'http://loinc.org', code }] },
  text: {
    status: 'generated',
    div: `<div xmlns="http://www.w3.org/1999/xhtml">${text}</div>`,
  },
});

const composition = {
  resourceType: 'Composition',
  status: 'preliminary', // required — preliminary|final|amended|entered-in-error
  type: { coding: [{ system: 'http://loinc.org', code: '11488-4', display: 'Consultation note' }] },
  subject: { reference: `Patient/${process.env.PATIENT_ID}` }, // required
  author: [{ reference: `Practitioner/${process.env.PRACTITIONER_ID}` }], // required
  section: [
    section('10154-3', 'Chief complaint', 'Cefalea de 3 días.'),
    section('29545-1', 'Physical findings', 'TA 120/80. Sin focalización.'),
    section('18776-5', 'Plan of care', 'Analgésico y control en 1 semana.'),
  ],
};

const resp = await fetch('https://api.huli.ai/fhir/R4/Composition', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.HULI_TOKEN}`,
    'Content-Type': 'application/fhir+json',
    Accept: 'application/fhir+json',
  },
  body: JSON.stringify(composition),
});

if (resp.status === 201) {
  const created = (await resp.json()) as { id: string };
  // The weak ETag is your concurrency token for the amend in step 4.
  console.log('created', created.id, resp.headers.get('ETag'));
} else {
  const outcome = (await resp.json()) as { issue: { diagnostics: string }[] };
  console.log(resp.status, outcome.issue[0].diagnostics);
}
```

```python
import os
import requests


def section(code: str, title: str, text: str) -> dict:
    return {
        "title": title,
        "code": {"coding": [{"system": "http://loinc.org", "code": code}]},
        "text": {
            "status": "generated",
            "div": f'<div xmlns="http://www.w3.org/1999/xhtml">{text}</div>',
        },
    }


composition = {
    "resourceType": "Composition",
    "status": "preliminary",  # required — preliminary|final|amended|entered-in-error
    "type": {"coding": [{"system": "http://loinc.org", "code": "11488-4", "display": "Consultation note"}]},
    "subject": {"reference": f"Patient/{os.environ['PATIENT_ID']}"},  # required
    "author": [{"reference": f"Practitioner/{os.environ['PRACTITIONER_ID']}"}],  # required
    "section": [
        section("10154-3", "Chief complaint", "Cefalea de 3 días."),
        section("29545-1", "Physical findings", "TA 120/80. Sin focalización."),
        section("18776-5", "Plan of care", "Analgésico y control en 1 semana."),
    ],
}

resp = requests.post(
    "https://api.huli.ai/fhir/R4/Composition",
    headers={
        "Authorization": f"Bearer {os.environ['HULI_TOKEN']}",
        "Content-Type": "application/fhir+json",
        "Accept": "application/fhir+json",
    },
    json=composition,
    timeout=30,
)

if resp.status_code == 201:
    # resp.headers["ETag"] is the concurrency token for the amend in step 4.
    print("created", resp.json()["id"], resp.headers.get("ETag"))
else:
    print(resp.status_code, resp.json()["issue"][0]["diagnostics"])
```

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class WriteNote {
    public static void main(String[] args) throws Exception {
        // status/subject/author are required; each section's LOINC code routes its
        // narrative to a clinical field. Hand-built JSON keeps this dependency-free;
        // a real client would use a JSON library.
        String body = "{"
            + "\"resourceType\":\"Composition\","
            + "\"status\":\"preliminary\","
            + "\"type\":{\"coding\":[{\"system\":\"http://loinc.org\",\"code\":\"11488-4\",\"display\":\"Consultation note\"}]},"
            + "\"subject\":{\"reference\":\"Patient/" + System.getenv("PATIENT_ID") + "\"},"
            + "\"author\":[{\"reference\":\"Practitioner/" + System.getenv("PRACTITIONER_ID") + "\"}],"
            + "\"section\":["
            + "{\"code\":{\"coding\":[{\"system\":\"http://loinc.org\",\"code\":\"10154-3\"}]},"
            + "\"text\":{\"status\":\"generated\",\"div\":\"<div xmlns=\\\"http://www.w3.org/1999/xhtml\\\">Cefalea de 3 días.</div>\"}}"
            + "]}";

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.huli.ai/fhir/R4/Composition"))
            .header("Authorization", "Bearer " + System.getenv("HULI_TOKEN"))
            .header("Content-Type", "application/fhir+json")
            .header("Accept", "application/fhir+json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        // Capture the ETag for the conditional amend in step 4.
        System.out.println(response.statusCode());
        System.out.println(response.headers().firstValue("ETag").orElse(""));
        System.out.println(response.body());
    }
}
```

```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	// status/subject/author are required; each section's LOINC code routes its
	// narrative to a clinical field (10154-3 chief complaint, 29545-1 findings,
	// 18776-5 plan). The diagnosis section is read-only on write.
	body := []byte(`{
		"resourceType": "Composition",
		"status": "preliminary",
		"type": {"coding": [{"system": "http://loinc.org", "code": "11488-4", "display": "Consultation note"}]},
		"subject": {"reference": "Patient/` + os.Getenv("PATIENT_ID") + `"},
		"author": [{"reference": "Practitioner/` + os.Getenv("PRACTITIONER_ID") + `"}],
		"section": [
			{
				"code": {"coding": [{"system": "http://loinc.org", "code": "10154-3"}]},
				"text": {"status": "generated", "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Cefalea de 3 días.</div>"}
			}
		]
	}`)

	req, err := http.NewRequest(http.MethodPost,
		"https://api.huli.ai/fhir/R4/Composition", 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)
	}
	// resp.Header.Get("ETag") is the concurrency token for the amend in step 4.
	fmt.Printf("%d %s\n%s\n", resp.StatusCode, resp.Header.Get("ETag"), out)
}
```

:::

A `201 Created` returns the stored `Composition`. The response carries a weak `ETag` header —
`W/"<versionId>"`, derived from the visit's last-modified time — and the same value in
`meta.versionId`. Keep it: it is the token the amend in step 4 conditions on.

```json
{
  "resourceType": "Composition",
  "id": "01965e2a-8c4d-7000-9060-0000000000e9",
  "meta": {
    "versionId": "1769472764000000000",
    "lastUpdated": "2026-06-26T10:12:44.000-06:00",
    "profile": ["https://fhir.huli.ai/r4/StructureDefinition/HuliComposition"]
  },
  "status": "preliminary",
  "type": {
    "coding": [{ "system": "http://loinc.org", "code": "11488-4", "display": "Consultation note" }]
  },
  "subject": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2", "type": "Patient" },
  "encounter": {
    "reference": "Encounter/01965e2a-8c4d-7000-9060-0000000000e9",
    "type": "Encounter"
  },
  "author": [
    { "reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1", "type": "Practitioner" }
  ],
  "title": "Clinical note",
  "date": "2026-06-26T10:12:44.000-06:00",
  "section": [
    {
      "title": "Chief complaint",
      "code": {
        "coding": [
          { "system": "http://loinc.org", "code": "10154-3", "display": "Chief complaint" }
        ]
      },
      "text": {
        "status": "generated",
        "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Cefalea de 3 días.</div>"
      }
    }
  ]
}
```

Note the `encounter` reference: it resolves to the **same id** as the Composition — proof the two
are projections of one visit row.

### 3. Read the note and capture its version

A single read returns the current note and stamps the weak `ETag` you condition the amend on.
Search instead with `patient`, `date`, `type`, or `_id` to list a patient's notes.

<Endpoint method="GET" path="/fhir/R4/Composition/01965e2a-8c4d-7000-9060-0000000000e9" />

```bash
curl -i "https://api.huli.ai/fhir/R4/Composition/01965e2a-8c4d-7000-9060-0000000000e9" \
  -H "Authorization: Bearer $HULI_TOKEN" \
  -H "Accept: application/fhir+json"
```

Read the `ETag` response header — e.g. `ETag: W/"1769472764000000000"`. The `type` search
parameter only matches the consultation-note code (`11488-4` or `http://loinc.org|11488-4`); any
other value returns an empty bundle, since every Huli `Composition` is a consultation note.

### 4. Amend the note with If-Match

A `Composition` `PUT` is a **full replace** of the narrative: send the complete section set you
want stored, not just the changed one — a section you omit is cleared. Set the `If-Match` header
to the `ETag` you captured so a concurrent edit cannot be lost. The server re-checks the version
against the locked row inside the update transaction, so the guard holds even under a race.

<Endpoint method="PUT" path="/fhir/R4/Composition/01965e2a-8c4d-7000-9060-0000000000e9" />

:::CodeGroup

```bash
curl -i -X PUT https://api.huli.ai/fhir/R4/Composition/01965e2a-8c4d-7000-9060-0000000000e9 \
  -H "Authorization: Bearer $HULI_TOKEN" \
  -H "Content-Type: application/fhir+json" \
  -H "Accept: application/fhir+json" \
  -H 'If-Match: W/"1769472764000000000"' \
  -d '{
    "resourceType": "Composition",
    "status": "final",
    "subject": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2" },
    "author": [ { "reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1" } ],
    "section": [
      {
        "code": { "coding": [ { "system": "http://loinc.org", "code": "10154-3" } ] },
        "text": {
          "status": "generated",
          "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Cefalea de 3 días, ya resuelta.</div>"
        }
      },
      {
        "code": { "coding": [ { "system": "http://loinc.org", "code": "18776-5" } ] },
        "text": {
          "status": "generated",
          "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Alta. Sin necesidad de control.</div>"
        }
      }
    ]
  }'
```

```typescript
const etag = 'W/"1769472764000000000"'; // captured from the read in step 3

const resp = await fetch(
  'https://api.huli.ai/fhir/R4/Composition/01965e2a-8c4d-7000-9060-0000000000e9',
  {
    method: 'PUT',
    headers: {
      Authorization: `Bearer ${process.env.HULI_TOKEN}`,
      'Content-Type': 'application/fhir+json',
      Accept: 'application/fhir+json',
      'If-Match': etag,
    },
    body: JSON.stringify({
      resourceType: 'Composition',
      status: 'final', // completes the visit
      subject: { reference: `Patient/${process.env.PATIENT_ID}` },
      author: [{ reference: `Practitioner/${process.env.PRACTITIONER_ID}` }],
      section: [
        section('10154-3', 'Chief complaint', 'Cefalea de 3 días, ya resuelta.'),
        section('18776-5', 'Plan of care', 'Alta. Sin necesidad de control.'),
      ],
    }),
  },
);

if (resp.status === 200) {
  console.log('amended', resp.headers.get('ETag')); // a new version
} else if (resp.status === 409) {
  // HPB-00103 — someone amended it since you read it. Re-read step 3, reapply, retry.
  console.log('stale — re-read and retry');
}
```

:::

A `200 OK` returns the updated `Composition` with a **new** `versionId`/`ETag`. Setting `status`
to `final` completes the underlying visit.

## What to verify

- The create is `201`. <StatusBadge code="201" /> The response `resourceType` is `Composition`
  with a server-assigned `id`, and the `encounter` reference resolves to that same id.
- The response carries an `ETag` header and a matching `meta.versionId`.
- Your `section[]` round-trips: each LOINC code you sent comes back, with the narrative inside
  `text.div`.
- The amend is `200` and its `ETag`/`versionId` differs from the one you sent in `If-Match`.
- A re-read after an `entered-in-error` or `final` write reflects the new `status`.

## What can go wrong

All errors return a FHIR `OperationOutcome` — `{severity, code, diagnostics}`, no `details`
object. Branch on the HTTP status and `issue[0].code`; the Huli code is the prefix of
`issue[0].diagnostics`, split on `": "`.

<StatusBadge code="409" /> `HPB-00103` — **version conflict.** Your `If-Match` did not match the
stored version: the note changed between your read and your `PUT`. Re-read it (step 3), reapply
your edit onto the fresh copy, and retry with the new `ETag`. Do not strip `If-Match` to force
the write through — that is the exact lost-update you are guarding against.

<StatusBadge code="403" /> `HPB-00104` — **insufficient scope.** The token lacks
<Scope>system/Composition.cru</Scope> (or `.rs` for a read). Because the **Clinical information** card
is BAA-gated, confirm the key was minted with a BAA attestation in Practice Settings.

<StatusBadge code="404" /> `HPB-00117` — **Composition not found.** The id does not name a visit
in your organization (or it was never created). Confirm the id and the token's organization.

<StatusBadge code="400" /> `HPB-00101` — **validation error.** A required field is missing
(`status`, `subject`, or `author` on create) or the JSON is malformed. The `subject` must
reference a patient, and `author[0]` a practitioner, that resolve in your organization.

<StatusBadge code="422" /> **Illegal status transition.** A note already `final` cannot be set to
`entered-in-error` through this surface — a finalized visit cannot be voided, mirroring the
encounter model. Void a `preliminary` note instead; for a finalized one, record a correcting
note.

A representative `409` body:

```json
{
  "resourceType": "OperationOutcome",
  "issue": [
    {
      "severity": "error",
      "code": "conflict",
      "diagnostics": "HPB-00103: Version conflict"
    }
  ]
}
```

## Next recipes

- **[Creating a clinical encounter](/v1/recipes/creating-an-encounter)** — set the visit envelope
  and diagnoses (the `Encounter` projection of this same row).
- **[Uploading a document](/v1/recipes/uploading-a-document)** — attach a lab PDF or scanned file
  to the patient with `DocumentReference.$upload`, under the same Clinical information card.
- **[Fetching a patient's full record](/v1/recipes/fetching-a-patient-record)** — pull every
  Composition, Encounter, Observation, and document for a patient in one `$everything` Bundle.
