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 a409 (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.
Appointments
Appointments
-
Create needs
patient_id,location_id,appointment_type_id,practitioner_id,start,end(clinic timezone,YYYY-MM-DD HH:MM:SS); optionalresource_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 withGET /availability— or the create is refused with409 slot_unavailable; sendallow_clash: trueto overbook deliberately. Booking a place in an existing group appointment: sendgroup_master_id— refused with409when 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.
Patients
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.Invoices, payments and allocations
Invoices, payments and allocations
- Invoices are created with a
patient_idand at least one line inline_items[](description,quantity,price, optionalbillable_item_id,provider_id) — an invoice never exists without a line.PATCH /invoices/{id}withoutline_itemschanges only the header (note,invoice_date,practitioner_id— line providers are left as they are; sendline_itemswithprovider_idto change them); withline_itemsit reconciles the set — send each existing line’sidto change it in place, omit theidto add a line, leave a line out to remove it. Deleting an invoice voids it, which releases its allocations; add?void_payments=trueto also void manual payments left with nothing to pay. - Line items edit one line at a time:
POST /line_itemswithinvoice_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) andpayment_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 with422/409.
Files
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.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
APOST 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.