> ## Documentation Index
> Fetch the complete documentation index at: https://build.practicehub.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing

> Creating, updating and deleting records — with PracticeHub's own rules applied

| Request                   | Result                                                         |
| ------------------------- | -------------------------------------------------------------- |
| `POST /{resource}`        | Create — `201` with the new record                             |
| `PATCH /{resource}/{id}`  | Update the attributes you send — `200` with the updated record |
| `DELETE /{resource}/{id}` | Delete — `204`, no body                                        |

Send JSON with `Content-Type: application/json`. Updates use `PATCH` and are partial: only the attributes present in the body change (`PUT` is not supported — `405`). `id`, `created` and `updated` are always server-managed — sending them is ignored.

```bash theme={null}
curl -X PATCH https://your-clinic.your-region.practicehub.io/v3/api/patients/340 \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "email": "ada@example.org", "metadata": { "crm_id": "hs-778" } }'
```

## What each resource accepts

`GET /schema/{resource}` lists `write_operations` and, for `create` and `update`, every accepted attribute with whether it is required and its validation rules — for example `["required", "date_format:Y-m-d H:i:s"]`. Anything not listed is rejected with a `422`, so you never wonder whether an attribute was applied.

Broadly:

| Resources                                                                                      | Writes                                                                                                                                                                                                                        |
| ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `patients`, `appointments`, `payments`, `invoices`, `line_items`, `files`                      | Create, update, delete — through PracticeHub's own services (rules below)                                                                                                                                                     |
| `payment_allocations`                                                                          | Create and delete (allocations are not edited — remove and re-create)                                                                                                                                                         |
| `patient_logs`, `patient_log_tags`                                                             | Create, update, delete                                                                                                                                                                                                        |
| `payment_methods`, `referral_sources`                                                          | Create, update — **no delete** (other records point at these; deactivate them in PracticeHub instead — `DELETE` returns `405 unsupported_operation`)                                                                          |
| `practitioners`, `locations`, `practitioner_availabilities`, `appointment_types`, `modalities` | Read-only for now — the app creates these through flows with side effects (team member + user + licence; location + calendar settings; availability series). Writes return `405` until they are routed through those services |
| `numbers`, `addresses`                                                                         | Read-only as their own resources — write them through `patients` (`numbers[]`, `address`), which normalises and validates them                                                                                                |
| `deleted_entities`                                                                             | Read-only — writes return `405`                                                                                                                                                                                               |

## The app's rules apply

Writes to the clinical and financial resources run through the same code as the PracticeHub UI, so what you can do is what a team member can do, and every side effect happens as normal (confirmation messaging, balances, audit trail). Refusals come back as a `409` (a business rule) or `422` (invalid input) with a message you can show to a user; a read-only key gets `403` on any write — see [Errors](/guides/errors).

<AccordionGroup>
  <Accordion title="Appointments">
    * **Create** needs `patient_id`, `location_id`, `appointment_type_id`, `practitioner_id`, `start`, `end` (clinic timezone, `YYYY-MM-DD HH:MM:SS`); optional `resource_id`, `note`, `columns`. The patient must be active, the location and practitioner active, and any resource at that location. The time must be free in the practitioner's diary — find one with [`GET /availability`](/guides/booking) — or the create is refused with `409 slot_unavailable`; send `allow_clash: true` to overbook deliberately. Booking a place in an existing group appointment: send `group_master_id` — refused with `409` when the group is full.
    * **Update** moves the booking (`start`, `end`, `practitioner_id`, `location_id`, `resource_id`), changes its type (`appointment_type_id`) or note, or transitions its status:

      | `status`    | Effect                                                                  |
      | ----------- | ----------------------------------------------------------------------- |
      | `cancelled` | Cancels — optional `cancel_reason_id`, `cancel_note`                    |
      | `missed`    | Marks as a no-show — same optional fields                               |
      | `arrived`   | Marks the patient as arrived                                            |
      | `pending`   | Reinstates a cancelled/missed appointment, or un-arrives an arrived one |

      A visit that has already been processed cannot be changed — `409`.
    * **Delete** removes the appointment as the calendar would.
  </Accordion>

  <Accordion title="Patients">
    `first_name` is required on create. Contact details go in the nested `numbers` array (`number` and `type` — `mobile` / `home` / `work` / `other` — are required; `country_code`, `iso2` optional) and `address` object (`line1`, `line2`, `city`, `state`, `postcode`, `country`); both come back on every patient read in the same shape (`numbers[]` with each stored `id`, `intl_number` and normalised `type`; `address` or `null`). On update, `numbers` is reconciled by `id`: entries carrying the `id` you read back are updated in place, entries without one are added, and stored numbers you leave out are removed — so read the patient, edit the list, send it back (a list with no ids at all still replaces everything, for simple clients). An `id` that isn't one of the patient's numbers is a `422`. Sending `address` keys changes only those keys. Deleting a patient is the same soft-delete the app performs; the record then appears in `deleted_entities`.
  </Accordion>

  <Accordion title="Invoices, payments and allocations">
    * **Invoices** are created with a `patient_id` and at least one line in `line_items[]` (`description`, `quantity`, `price`, optional `billable_item_id`, `provider_id`) — an invoice never exists without a line. `PATCH /invoices/{id}` without `line_items` changes only the header (`note`, `invoice_date`, `practitioner_id` — line providers are left as they are; send `line_items` with `provider_id` to change them); with `line_items` it reconciles the set — send each existing line's `id` to change it in place, omit the `id` to add a line, leave a line out to remove it. Deleting an invoice voids it, which releases its allocations; add `?void_payments=true` to also void manual payments left with nothing to pay.
    * **Line items**: `POST /line_items` with `invoice_id` and either one line (`description`, `quantity`, `price`, …) or `lines: [...]` to add several atomically (returns the created lines); `PATCH /line_items/{id}` to change one, `DELETE /line_items/{id}` to remove one. Every change re-totals the invoice and its balance. Void and care-plan invoices refuse edits (`409 billing_rule`), and the last line on an invoice cannot be removed — void the invoice instead.
    * **Payments** need `patient_id`, `amount` (2 dp) and `payment_type_id` — a real, active payment method (`GET /payment_methods?active=eq:1`); card-on-file, refunds and credit transfers have their own flows in PracticeHub and are not created here. Deleting a payment voids it.
    * **Payment allocations** apply a payment to an invoice: `patient_id`, `payment_id`, `invoice_id`, `amount`. Over-allocating either side is refused with `422`/`409`.
  </Accordion>

  <Accordion title="Files">
    Upload as `multipart/form-data` with the `file` part (max 16 MB) plus `patient_id` and `type` (`generic`, `patient_profile`, `custom_content`, `diagnostic_image`, `patient_log` — the last also needs `patient_log_id`); optional `description`, `patient_viewable`. `patient_profile` accepts jpg/jpeg/png only. Update changes `description`, `patient_viewable` and `type` (between `generic`, `custom_content` and `diagnostic_image` — `patient_profile` and `patient_log` are set at upload), not the file itself.
  </Accordion>
</AccordionGroup>

## Who did it

Audit trails record API writes against the integration the key belongs to (*"Created via API: Booking widget"*); actions taken through an [OAuth connector](/mcp/connectors) are recorded against the team member who approved it.

## Retrying safely: idempotency keys

A `POST` that times out may or may not have created the record. Send an `Idempotency-Key` header — any unique string of your choosing, up to 255 characters, such as a UUID or your own order id — and retries are safe:

```bash theme={null}
curl -X POST https://your-clinic.your-region.practicehub.io/v3/api/payments \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2f7e-order-778" \
  -d '{ "patient_id": 340, "amount": "45.00", "payment_type_id": 3 }'
```

* The first request with a key runs normally. Any repeat with the **same key and same body** within 24 hours returns the **original response** (same status and body, plus `Idempotent-Replayed: true`) without creating anything.
* The same key with a **different body** is refused with `422` — a key identifies one operation.
* A duplicate sent while the first is still running gets `409`; wait and retry.
* Keys are scoped to your credential, so different integrations cannot collide. Server errors (`5xx`) are not remembered, so those retries execute normally.

Updates and deletes are naturally repeatable and do not need a key. Updates are last-write-wins.
