Booked calls Updated Nov 13, 2026

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.

Default: disabled. A Bespoke admin must enable Video visits on your partner account. Bespoke has no patient accounts and sends no email or SMS to patients: you notify the patient from the webhooks below.

Overview

  • A consultation is either async (written review, as today) or sync (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.reminder about 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

WhoWhat
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 is sync automatically; you do not need to send consult_mode.
  • You send "consult_mode": "sync" on POST https://a-nirwan.bespoke-consults.designingit.co/v1/visits or on PUT 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

  1. You create a TRT consultation (it is sync), or you receive consultation.call_requested.
  2. 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.
  3. Your UI shows the patient the days. When the patient picks another day, ask again with &date=YYYY-MM-DD for its slots. The patient picks a time.
  4. Your backend books it: POST /visits/{id}/appointment with the slot id.
  5. You receive appointment.scheduled with booked_by: "partner". The prescriber does not confirm: the call is booked.
  6. 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

Changed on November 10, 2026. Availability used to return only the nearest day after today. It now also lists every free day in 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
QueryMeaning
timezoneRecommended. 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.
dateOptional. 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"
    }
  ]
}
  • days lists every day with a free slot, earliest first, in timezone: today (if a slot is still 2 hours away) up to 14 days ahead. It carries no slot ids.
  • date is the day the slots belong to. timezone says which zone was used, the patient's or the prescriber's.
  • local_starts_at and local_ends_at are in that zone.
  • A date that is full, past, or more than 14 days ahead returns an empty slots, not an error. A badly written date returns 422 on date.
  • If nothing is free in the next 14 days, days is empty, date is null, and slots is empty. Ask again later.
  • slot is 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"
  }'
FieldMeaning
slotRequired. A slot id from availability.
modalityRequired. video or audio.
patient_timezoneOptional IANA zone. Used for local times in responses and webhooks.
patient_location_stateOptional 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 slot from availability, which then offers only the days and slots of the same prescriber. modality is optional and keeps the old one if left out. To change only the type, pick another time: the same time with another modality returns 422. The old appointment is closed with cancel_reason: "rescheduled", and the new one has rescheduled_from_id.
  • Cancel takes reason: patient_request or other. The optional note is 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 …/appointment returns the booked call. GET …/appointments lists every call on the consultation, cancelled ones included, sorted by call start time (calls at the same time in the order they were booked). Follow rescheduled_from_id to 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"
}
FieldValues
statusscheduled, completed (the call took place), no_show (the patient did not join), cancelled
booked_bypartner or admin (Bespoke staff, on the patient's behalf)
cancelled_bypartner, prescriber, admin (Bespoke staff), system
cancel_reasonpatient_request, other, rescheduled, prescriber_unavailable (time off, or the prescriber left), consultation_closed (the consultation was completed, declined, or cancelled)

Webhooks

EventWhen
consultation.call_requestedThe prescriber asked for a call on an async consultation. It is now sync. Ask the patient to pick a time.
appointment.scheduledA call was booked, by you or by Bespoke staff on the patient's behalf (booked_by).
appointment.rescheduledA call moved. previous_appointment has the old time.
appointment.cancelledA 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_showThe prescriber recorded that the patient did not join (from 10 minutes after the start). The consultation stays open, and you may book again.
appointment.reminderThe 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

StatusCodeWhen
403video_visits_disabledVideo visits are not enabled for your account.
404not_foundNo consultation with that id on your account.
404no_appointmentReschedule, cancel, or get with no booked call.
409slot_unavailableThe slot was taken or stopped being free. Fetch availability again.
422validation_failedA 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

  1. Ask Bespoke to enable video visits and webhooks on your account.
  2. Subscribe to consultation.call_requested and the five appointment.* events.
  3. Send questionnaire_type: "trt" for TRT consultations; they need a call and become sync on their own.
  4. Show the patient the days from /availability (with timezone), fetch the slots of the day they pick with date, book the time they pick, and handle 409 by fetching availability again.
  5. On appointment.reminder, remind the patient.
  6. On appointment.scheduled with booked_by: "admin", tell the patient the time Bespoke staff booked.
  7. On appointment.cancelled with cancelled_by: "system", offer the patient a new time.
  8. Send patient_timezone when booking so local times are right.
  9. Let the patient join the call from your site: Joining a call.