Booked calls API
Some consultations need a live video or audio call between the patient and a prescriber. This API lists the days that have free times, shows the free times of the day the patient picks, books the time they pick, moves or cancels it, reminds you an hour before, and tells you about every change so you can notify the patient. How the patient joins the call from your site: Joining a call.
Overview
- A consultation is either
async(written review, as today) orsync(it needs a call). - Calls are for categories that require them. Today that is TRT (
questionnaire_type: "trt"). Other categories cannot have a call. - A call belongs to one consultation and one patient. Only the consultation's prescriber joins it.
- Calls are 5 minutes, inside the weekly hours of prescribers Bespoke cleared for the category. Weekends are offered only when a prescriber works them.
- The patient picks any day with a free slot in the next 14 days, today included: a call today can start 2 hours from now at the earliest.
- Only the patient picks the time, through you. The prescriber does not book or move calls, and does not confirm them: a booked call goes straight into the prescriber's calendar.
- You receive
appointment.reminderabout 60 minutes before each call, so you can remind the patient. - One consultation has at most one booked call at a time. A follow-up call is a new booking on the same consultation.
- Booking a call never changes the consultation
status. A call that took place does not approve the consultation: that stays the prescriber's decision.
Enablement
| Who | What |
|---|---|
| Bespoke admin | Turns on Video visits for your account (off by default). |
| Bespoke admin | Turns on outbound webhooks, so you hear about bookings, reminders, and changes Bespoke makes. |
| Partner owner | Under Integration → Webhooks, subscribes to the appointment.* events and consultation.call_requested. If you picked your events before, tick the new ones: they are not added for you. |
If video visits are disabled, every endpoint on this page returns 403 video_visits_disabled, consult_mode: "sync" is rejected with 422, and TRT consultations stay async as before.
Call consultations
A consultation becomes sync in one of these ways:
- You create a TRT consultation (
questionnaire_type: "trt") while video visits are on. It issyncautomatically; you do not need to sendconsult_mode. - You send
"consult_mode": "sync"onPOST https://a-nirwan.bespoke-consults.designingit.co/v1/visitsor onPUT https://a-nirwan.bespoke-consults.designingit.co/v1/visits/{id}while it is open. - The prescriber asks for a call while reviewing it. You receive
consultation.call_requested.
"consult_mode": "sync" for a category that does not offer calls (for example
GLP-1) returns 422 on consult_mode. A consultation sent with no
category may still ask for a call. A sync consultation never goes back to
async; sending "consult_mode": "async" for it returns
422. The visit object carries consult_mode and an
appointment summary (the booked call, or null).
A sync consultation keeps a category that offers calls: changing
questionnaire_type to one without calls, on POST https://a-nirwan.bespoke-consults.designingit.co/v1/visits
with the same external_ref or on https://a-nirwan.bespoke-consults.designingit.co/v1/visits/{id}/questionnaire,
returns 422 on questionnaire_type. While a call is booked, the
consultation also keeps its prescriber (422 on practitioner_id) and
its patient (422 on patient): cancel the call first.
curl -X POST "https://a-nirwan.bespoke-consults.designingit.co/v1/visits" \
-H "X-Api-Key: $BESPOKE_KEY" \
-H "X-Api-Secret: $BESPOKE_SECRET" \
-H "Content-Type: application/json" \
-d '{
"external_visit_id": "your-consult-2001",
"patient_ref": "your-pt-55",
"questionnaire_type": "trt"
}'
Flow: the patient picks a time
- You create a TRT consultation (it is
sync), or you receiveconsultation.call_requested. - Your backend asks for availability:
GET /visits/{id}/availability?timezone=…. The answer lists every day with a free slot (days) and the slots of the nearest one. - Your UI shows the patient the days. When the patient picks another day, ask again with
&date=YYYY-MM-DDfor its slots. The patient picks a time. - Your backend books it:
POST /visits/{id}/appointmentwith theslotid. - You receive
appointment.scheduledwithbooked_by: "partner". The prescriber does not confirm: the call is booked. - About 60 minutes before the start you receive
appointment.reminder. Remind the patient.
Which days are offered, in the patient's time zone: every day from today through the next
14 days that still has a free slot. Today counts once a slot is at least
2 hours away; a full day is left out. Weekends are offered only when a
prescriber works them. The slots are the hours of the consultation's own prescriber, the
one assigned when the consultation was created (the visit's practitioner_id).
Booking never changes the prescriber. Slots carry no prescriber details.
Endpoints
GET https://a-nirwan.bespoke-consults.designingit.co/v1/visits/{id}/availability
POST https://a-nirwan.bespoke-consults.designingit.co/v1/visits/{id}/appointment
GET https://a-nirwan.bespoke-consults.designingit.co/v1/visits/{id}/appointment
POST https://a-nirwan.bespoke-consults.designingit.co/v1/visits/{id}/appointment/reschedule
POST https://a-nirwan.bespoke-consults.designingit.co/v1/visits/{id}/appointment/cancel
GET https://a-nirwan.bespoke-consults.designingit.co/v1/visits/{id}/appointments
{id} is the visit UUID or your external_visit_id. Same auth headers
as the rest of the API. All times are ISO 8601. Responses give UTC and, when you send the
patient's time zone, the patient's local time.
Free slots
days, takes the day you
want in date, and offers today from 2 hours ahead. Calls are
now 5 minutes. The other fields are unchanged, so code that shows the one
day it gets still works; to let the patient pick another day, read days and send
date.
GET https://a-nirwan.bespoke-consults.designingit.co/v1/visits/your-consult-2001/availability?timezone=America/Chicago
GET https://a-nirwan.bespoke-consults.designingit.co/v1/visits/your-consult-2001/availability?timezone=America/Chicago&date=2026-11-11
| Query | Meaning |
|---|---|
timezone | Recommended. IANA name of the patient's zone, for example America/New_York. It decides which day is "today" for the patient. Without it, the prescriber's zone is used. |
date | Optional. The day whose slots you want, YYYY-MM-DD in that zone, usually one from days. Without it, you get the nearest day in days. |
There is no date range and no choice of prescriber: from, to, and
prescriber are rejected with 422. Slots come one day at a time,
since each slot id is encrypted and a day of 5-minute calls already holds many.
Each call is followed by a 5-minute break, so slots start every 10 minutes.
Asked on Tuesday, November 10, 2026, for Wednesday:
{
"visit_id": "…",
"external_visit_id": "your-consult-2001",
"days": ["2026-11-10", "2026-11-11", "2026-11-12", "2026-11-13", "2026-11-16"],
"date": "2026-11-11",
"timezone": "America/Chicago",
"slot_minutes": 5,
"slot_valid_minutes": 60,
"slots": [
{
"slot": "eyJpdiI6…",
"starts_at": "2026-11-11T14:00:00+00:00",
"ends_at": "2026-11-11T14:05:00+00:00",
"local_starts_at": "2026-11-11T08:00:00-06:00",
"local_ends_at": "2026-11-11T08:05:00-06:00"
}
]
}
dayslists every day with a free slot, earliest first, intimezone: today (if a slot is still 2 hours away) up to 14 days ahead. It carries no slot ids.dateis the day the slots belong to.timezonesays which zone was used, the patient's or the prescriber's.local_starts_atandlocal_ends_atare in that zone.- A
datethat is full, past, or more than 14 days ahead returns an emptyslots, not an error. A badly writtendatereturns422ondate. - If nothing is free in the next 14 days,
daysis empty,dateisnull, andslotsis empty. Ask again later. slotis an opaque id. It works only for this consultation and only for 60 minutes. After that, fetch availability again.
Book
curl -X POST "https://a-nirwan.bespoke-consults.designingit.co/v1/visits/your-consult-2001/appointment" \
-H "X-Api-Key: $BESPOKE_KEY" \
-H "X-Api-Secret: $BESPOKE_SECRET" \
-H "Content-Type: application/json" \
-d '{
"slot": "eyJpdiI6…",
"modality": "video",
"patient_timezone": "America/Chicago"
}'
| Field | Meaning |
|---|---|
slot | Required. A slot id from availability. |
modality | Required. video or audio. |
patient_timezone | Optional IANA zone. Used for local times in responses and webhooks. |
patient_location_state | Optional two-letter state where the patient will be during the call. Stored for later licensure checks. |
Returns 201 with { visit_id, external_visit_id, appointment }.
Sending the same slot again returns 200 with the same appointment, so a retry is safe.
If someone took the slot first, you get 409 slot_unavailable: fetch availability
again and let the patient choose. If the consultation already has a call, reschedule or cancel it instead.
Reschedule and cancel
POST https://a-nirwan.bespoke-consults.designingit.co/v1/visits/{id}/appointment/reschedule
{ "slot": "eyJpdiI6…", "modality": "audio" }
POST https://a-nirwan.bespoke-consults.designingit.co/v1/visits/{id}/appointment/cancel
{ "reason": "patient_request", "note": "Patient asked to cancel." }
- Reschedule takes a new
slotfrom availability, which then offers only the days and slots of the same prescriber.modalityis optional and keeps the old one if left out. To change only the type, pick another time: the same time with anothermodalityreturns422. The old appointment is closed withcancel_reason: "rescheduled", and the new one hasrescheduled_from_id. - Cancel takes
reason:patient_requestorother. The optionalnoteis kept for Bespoke staff and is not sent back. - Both are allowed any time before the call, with no limit. With no booked call they return
404 no_appointment. GET …/appointmentreturns the booked call.GET …/appointmentslists every call on the consultation, cancelled ones included, sorted by call start time (calls at the same time in the order they were booked). Followrescheduled_from_idto see which call replaced which.
Appointment object
{
"appointment_id": "…",
"status": "scheduled",
"modality": "video",
"starts_at": "2026-11-02T14:00:00+00:00",
"ends_at": "2026-11-02T14:05:00+00:00",
"patient_timezone": "America/Chicago",
"patient_local_starts_at": "2026-11-02T08:00:00-06:00",
"patient_local_ends_at": "2026-11-02T08:05:00-06:00",
"practitioner_id": "…",
"external_practitioner_id": null,
"booked_by": "partner",
"cancelled_by": null,
"cancel_reason": null,
"cancelled_at": null,
"completed_at": null,
"no_show_at": null,
"rescheduled_from_id": null,
"created_at": "2026-10-30T09:12:44+00:00"
}
| Field | Values |
|---|---|
status | scheduled, completed (the call took place), no_show (the patient did not join), cancelled |
booked_by | partner or admin (Bespoke staff, on the patient's behalf) |
cancelled_by | partner, prescriber, admin (Bespoke staff), system |
cancel_reason | patient_request, other, rescheduled, prescriber_unavailable (time off, or the prescriber left), consultation_closed (the consultation was completed, declined, or cancelled) |
Webhooks
| Event | When |
|---|---|
consultation.call_requested | The prescriber asked for a call on an async consultation. It is now sync. Ask the patient to pick a time. |
appointment.scheduled | A call was booked, by you or by Bespoke staff on the patient's behalf (booked_by). |
appointment.rescheduled | A call moved. previous_appointment has the old time. |
appointment.cancelled | A call was cancelled, including by the system (cancelled_by: "system") when the prescriber is unavailable or the consultation closed. Rebook if the consultation is still open. |
appointment.no_show | The prescriber recorded that the patient did not join (from 10 minutes after the start). The consultation stays open, and you may book again. |
appointment.reminder | The call starts in about 60 minutes. Sent once per booked call; a rescheduled call gets its own, a cancelled one none. Remind the patient. |
Every event is also sent for changes you made, so one handler keeps your UI in step. Appointment events carry ids and times only:
{
"id": "delivery-uuid",
"event": "appointment.rescheduled",
"occurred_at": "2026-10-30T09:20:01+00:00",
"partner_id": "…",
"data": {
"visit_id": "…",
"external_visit_id": "your-consult-2001",
"patient_id": "…",
"external_patient_id": "your-pt-55",
"brand_code": "acme",
"consult_mode": "sync",
"appointment": { "appointment_id": "…", "status": "scheduled", "starts_at": "…", "booked_by": "partner", "…": "…" },
"previous_appointment": {
"appointment_id": "…",
"starts_at": "2026-11-02T14:00:00+00:00",
"ends_at": "2026-11-02T14:05:00+00:00",
"practitioner_id": "…"
},
"event_context": "appointment.rescheduled"
}
}
previous_appointment is null on every event except
appointment.rescheduled. Envelope, signatures, and retries are the same as for
every other event: see the webhook documentation.
Errors
| Status | Code | When |
|---|---|---|
| 403 | video_visits_disabled | Video visits are not enabled for your account. |
| 404 | not_found | No consultation with that id on your account. |
| 404 | no_appointment | Reschedule, cancel, or get with no booked call. |
| 409 | slot_unavailable | The slot was taken or stopped being free. Fetch availability again. |
| 422 | validation_failed | A field is wrong, and fields names it. For example: the consultation is not sync or is closed, it already has a call, the slot id is invalid, expired, or for another consultation, the time zone is unknown, or availability got a badly written date, or from, to, or prescriber. |
Integration checklist
- Ask Bespoke to enable video visits and webhooks on your account.
- Subscribe to
consultation.call_requestedand the fiveappointment.*events. - Send
questionnaire_type: "trt"for TRT consultations; they need a call and becomesyncon their own. - Show the patient the
daysfrom/availability(withtimezone), fetch the slots of the day they pick withdate, book the time they pick, and handle409by fetching availability again. - On
appointment.reminder, remind the patient. - On
appointment.scheduledwithbooked_by: "admin", tell the patient the time Bespoke staff booked. - On
appointment.cancelledwithcancelled_by: "system", offer the patient a new time. - Send
patient_timezonewhen booking so local times are right. - Let the patient join the call from your site: Joining a call.