For developers
Integrate your website with Consulta
Consulta ships the integration surfaces a clinic actually uses, live today: an inline booking widget, the booking endpoints it runs on, signed webhooks for everything that needs to know a consult happened, and one-click data export. This page documents all of them. A general-purpose partner REST API is roadmap. It gets built the day a partner has a concrete use these surfaces cannot serve, so if that is you, tell us the use case.
The inline booking widget
Two lines of paste. Live consult times render inline on your own page, in your brand colour, in each visitor's own timezone, and the whole booking happens there. It is native HTML in a shadow root, not an iframe: your fonts flow in, your CSS cannot break it, and it degrades to a plain booking button if times cannot load (a failed submission shows an inline error instead, so the visitor's details are never thrown away). Payment, when the patient pays, hands off to Stripe's hosted checkout so card details never touch your site or ours.
<div data-consulta-booking="your-page-slug"></div> <script async src="https://consultahealth.com.au/widget.js"></script>
Optional attributes
| Field | Type | Notes |
|---|---|---|
| data-topic | string | Scope the widget to one consult topic |
| data-limit | number | How many times to show, 1-12 (default 6) |
| data-label | string | Text for the fallback button if times cannot load |
Your page slug and copy-ready snippets live in your portal under Booking page. A live interactive preview is on the embed page.
Booking endpoints
The widget runs on two public cross-origin endpoints, and you can call them directly from your own form instead. Both are rate limited (a throttled request returns HTTP 429 with a Retry-After header). The booking endpoint additionally carries a honeypot and enforces the same terms, server-side pricing and slot-race handling as every other booking path.
GET /api/embed/slots
GET https://consultahealth.com.au/api/embed/slots?slug=your-page-slug&limit=6
&topic=optional-topic| Field | Type | Notes |
|---|---|---|
| clinicName | string | Your clinic's display name |
| accent | string | Your page's brand colour (hex) |
| bookUrl | string | Your hosted booking page URL |
| mode | string | patient_paid or clinic_paid |
| priceCents | number | The patient price your page shows |
| payOnline | boolean | Whether booking ends at a Stripe checkout |
| defaultTopic | string | Topic used when none is passed |
| questionnaire | object | Your page's question set: source, questions[] with key / label / required / placeholder, and acknowledgements[] with key / label. Render your form from this and submit answers by key. A page set to collect its questionnaire outside Consulta returns {source: "clinic"} alone |
| slots[] | {start, doctorId} | Next open times, pass both values back when booking |
POST /api/embed/book
POST https://consultahealth.com.au/api/embed/book
Content-Type: application/json
{
"slug": "your-page-slug",
"doctorId": "<from the slots feed>",
"slotStart": "<from the slots feed>",
"topic": "optional-topic",
"name": "Alex Example",
"email": "alex@example.com",
"phone": "0400 000 000",
"dob": "1990-01-31",
"timezone": "Australia/Adelaide",
"termsAccepted": true,
"intake": [{ "key": "reason", "answer": "..." }],
"acknowledgements": ["ack_1"]
}| Field | Type | Notes |
|---|---|---|
| slug | string, required | Your booking page slug |
| doctorId | string, required | From the slots feed |
| slotStart | string, required | From the slots feed |
| topic | string | Falls back to the page's default topic |
| name / email | string, required | The patient's details |
| phone | string, required | An Australian mobile (04xx xxx xxx, or +61 4xx xxx xxx) or an international number written as + and 8 to 15 digits. Spaces, brackets, dots and dashes are ignored |
| dob | string | Optional date of birth, YYYY-MM-DD |
| timezone | string | IANA zone, patient-facing times render in it |
| termsAccepted | boolean, required | Must be true |
| intake[] | {key, answer} | Optional questionnaire answers, see below |
| acknowledgements[] | string[] | Keys of your page's tick statements, required when intake is sent and your page defines any |
Submitting the questionnaire with the booking: your page's question set, authored in your portal, is the contract, and the slots feed's questionnaire field serves it: render your form from those questions and submit each answer keyed by its key. Labels are snapshotted from your definition, never trusted from the caller. Every required question and every defined acknowledgement must be present or the request is refused with intake_incomplete before anything is booked. Omit intake entirely and the patient is instead invited to complete the questionnaire after booking, chased automatically. Pages set to collect their questionnaire outside Consulta refuse intake with intake_not_accepted.
Responses
// patient pays on your page's terms: redirect the visitor here
{ "ok": true, "checkout": true, "checkoutUrl": "https://checkout.stripe.com/..." }
// no online payment step: booked immediately
{ "ok": true, "reference": "BK-8A3101", "doctorName": "...",
"slotStart": "...", "priceLabel": "...", "manageUrl": "https://..." }
// refused: nothing was booked or charged
{ "ok": false, "code": "slot_taken", "message": "..." }Refusals are HTTP 400 with a code and a patient-presentable message, except too_many_requests (429 with Retry-After) and unavailable / lookup_failed (503, safe to retry: nothing was booked). Malformed JSON is a 400 with a message and no code.
| Field | Type | Notes |
|---|---|---|
| slot_taken | code | That time was just booked, refresh the feed |
| invalid_details / invalid_phone | code | Patient details failed validation |
| terms_required | code | termsAccepted was not true |
| invalid_intake | code | intake or acknowledgements was not an array |
| intake_incomplete | code | A required question or acknowledgement is missing |
| intake_not_accepted | code | The page collects its questionnaire outside Consulta |
| intake_too_large | code | Questions plus acknowledgements exceed the per-booking cap, omit intake |
| closed | code | The page is not taking bookings |
| lookup_failed | code | Your booking page could not be read. Temporary, retry (HTTP 503) |
| too_many_requests | code | Rate limit reached (HTTP 429) |
| unavailable | code | Transient failure, retry (HTTP 503) |
| unknown | code | Anything else, nothing was booked or charged |
Webhooks
Self-serve in your portal (Booking page tab, "Notify your own systems"): set a URL and a signing secret and Consulta POSTs on ten booking-lifecycle events. This is the route into a CRM, Mailchimp or anything Zapier or Make can reach, with zero custom development on either side.
| Field | Type | Notes |
|---|---|---|
| booking.created | event | A booking is made, any channel |
| booking.rescheduled | event | The patient moves the appointment |
| booking.cancelled_by_patient / _by_clinic / _by_doctor | event | A cancellation, with who cancelled in the name |
| consult.completed | event | The practitioner confirms the consult happened |
| consult.no_answer | event | The practitioner could not reach the patient |
| consult.script_returned | event | A script comes back to the booking |
| booking.questionnaire_completed | event | The questionnaire is submitted |
| booking.patient_document | event | The patient shares a document (adds file_name) |
{
"event": "booking.created",
"delivery_id": "5e2f...",
"occurred_at": "2026-08-19T09:30:00.000Z",
"booking": {
"reference": "BK-8A3101",
"status": "confirmed",
"slot_start": "2026-08-20T23:30:00+00:00",
"slot_end": "2026-08-20T23:50:00+00:00",
"topic": "weight-management",
"doctor_name": "Dr. Jane Citizen",
"patient": { "name": "Alex Example",
"email": "alex@example.com",
"phone": "0400 000 000" }
}
}Deliberately absent, by construction: questionnaire answers, clinical notes, outcome detail, prescriptions and pricing never appear in a payload. The payload is built from a fixed field whitelist, so those cannot leak by accident. Payloads do carry the patient's contact details, the consult topic and, for patient documents, the file name, so treat every payload as personal information and point webhooks only at systems entitled to hold it.
Verifying and retries
Every request carries X-Consulta-Event, X-Consulta-Delivery (unique id, reused on retries, use it for idempotency) and X-Consulta-Signature: sha256=<hex>, an HMAC-SHA256 of the raw body keyed with your signing secret. Reject anything that fails verification. Respond 2xx within 6 seconds. Redirects are not followed. Otherwise Consulta makes up to five attempts in total: the immediate send, then retries about 5 minutes, 25 minutes, 105 minutes and 7 hours after the event. Your portal shows the most recent deliveries and their status.
const crypto = require("crypto");
const expected = "sha256=" +
crypto.createHmac("sha256", process.env.CONSULTA_WEBHOOK_SECRET)
.update(rawBody).digest("hex");
const given = String(req.headers["x-consulta-signature"] ?? "");
const ok = given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given));Data and export
You are never locked in. Your portal's Settings > Data tab exports your records as CSV whenever you like, without asking us. It covers every consult with its practitioner, times, outcome and price, your patient roster with contact details and references, every questionnaire answer, and every prescription with its eScript link. Attached documents and the printable per-consult compliance file download per consult from the booking's records.
Patient fields Consulta holds
| Field | Type | Notes |
|---|---|---|
| full name | string | Required at booking |
| string | Required at booking | |
| mobile | string | Required, the practitioner calls it |
| date of birth | date | Optional at booking |
| your reference | string | Your own patient identifier, optional |
| questionnaire answers | per your page | Whatever questions you author, snapshotted as answered |
Contact details on old bookings are purged on a schedule as part of Consulta's data-minimisation posture. The clinical record itself is retained.
Security
Patient and clinic records are stored in Australia (Sydney). Payments are processed by Stripe, and anything you point a webhook at is hosted wherever that system lives. Every clinic and practitioner account must enrol multi-factor authentication before using the portal. Access is enforced with row-level security at the database, scoping a clinic's access to its own records, and team roles gate configuration (billing, booking-page settings and webhook changes are manager-only). Card details never touch Consulta's servers or yours.
Questions, or a use case these surfaces don't cover? bookings@consultahealth.com.au