Skip to main content
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 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 — 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_at may 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 send ends_at it is used as-is (it must be after starts_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 with GET /availability. resource_id is required when the location uses a resource-view calendar (GET /locations → calendar_view = 1: appointments sit in a room/table/bench, listed by GET /resources?location_id=…) and must be an active resource at that location — a missing or wrong one is a 422 resource_unavailable; on a practitioner-view location (calendar_view = 2) leave it out. Optional note (≤ 1000 chars). The patient must be active and any location, practitioner and type must exist (422 with patient_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: send patient_id and group_master_id (optionally appointment_type_id) — the session’s time, place and practitioner are the master’s; 409 group_full when it is full, 422 group_not_found when 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_id cannot be sent as null on update — an appointment can’t be unassigned — that’s a 422; omit it to leave the practitioner as is. patient_id and group_master_id are not accepted on update. A visit that has already been processed cannot be changed — 409.
  • Delete is a permanent hard delete — only a deleted_entities audit row survives, not a soft delete. Only a visit still pending or arrived, with no active invoice, can be deleted; a processed, cancelled or missed visit refuses with 409 appointment_rule — unprocess or reinstate it (and void any invoice) first.
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 are created with a patient_id and at least one line in line_items[] (description ≤ 255 chars, quantity — a whole number ≥ 1, price with 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 optional provider_id) — an invoice never exists without a line; optional practitioner_id, invoice_date (YYYY-MM-DD, defaults to today), note (≤ 1000 chars). 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 the id you read back to keep/update a line, omit the id to add a line, leave a line out to remove it. On a line sent with id, omitting billable_item_id leaves its existing service/product link unchanged; billable_item_id: null is refused — a line can never be stripped of its link. 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, billable_item_id, …) or lines: [...] to add several atomically (returns the created lines); PATCH /line_items/{id} to change one (same billable_item_id omit-to-keep rule as above — null is 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 is 404, and the last line on an invoice cannot be removed — void the invoice instead.
  • Payments need patient_id, amount (2 dp) and payment_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 is 422 payment_method_not_found / payment_method_inactive. Optional note (≤ 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 to patient_id (404 otherwise); 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 with 409.
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.
  • Patient logs (POST /patient_logs): patient_id, type (note, comms, financial, appt, membership, system, historical) and data; optional sub_type, source_entity. Entries are recorded against your integration (source: api) and can be deleted (which also removes their attachments), the same as the Activity Log tab.
  • Patient log tags: patient_log_id (must be an entry on this account — 422 patient_log_not_found) and tag_name (≤ 64 chars).
  • Payment methods and referral sources follow the Settings screens’ rules: name is required and unique on the account (payment-method names allow letters, digits, spaces and - _ / & ( )); a referral source needs visibility (public or private) on create. Neither can be deleted through the API — deactivate instead (active: false).

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.