Medical messaging Updated Oct 26, 2026

Medical messaging API

Patient ↔ provider messaging for the medical channel only. Your portal keeps customer/support questions on your side; treatment questions come to Bespoke over this API (not an iframe).

Default: disabled. A Bespoke admin must enable Medical messaging on your partner account. Provider replies are delivered to you via the medical_message.created webhook when webhooks are also enabled.

Overview

  • One message thread per consultation, and a separate thread on the patient when there is no consultation yet
  • You post messages from your website/app with a consult id, or with only a patient id
  • Prescribers read and reply in Bespoke’s Messages inbox
  • Replies are pushed back to your callback URL
  • History stays on the patient, and on the consultation when one exists

Enablement

WhoWhat
Bespoke admin Toggle Enable medical messaging API on the partner (off by default).
Bespoke admin Optionally enable outbound webhooks so provider replies reach your URL.
Partner owner Set callback URL under Integration → Webhooks (subscribe to medical_message.created).

If messaging is disabled, POST/GET …/messages returns 403 messaging_disabled.

Flow

On a consultation

  1. Patient asks a treatment question in your app.
  2. Your backend POSTs to Bespoke with the visit id (UUID or your external_visit_id) and the message.
  3. The message appears on that consult for the assigned provider.
  4. The provider replies in Bespoke.
  5. You receive medical_message.created and show the reply in your UI.

With no consultation

  1. Create or update the patient with POST https://s-mkrtchyan.bespoke-consults.designingit.co/v1/patients. Include state (two-letter US code) so a licensed prescriber can open the thread.
  2. Your backend POSTs to /patients/{id}/messages. {id} is the Bespoke patient UUID or your external_ref. Do not create a visit.
  3. The thread appears in the prescriber Messages inbox as a direct message with no consultation.
  4. A prescriber replies in Bespoke.
  5. You receive medical_message.created with visit_id and external_visit_id set to null. Show that reply in the same patient chat.

A prescriber licensed in the patient’s state can open a no-consult thread. The platform’s default provider can open every thread. If state is missing, only the default provider sees it.

Endpoints

GET  https://s-mkrtchyan.bespoke-consults.designingit.co/v1/visits/{visit_id_or_external_ref}/messages
POST https://s-mkrtchyan.bespoke-consults.designingit.co/v1/visits/{visit_id_or_external_ref}/messages

GET  https://s-mkrtchyan.bespoke-consults.designingit.co/v1/patients/{patient_id_or_external_ref}/messages
POST https://s-mkrtchyan.bespoke-consults.designingit.co/v1/patients/{patient_id_or_external_ref}/messages

Same auth headers as the rest of the API: X-Api-Key, X-Api-Secret.

Use the visit routes when the message belongs to a consultation. Use the patient routes when there is no consultation. GET /patients/…/messages returns only messages that were posted on the patient. Messages posted on a visit stay on GET /visits/…/messages.

Post a message (partner → Bespoke)

curl -X POST "https://s-mkrtchyan.bespoke-consults.designingit.co/v1/visits/javier-consult-1001/messages" \
  -H "X-Api-Key: $BESPOKE_KEY" \
  -H "X-Api-Secret: $BESPOKE_SECRET" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Patient asks about injection site redness after week 2.",
    "external_message_id": "msg-7781",
    "sender_type": "patient",
    "sent_at": "2026-08-30T15:04:00Z",
    "metadata": { "source": "patient_app" }
  }'
FieldRequiredNotes
messageyes, unless files are attachedBody text (alias: body). Max 10,000 characters.
attachmentsnoUp to 5 images or PDFs. See Images and PDFs.
external_message_idnoYour idempotency key — re-posts return the same message and do not add more files
sender_typenopatient (default) or partner
sent_atnoISO-8601; defaults to now
metadatanoSmall JSON object for your own keys

Response 201:

{
  "message_id": "…",
  "external_message_id": "msg-7781",
  "direction": "inbound",
  "sender_type": "patient",
  "sender_name": "Patient",
  "body": "Patient asks about injection site redness after week 2.",
  "attachments": [],
  "sent_at": "2026-08-30T15:04:00+00:00",
  "metadata": { "source": "patient_app" },
  "created_at": "…"
}

Images and PDFs

A message can include up to 5 files, with or without text. Send each file as an HTTPS URL you host. We do not accept a multipart upload on these endpoints. Allowed types are JPEG, PNG, GIF, WebP, and PDF. Include filename or content_type so the type is known. If you send both, they must agree.

curl -X POST "https://s-mkrtchyan.bespoke-consults.designingit.co/v1/patients/your-pt-55/messages" \
  -H "X-Api-Key: $BESPOKE_KEY" \
  -H "X-Api-Secret: $BESPOKE_SECRET" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Lab panel from this morning.",
    "external_message_id": "msg-lab-1",
    "sender_type": "patient",
    "attachments": [
      {
        "url": "https://files.yourapp.com/labs/panel.pdf",
        "filename": "panel.pdf",
        "content_type": "application/pdf"
      }
    ]
  }'

The same attachments array works on POST /visits/{id}/messages.

Each stored file comes back as:

{
  "filename": "panel.pdf",
  "content_type": "application/pdf",
  "url": "https://files.yourapp.com/labs/panel.pdf"
}

Files you host keep the URL you sent. When a provider attaches a file in Bespoke, url is a signed link that lasts 7 days. Call GET on the thread again to receive a fresh link. medical_message.created includes the same list. Posting the same external_message_id again returns the original message and does not add files.

Patient thread, no consult

Create the patient first, then post with the same fields as a consult message. No visit is created and no prescription is involved.

curl -X POST "https://s-mkrtchyan.bespoke-consults.designingit.co/v1/patients" \
  -H "X-Api-Key: $BESPOKE_KEY" \
  -H "X-Api-Secret: $BESPOKE_SECRET" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "external_ref": "your-pt-55",
    "first_name": "Nora",
    "last_name": "Ellison",
    "date_of_birth": "1990-04-12",
    "gender": "female",
    "email": "nora@example.com",
    "phone": "3055550148",
    "address": {
      "line1": "1200 Brickell Ave",
      "city": "Miami",
      "state": "FL",
      "postal_code": "33131"
    }
  }'

curl -X POST "https://s-mkrtchyan.bespoke-consults.designingit.co/v1/patients/your-pt-55/messages" \
  -H "X-Api-Key: $BESPOKE_KEY" \
  -H "X-Api-Secret: $BESPOKE_SECRET" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Can someone review my latest question?",
    "external_message_id": "msg-9001",
    "sender_type": "patient",
    "sent_at": "2026-10-23T14:00:00Z",
    "metadata": { "source": "patient_app" }
  }'

Response 201. The body matches a consult message. There is no visit id on it:

{
  "message_id": "…",
  "external_message_id": "msg-9001",
  "direction": "inbound",
  "sender_type": "patient",
  "sender_name": "Patient",
  "body": "Can someone review my latest question?",
  "attachments": [],
  "sent_at": "2026-10-23T14:00:00+00:00",
  "metadata": { "source": "patient_app" },
  "created_at": "…"
}

List that thread:

curl "https://s-mkrtchyan.bespoke-consults.designingit.co/v1/patients/your-pt-55/messages" \
  -H "X-Api-Key: $BESPOKE_KEY" \
  -H "X-Api-Secret: $BESPOKE_SECRET" \
  -H "Accept: application/json"
{
  "patient_id": "…",
  "external_patient_id": "your-pt-55",
  "visit_id": null,
  "external_visit_id": null,
  "messages": [ /* chronological, same message objects as POST */ ]
}
ResultWhen
201Message stored. Posting the same external_message_id again returns the original message and does not create a second row. That id is unique on your account, including ids already used on a visit.
200GET succeeded.
403 messaging_disabledMedical messaging is off for this account.
404No patient with that UUID or external_ref on this account.
422 validation_failedmessage is missing and there are no attachments, a file is not a JPEG, PNG, GIF, WebP, or PDF, or a field is the wrong type. message max length is 10,000 characters.

Pass ?brand_code= on the URL when the same external_ref exists under more than one brand.

List the thread

curl "https://s-mkrtchyan.bespoke-consults.designingit.co/v1/visits/javier-consult-1001/messages" \
  -H "X-Api-Key: $BESPOKE_KEY" \
  -H "X-Api-Secret: $BESPOKE_SECRET" \
  -H "Accept: application/json"
{
  "visit_id": "…",
  "external_visit_id": "javier-consult-1001",
  "messages": [ /* chronological */ ]
}

Provider reply → your webhook

When a provider replies in Bespoke, we store an outbound message and — if webhooks are enabled — POST:

{
  "id": "delivery-uuid",
  "event": "medical_message.created",
  "occurred_at": "…",
  "partner_id": "…",
  "data": {
    "message_id": "…",
    "direction": "outbound",
    "sender_type": "prescriber",
    "sender_name": "Alan Reyes",
    "body": "Apply a cool compress and message us if it worsens.",
    "attachments": [],
    "sent_at": "…",
    "visit_id": "…",
    "external_visit_id": "javier-consult-1001",
    "patient_id": "…",
    "external_patient_id": "javier-pt-55",
    "practitioner_id": "…",
    "external_practitioner_id": "dr-42"
  }
}

Signatures, retries, and auth are the same as other partner callbacks — see webhook documentation.

A reply on a patient thread (no consultation) uses the same event. Match it on patient_id or external_patient_id. visit_id, external_visit_id, practitioner_id, and external_practitioner_id are null. sender_name is the prescriber who replied.

{
  "id": "delivery-uuid",
  "event": "medical_message.created",
  "occurred_at": "…",
  "partner_id": "…",
  "data": {
    "message_id": "…",
    "direction": "outbound",
    "sender_type": "prescriber",
    "sender_name": "Alan Reyes",
    "body": "Happy to answer that here.",
    "attachments": [],
    "sent_at": "…",
    "visit_id": null,
    "external_visit_id": null,
    "patient_id": "…",
    "external_patient_id": "your-pt-55",
    "practitioner_id": null,
    "external_practitioner_id": null
  }
}

Medical record

  • Messages are permanent rows linked to the patient, and to a consultation when one exists.
  • Visible in the prescriber Message center (the Messages inbox and the patient chart). A consult message also stays on that consultation.
  • Not sent to the pharmacy network.

Integration checklist

  1. Ask Bespoke to enable medical messaging on your account.
  2. Enable webhooks and subscribe to medical_message.created.
  3. For a consult thread, send the visit id. For a thread with no consult, create the patient (include state) and POST /patients/{id}/messages.
  4. Use external_message_id for safe retries from your app. It is unique per account across both thread types. A repeat does not add more files.
  5. Send lab PDFs and photos as attachments HTTPS URLs. Download provider files from the url on each attachment. That URL refreshes when you GET the thread.
  6. Render provider replies from medical_message.created. If visit_id is null, attach the reply to the patient chat. Poll GET /patients/{id}/messages or GET /visits/{id}/messages as a backup.

← API reference Webhooks →