Skip to main content
Send JSON with Content-Type: application/json. Updates are partial: only the attributes present in the body change, so PUT and PATCH behave the same. id, created and updated are always server-managed — sending them is ignored.

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:

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.
  • 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 — 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: A visit that has already been processed cannot be changed — 409.
  • Delete removes the appointment as the calendar would.
first_name is required on create. Contact details go in the nested numbers array (number and typemobile / 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.
  • 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 edit one line at a time: POST /line_items with invoice_id + the line, PATCH /line_items/{id} to change it, DELETE /line_items/{id} to remove it. 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.
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_imagepatient_profile and patient_log are set at upload), not the file itself.

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 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:
  • 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.