Partner webhooks
Outbound callbacks from Bespoke to your HTTPS endpoint when a consultation is created, approved, declined, or needs more information — and when a prescription is returned to your system for pharmacy routing.
Overview
- Bespoke admin enables webhooks for your account.
- You set callback URL + auth in the partner dashboard.
- Prescribers approve / decline / request information on consults.
- Bespoke POSTs a JSON envelope to your URL (queued + retried).
- Your app matches on
external_visit_id/external_patient_idand continues the patient flow.
Enablement
| Who | What |
|---|---|
| Bespoke admin | Toggle Enable outbound webhooks on the partner (off by default). |
| Partner owner | Sees Integration → Webhooks only when enabled; sets URL, auth, and subscribed events. |
If webhooks are disabled, Bespoke queues no deliveries — even if a URL was saved earlier.
Events
| Event | When |
|---|---|
consultation.created |
Consultation created |
consultation.approved |
Consultation approved |
consultation.declined |
Consultation declined |
consultation.needs_information |
Consultation needs information |
prescription.created |
Prescription created (returned to partner) |
medical_message.created |
Medical message created |
Envelope
{
"id": "delivery-uuid",
"event": "consultation.approved",
"occurred_at": "2026-08-30T12:00:00+00:00",
"partner_id": "…",
"data": {
"visit_id": "…",
"external_visit_id": "your-consult-1001",
"external_patient_id": "your-pt-55",
"patient_id": "…",
"status": "completed",
"status_reason_code": null,
"status_reason": null,
"patient_letter": "Hi Dana,\n\nThank you for completing your consultation…",
"practitioner_id": "…",
"external_practitioner_id": "dr-42",
"practitioner": {
"practitioner_id": "…",
"external_practitioner_id": "dr-42",
"npi": "1999999999",
"first_name": "Patrick",
"last_name": "Prescriber",
"phone": "4805550100",
"address": { "line1": "…", "city": "…", "state": "AZ", "postal_code": "85004" },
"licenses": []
},
"prescriptions": [
{
"submission_id": "…",
"external_prescription_id": "…",
"prescriber": { "npi": "1999999999", "first_name": "Patrick", "last_name": "Prescriber" },
"prescription": {
"medication": "Semaglutide",
"strength": "0.25mg",
"directions": "Inject 0.25mg subcutaneously once weekly",
"quantity": 1,
"days_supply": 28,
"refills": 0
}
}
],
"pharmacy_compatible": {
"externalPatientId": "your-pt-55",
"referenceId": "visit-uuid",
"fulfillmentDetails": {
"shippingAddress": {
"addressLine1": "1200 Brickell Ave",
"addressCity": "Miami",
"addressState": "Florida",
"addressZip": "33131"
}
},
"prescriptions": [
{
"rxReferenceId": "…",
"description": "Semaglutide",
"strength": "0.25mg",
"directions": "Inject 0.25mg subcutaneously once weekly",
"quantity": 1,
"totalRefills": 0,
"daysSupply": 28,
"prescribedBy": {
"npi": "1999999999",
"firstName": "Patrick",
"lastName": "Prescriber",
"phoneNumber": "+14805550100"
}
}
]
}
}
}
The envelope above is consultation.approved, the only consult event that
carries clinical output. Fills are created before it is sent, so
data.prescriptions is always populated on approve. Questionnaires and
visit notes are never included.
Status-only events
consultation.created, consultation.needs_information and
consultation.declined tell you which consult changed, to what, and why.
They carry no prescriptions, no pharmacy_compatible, no letter, and no
prescriber detail beyond the id, because nothing has been written yet:
{
"id": "delivery-uuid",
"event": "consultation.declined",
"occurred_at": "2026-08-30T12:00:00+00:00",
"partner_id": "…",
"data": {
"visit_id": "…",
"external_visit_id": "your-consult-1001",
"external_patient_id": "your-pt-55",
"patient_id": "…",
"brand_code": "acme",
"status": "declined",
"status_reason_code": "missing_invalid_image",
"status_reason": "Photo ID was unreadable.",
"practitioner_id": "…",
"external_practitioner_id": "dr-42",
"event_context": "consultation.declined"
}
}
Match on external_visit_id (or visit_id) to find the consult
in your system, and read status with status_reason_code to
know what happened. Codes are listed under
decline & needs-information reasons.
data.patient_letter is the letter the prescriber signed off on for the
patient: plain text with blank lines between paragraphs, ready to show in your app or
email. It is fixed at approve, so it is only on
consultation.approved. The same text is also on
GET /v1/visits/{id} if you would rather poll.
pharmacy_compatible is the e-script clinical shape (medication, strength,
directions, quantity, refills, shipping, prescribedBy) so you can map the
same fields you already use for pharmacy routing — without Bespoke’s fulfilling pharmacy.
For prescription.created with partner pharmacy return, data.prescription
contains the same partner-facing Rx payload (patient, prescriber, medication, days_supply, refills).
See also the API guide for field aliases.
Authentication
| Method | How it is sent |
|---|---|
hmac (recommended) |
X-Bespoke-Signature: sha256=<hex> over the raw request body using your secret |
bearer |
Authorization: Bearer <token> |
none |
No signature or token (not recommended for production) |
Also sent: X-Bespoke-Event, X-Bespoke-Delivery, Content-Type: application/json.
Verify HMAC (Node example)
const crypto = require('crypto');
function verify(rawBody, header, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
Decline & needs-information reasons
Prescribers must pick a coded reason. Codes are returned on the webhook as status_reason_code with free-text status_reason.
| Code | Label |
|---|---|
missing_invalid_image |
Missing / invalid image |
contraindication |
Contraindication |
incomplete_questionnaire |
Incomplete questionnaire |
medical_reason |
Medical reason |
additional_information_required |
Additional information required |
other |
Other |
Pharmacy return (optional)
Some partners (e.g. Javier) do not want Bespoke to relay to the pharmacy network.
When admin sets pharmacy routing to partner_return and webhooks are enabled:
- Approved prescriptions are not sent to Bespoke’s pharmacy.
- You receive
prescription.createdwith the Rx payload. - Your system chooses which pharmacy receives it.
- Pause / activate (
POST /visits/{id}/prescriptions/pauseand…/activate) are not available on this routing. Those endpoints work only when pharmacy routing is activated to send fills through the Bespoke pharmacy channel.
Retries & failures
- Respond with HTTP 2xx to acknowledge.
- Non-2xx / network errors are retried with backoff (multiple attempts).
- Exhausted deliveries are marked failed and visible to Bespoke ops (Partner webhook deliveries).
- Same logical event uses a stable idempotency key — safe to retry without duplicate business effects on our side.
Medical messaging
Patient ↔ provider messaging is a separate product surface (API thread per consult).
Docs: /docs/messaging.
Provider replies use the medical_message.created webhook event above when webhooks are enabled.
Integration checklist
- Ask Bespoke to enable webhooks on your account.
- Open Partner dashboard → Integration → Webhooks and set HTTPS URL + HMAC secret.
- Subscribe to the events you need.
- Verify signatures and store
X-Bespoke-Deliveryfor idempotency. - Match records via your
external_*ids. - If using pharmacy return, confirm
partner_returnrouting with your account manager.