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_at and updated_at 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:YYYY-MM-DDTHH:MM:SS"]. 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 — the same booking rules the PracticeHub calendar applies, because it is the same code. Required:
patient_id,location_id,appointment_type_id,practitioner_id,starts_at(clinic timezone,YYYY-MM-DDTHH:MM:SS).ends_atmay be omitted — it is derived exactly as the calendar does (the practitioner’s duration for the type, else the type’s, else 30 minutes); if you sendends_atit is used as-is (it must be afterstarts_at). A booking does not check the practitioner’s diary — like a staff click in the calendar it books what you send, so a taken time is written alongside the existing appointment rather than refused; find a free one first withGET /availability.resource_idis required when the location uses a resource-view calendar (GET /locations→calendar_view=1: appointments sit in a room/table/bench, listed byGET /resources?location_id=…) and must be an active resource at that location — a missing or wrong one is a422 resource_unavailable; on a practitioner-view location (calendar_view=2) leave it out. Optionalnote(≤ 1000 chars). The patient must be active and any location, practitioner and type must exist (422withpatient_not_found/location_not_found/practitioner_not_found/appointment_type_not_found). Booking a place in an existing group session is the second request shape: sendpatient_idandgroup_master_id(optionallyappointment_type_id) — the session’s time, place and practitioner are the master’s;409 group_fullwhen it is full,422 group_not_foundwhen it is not a group session. -
Update moves the booking (
starts_at,ends_at,practitioner_id,location_id,resource_id), changes its type (appointment_type_id) or note, or transitions its status.practitioner_idcannot be sent asnullon update — an appointment can’t be unassigned — that’s a422; omit it to leave the practitioner as is.patient_idandgroup_master_idare not accepted on update.A visit that has already been processed cannot be changed —409. -
Delete is a permanent hard delete — only a
deleted_entitiesaudit row survives, not a soft delete. Only a visit stillpendingorarrived, with no active invoice, can be deleted; a processed, cancelled or missed visit refuses with409 appointment_rule— unprocess or reinstate it (and void any invoice) first.
Patients
Patients
first_name and last_name are required on create — the same rule as every patient form in PracticeHub. sex is male, female, other or prefer_not_to_say; dob is YYYY-MM-DD. Contact details go in the nested numbers array (number required; type required with it — mobile, home, work, other (the API’s lowercase vocabulary; capitalised input is accepted too, anything else is stored as mobile); country_code, iso2 optional, intl_number derived) and address object (line_1, line_2, city, state, postal_code, 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). default_location_id must be one of the account’s locations; referral_source_id and referral_source_type (referral_source or patient) travel together and must resolve to an active referral source or a live patient other than this one (422 referral_source_not_found / referral_self); a patient referral through the API always names the referring patient — the “referred by name, unmatched” state a public form can leave for an admin to match is not something an integration with patient access should create (422 referral_patient_required). On update, numbers is reconciled by id: send the id you read back to keep/update a row; 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 phone_number_not_found. 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≤ 255 chars,quantity— a whole number ≥ 1,pricewith 2 dp — negative for a discount or credit line, as on the invoice screen,billable_item_id— required, every line bills one of the account’s services or products — and optionalprovider_id) — an invoice never exists without a line; optionalpractitioner_id,invoice_date(YYYY-MM-DD, defaults to today),note(≤ 1000 chars).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 theidyou read back to keep/update a line, omit theidto add a line, leave a line out to remove it. On a line sent withid, omittingbillable_item_idleaves its existing service/product link unchanged;billable_item_id: nullis refused — a line can never be stripped of its link. Deleting an invoice voids it, which releases its allocations; add?void_payments=trueto also void manual payments left with nothing to pay. - Line items:
POST /line_itemswithinvoice_idand either one line (description,quantity,price,billable_item_id, …) orlines: [...]to add several atomically (returns the created lines);PATCH /line_items/{id}to change one (samebillable_item_idomit-to-keep rule as above —nullis refused),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), an unknown invoice is404, and the last line on an invoice cannot be removed — void the invoice instead. - Payments need
patient_id,amount(2 dp) andpayment_method_id. The method rules are PracticeHub’s own: a real, active payment method of the account (GET /payment_methods); a card method cannot be recorded here because no card is charged (422 card_payment_not_allowed), and PracticeHub’s special types at negative ids (refund, credit, write-off, transfers, rewards, card on file, direct debit) are the app’s own to write (422 payment_method_not_recordable); an unknown or inactive method is422 payment_method_not_found/payment_method_inactive. Optionalnote(≤ 1000 chars),payment_date(YYYY-MM-DD),location_id. Deleting a payment voids it. - Payment allocations apply a payment to an invoice:
patient_id,payment_id,invoice_id,amount(2 dp, > 0). The invoice must belong topatient_id(404otherwise); the payment may be another patient’s — a family member’s payment settling this patient’s invoice is by design, and both balances are refreshed. Over-allocating either side is refused with409.
Files
Files
Upload as
multipart/form-data with the file part (max 50 MB; jpg/jpeg/png/mp3/pdf/txt/csv/doc/docx/xls/xlsx) plus patient_id and file_type (generic, patient_profile, custom_content, diagnostic_image, patient_log — the last also needs patient_log_id, one of the patient’s own log entries); optional description (≤ 255 chars), patient_viewable. patient_profile accepts images only, and a patient has one profile photo: uploading a new one demotes the previous to generic, exactly as the app does. Update changes description, patient_viewable and file_type (generic, custom_content, diagnostic_image, or patient_profile for an image); a patient_log attachment keeps its type (422 patient_log_attachment_locked). The file itself is immutable.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.