Medical questionnaire flow
A questionnaire is a treatment-specific medical assessment stored on the visit. It is clinical review data for Bespoke providers — not a prescription, and never sent to the pharmacy network.
End-to-end sequence
POST https://a-nirwan.bespoke-consults.designingit.co/v1/patients— create/upsert the patient (optional if nested on the visit).POST https://a-nirwan.bespoke-consults.designingit.co/v1/visits— open a consult (pending). Includequestionnaire+questionnaire_type, or attach them in the next step.PUT https://a-nirwan.bespoke-consults.designingit.co/v1/visits/{id}/questionnaire— attach or replace answers after your intake form finishes.GET https://a-nirwan.bespoke-consults.designingit.co/v1/visits/{id}— confirm what was stored (optional).- Provider reviews in Bespoke admin → Consultation review.
- Wait for a Bespoke provider to review and approve the consult in Bespoke. Partners cannot complete consults via the API.
- When a provider approves the consult, Bespoke creates the prescriptions.
GET https://a-nirwan.bespoke-consults.designingit.co/v1/prescriptions/{id}— poll fulfilment status.- Pause / activate remaining fills only if pharmacy routing is activated (Bespoke pharmacy channel). Then
POST https://a-nirwan.bespoke-consults.designingit.co/v1/visits/{id}/prescriptions/pauseand…/activateafter the consult is completed. If routing returns prescriptions to you, these calls return403 pharmacy_channel_required. Shipped fills cannot be held.
You can open the visit first without answers, then call
PUT …/questionnaire later. Replacing overwrites the previous JSON and
refreshes questionnaire_submitted_at.
Create a visit with a questionnaire
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" \
-H "Accept: application/json" \
-d '{
"external_visit_id": "consult-1001",
"external_patient_id": "acme-patient-90210",
"questionnaire_type": "glp-1",
"questionnaire": {
"type": "glp-1",
"version": "1.0",
"title": "GLP-1 Medical Assessment",
"answers": [
{ "id": "bmi", "question": "BMI", "answer": "32.1" },
{ "id": "contraindications", "question": "Any contraindications?", "answer": "No" },
{ "id": "goals", "question": "Treatment goals", "answer": "Weight management" }
]
},
"notes": "GLP-1 eligibility review"
}'
Attach or replace later
curl -X PUT "https://a-nirwan.bespoke-consults.designingit.co/v1/visits/consult-1001/questionnaire" \
-H "X-Api-Key: $BESPOKE_KEY" \
-H "X-Api-Secret: $BESPOKE_SECRET" \
-H "Content-Type: application/json" \
-d '{
"questionnaire_type": "glp-1",
"questionnaire": {
"type": "glp-1",
"version": "1.0",
"title": "GLP-1 Medical Assessment",
"answers": [
{ "id": "bmi", "question": "BMI", "answer": "31.4" }
]
}
}'
POST to the same path is accepted as an alias.
Payload shape
| Field | Type | Notes |
|---|---|---|
type | string | Recommended; e.g. glp-1, trt, ed, peptides |
version | string | Your schema version, e.g. 1.0 |
title | string | Display title for providers |
answers | array | List of { id, question, answer } |
Schema is flexible JSON. Keep clinical content here — never on the prescription
(directions, Rx metadata, etc.).
Where providers see it
- Admin → Consultation review — inbox; View shows assessment type + full answers.
- Partner → Visits / Assessments — read-only view of the same questionnaire.
Isolation rules
- Questionnaire fields live only on the visit.
- Pharmacy transmit uses Rx / patient / shipping / prescriber only — never visit questionnaire, notes, or visit metadata.
- Completing a consult is independent of creating an Rx — Bespoke issues fills on approve.
After the questionnaire
- Optionally send photo URLs:
POST https://a-nirwan.bespoke-consults.designingit.co/v1/visits/{id}/imageswith JSONface_urland/orphoto_id_url(or include them on visit create/update). - Assign
practitioner_id(PUT /visits/{id}or at create). - Wait for a Bespoke provider to approve. Partners cannot complete consults via the API. You may
POST /visits/{id}/cancelonly while the consult ispending. - On approve, Bespoke creates the prescriptions for the visit.
- Poll
GET /prescriptions/{submission_id}.
Consult photos (optional)
A consult may include clinical photos as HTTPS URLs:
face (patient face) and photo_id (front of a photo ID).
Partners host the files and send the URLs. Photos are optional and are
not required to mark a consult completed.
Photos stay inside Bespoke for provider review and are never sent to the pharmacy.
Attach photo URLs
curl -X POST "https://a-nirwan.bespoke-consults.designingit.co/v1/visits/consult-1001/images" \
-H "X-Api-Key: $BESPOKE_KEY" \
-H "X-Api-Secret: $BESPOKE_SECRET" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"face_url": "https://cdn.partner.example/patients/pt-1/face.jpg",
"photo_id_url": "https://cdn.partner.example/patients/pt-1/id-front.jpg"
}'
- JSON fields
face_urlandphoto_id_urlare optional; send one or both. - Same fields may be included on
POST /visitsorPUT /visits/{id}. - Re-submit replaces the previous URL for that slot.
- Resolve later with
GET https://a-nirwan.bespoke-consults.designingit.co/v1/visits/{id}/images/face(orphoto_id) — redirects to the partner URL.
Partner callbacks & Rx return
Outbound webhooks are documented on a dedicated page. They are disabled by default — a Bespoke admin enables them per partner, then the partner owner sets the callback URL in Partner dashboard → Integration → Webhooks.
Medical messaging
Patient ↔ provider messaging over API (one thread per consult). Disabled by default;
enable on the partner account, then use POST/GET /v1/visits/{id}/messages.
Bespoke Prescription API — Developer Guide
Version 1.2
This guide is everything you need to integrate with the Bespoke Prescription API: authentication, endpoints, request and response payloads, status lifecycle, errors, and recommended patterns.
External references
Partners should send stable external ids so Bespoke updates existing objects instead of
creating disconnected snapshots. Typed aliases are accepted and returned alongside the
legacy external_ref / brand_code names:
| Partner field | Stored as | Entity |
|---|---|---|
external_patient_id |
external_ref |
Patient |
external_visit_id |
external_ref |
Visit / consult |
external_product_id |
external_product_id |
Pharmacy medication (on Rx) |
external_prescription_id |
external_ref |
Prescription submission |
external_practitioner_id |
external_ref |
Provider |
external_clinic_id |
external_ref |
Clinic |
external_tenant_id |
brand_code |
Brand under the partner account |
Recommended minimum: external_patient_id, external_visit_id, external_product_id.
Look up a prescription with GET /prescriptions/{external_prescription_id}.
What the API does
You submit clinical consults (visits). When a Bespoke provider approves a consult, Bespoke creates the prescriptions for fulfilment. You then:
- Track those prescriptions with
GET /prescriptions - Receive status as the order is accepted, shipped, and delivered
You can also manage patients independently via /v1/patients (create/upsert, retrieve,
update). Reuse the same external_ref on later consults so demographics are not
duplicated.
One partner account can own many brands (sub-companies). Send brand_code on patients
and prescriptions so Bespoke knows which brand every record belongs to. Omitted
brand_code uses the partner's default brand. Hierarchy:
Partner → Brand → Patient → Visit → Prescription
You track progress by polling GET /prescriptions/{submission_id} and/or receiving
outbound partner webhooks (configured per account).
Your system → POST /v1/patients → Bespoke patient record (optional first step)
Your system → POST /v1/visits → Bespoke consult (Open / stored `pending` + questionnaire)
Your system ← webhook consultation.created
Your system → PUT /v1/visits/{id} → notes / clinic / practitioner (no status change)
Prescriber → Completed or Declined → Bespoke consult decision
Your system ← webhook consultation.approved | declined
↳ on approve: fills + doctor details + pharmacy-compatible e-script fields
Your system ← GET /v1/prescriptions/{id} ← status updates (uuid or external_prescription_id)
Partner webhooks (callbacks)
Outbound webhooks are disabled by default. A Bespoke admin enables them on your account; you then configure the callback URL in Partner dashboard → Integration → Webhooks.
Full webhook contract (events, signatures, decline codes, pharmacy return):
/docs/webhooks
Base URL
https://a-nirwan.bespoke-consults.designingit.co/v1
Replace the host with the URL your account manager provides for sandbox or production.
All requests must use HTTPS. Plain HTTP is rejected.
Authentication
Every request requires two headers:
| Header | Example | Notes |
|---|---|---|
X-Api-Key |
bsk_live_7Qd3xR2mVn8pLc4KfW1sTbYz |
Public key id |
X-Api-Secret |
(your secret) | Shown once when issued |
GET /v1/ping HTTP/1.1
Host: api.bespoke.example
X-Api-Key: bsk_live_7Qd3xR2mVn8pLc4KfW1sTbYz
X-Api-Secret: <your secret>
Accept: application/json
Credentials are issued in the Bespoke partner dashboard. The secret cannot be recovered later; if it is lost, rotate the credential and deploy the new pair.
Rotation issues a new pair and keeps the previous pair valid for 24 hours so you can cut over without downtime.
| Situation | HTTP | Code |
|---|---|---|
| Missing or malformed headers | 401 | unauthenticated |
| Unknown key, wrong secret, or revoked | 401 | invalid_credentials |
| Account temporarily paused | 403 | account_paused |
| Account closed | 403 | account_disabled |
Never put credentials in a query string. Never call the API from a browser or mobile app — only from your backend.
Sandbox keys use the bsk_test_ prefix. Live keys use bsk_live_.
Request and response conventions
| Rule | Detail |
|---|---|
| Content type | Content-Type: application/json and Accept: application/json |
| Encoding | UTF-8 |
| Timestamps | ISO 8601, e.g. 2026-08-12T11:14:03Z |
| Request id | Every response includes X-Request-Id. Log it; support needs it |
Idempotency
Write endpoints such as POST /visits and POST /patients upsert by your external ids, so
retrying the same payload updates the existing record instead of creating a duplicate.
Prescriptions are not created via the API.
Rate limits
120 requests per minute per account (default). Over the limit:
- HTTP
429 - Code
rate_limited - Header
Retry-After(seconds)
Back off and retry. Do not busy-loop.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/ping |
Verify credentials |
GET |
/medications |
List pharmacy-supported medications |
GET |
/products |
Alias of /medications (same response) |
POST |
/patients |
Create or update a patient (upsert by external_ref) |
GET |
/patients |
List patients for your account |
GET |
/patients/{external_ref_or_patient_id} |
Retrieve a patient |
PUT |
/patients/{external_ref_or_patient_id} |
Update an existing patient |
POST |
/visits |
Create or update a clinical consult (starts Open, stored pending) |
GET |
/visits |
List consults (?status= / ?patient_ref= / brand) |
GET |
/visits/{external_ref_or_visit_id} |
Retrieve a consult |
PUT |
/visits/{id} |
Update consult fields (notes, clinic, practitioner) — not status |
POST |
/visits/{id}/status |
Partners: withdraw an Open consult only while stored pending |
POST |
/visits/{id}/complete |
Not available to partners (provider review only) |
POST |
/visits/{id}/cancel |
Withdraw an Open consult only while stored pending |
PUT / POST |
/visits/{id}/questionnaire |
Attach or replace medical questionnaire |
POST |
/visits/{id}/images |
Attach optional photo URLs (face_url and/or photo_id_url) |
GET |
/visits/{id}/images/{slot} |
Resolve one photo (face or photo_id) — redirects to partner URL |
GET |
/visits/{id}/messages |
List medical messages on a consult (requires messaging enabled) |
POST |
/visits/{id}/messages |
Post a medical message into the consult thread |
POST |
/clinics |
Create or update a clinic (upsert by external_ref) |
GET |
/clinics |
List clinics |
GET |
/clinics/{external_ref_or_clinic_id} |
Retrieve a clinic |
POST |
/practitioners |
Create or update a practitioner (upsert by NPI / external_ref) |
GET |
/practitioners |
List practitioners |
GET |
/practitioners/{external_ref_or_practitioner_id} |
Retrieve a practitioner |
GET |
/prescriptions/{submission_id_or_external_prescription_id} |
Retrieve one submission |
GET |
/prescriptions |
List submissions (?patient_ref= / ?status= / ?brand_code=) |
POST |
/visits/{id}/prescriptions/pause |
Hold remaining fills on a completed consult — only if pharmacy routing is activated (Bespoke channel) |
POST |
/visits/{id}/prescriptions/activate |
Release held fills and send them to the pharmacy again |
POST |
/prescriptions/{id}/pause |
Hold one fill on a completed consult |
POST |
/prescriptions/{id}/activate |
Release one held fill |
GET /ping
Smoke-test credentials (useful in deploy pipelines).
curl "https://a-nirwan.bespoke-consults.designingit.co/v1/ping" \
-H "X-Api-Key: $BESPOKE_KEY" \
-H "X-Api-Secret: $BESPOKE_SECRET" \
-H "Accept: application/json"
200 OK
{
"status": "ok",
"account": "Acme Health",
"environment": "live",
"server_time": "2026-08-12T11:14:03+00:00"
}
| Field | Meaning |
|---|---|
environment |
sandbox for bsk_test_ keys, otherwise live |
account |
Your partner account display name |
GET /medications
List medications the pharmacy network supports (and, when you have linked partner catalog products, only those linked items).
Query params: q (search name / strength / external id), brand_code / brand_id
(limit to a brand's linked catalog).
curl "https://a-nirwan.bespoke-consults.designingit.co/v1/medications?q=sema" \
-H "X-Api-Key: $BESPOKE_KEY" \
-H "X-Api-Secret: $BESPOKE_SECRET" \
-H "Accept: application/json"
200 OK
{
"data": [
{
"medication_id": "3f2a1c8e-9b4d-4e6a-8f1c-2d7e9a0b5c4d",
"external_product_id": "prx-sema-025",
"name": "Semaglutide",
"strength": "0.25mg",
"ndc": null,
"default_directions": "Inject 0.25 mg subcutaneously once weekly"
}
]
}
Use external_product_id on consults when identifying a catalog product.
GET /products
Identical to GET /medications (path alias for partners that call the catalog “products”).
POST /patients
Create or update a standalone patient for your account. Identity is your external_ref
(unique per account). Sending the same external_ref again updates demographics instead of
creating a duplicate.
Returns 201 Created on first insert, 200 OK on update.
curl -X POST "https://a-nirwan.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": "acme-patient-90210",
"first_name": "Dana",
"last_name": "Whitfield",
"date_of_birth": "1986-04-11",
"gender": "female",
"email": "dana@example.com",
"phone": "3055551234",
"address": {
"line1": "123 Main Street",
"line2": "Apt 4",
"city": "Miami",
"state": "FL",
"postal_code": "33101"
}
}'
201 / 200 response
{
"patient_id": "3f2a1c8e-9b4d-4e6a-8f1c-2d7e9a0b5c4d",
"external_ref": "acme-patient-90210",
"brand_id": "7c1e9a2b-4d5f-4a8e-9c3b-1f0e2d4a6b8c",
"brand_code": "brand-a",
"first_name": "Dana",
"last_name": "Whitfield",
"date_of_birth": "1986-04-11",
"gender": "f",
"email": "dana@example.com",
"phone": "305-555-1234",
"address": {
"line1": "123 Main Street",
"line2": "Apt 4",
"city": "Miami",
"state": "FL",
"postal_code": "33101"
},
"created_at": "2026-08-12T11:14:03+00:00",
"updated_at": "2026-08-12T11:14:03+00:00"
}
| Field | Type | Required | Rules |
|---|---|---|---|
external_ref |
string | yes | Your patient id. Unique per brand. Max 191 |
brand_code |
string | no | Your brand/sub-company code (configured by Bespoke). Omitting uses the default brand |
brand_id |
string (uuid) | no | Alternative to brand_code |
first_name |
string | yes | Max 255 |
last_name |
string | yes | Max 255 |
date_of_birth |
string | yes | YYYY-MM-DD, must be before today |
gender |
string | yes | male / female / m / f (case-insensitive) |
email |
string | no | Valid email |
phone |
string | yes | US phone; punctuation is fine |
address |
object | yes | Same address rules as prescriptions |
Response patient_id is the Bespoke patient UUID. Use either patient_id or external_ref
on later GET/PUT calls (add ?brand_code= when the same ref exists under multiple brands).
Reuse the same external_ref + brand_code on later consults.
GET /patients/{external_ref_or_patient_id}
Retrieve one patient owned by your account. Path may be your external_ref or the Bespoke
patient_id UUID.
curl "https://a-nirwan.bespoke-consults.designingit.co/v1/patients/acme-patient-90210" \
-H "X-Api-Key: $BESPOKE_KEY" \
-H "X-Api-Secret: $BESPOKE_SECRET" \
-H "Accept: application/json"
200 OK — same body as POST /patients. 404 not_found if unknown or owned by another account.
PUT /patients/{external_ref_or_patient_id}
Replace demographics for an existing patient. Path identifies the patient (external_ref or
patient_id). Body uses the same fields as POST /patients except external_ref is optional
(identity cannot be changed via the body).
200 OK on success. 404 if the patient does not exist for your account.
GET /patients
List patients for your account. Optional brand_code / brand_id to scope to one brand.
POST /visits
Open a clinical consult before sending a prescription. It starts Open
(stored pending). Completing the consult is a separate explicit action — submitting
an Rx never marks the visit Completed.
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_ref": "consult-1001",
"patient_ref": "acme-patient-90210",
"notes": "GLP-1 eligibility review"
}'
| Field | Type | Required | Rules |
|---|---|---|---|
external_ref |
string | no | Your consult id; unique per brand |
brand_code / brand_id |
string | no | Same brand rules as patients |
patient_ref / patient_id |
string | conditional | Existing patient |
patient |
object | conditional | Nested patient upsert if not referencing one |
clinic_id / clinic |
uuid / object | no | Optional clinic |
practitioner_id |
uuid | no | Reviewing provider (omit to auto-assign Patrick when partner auto-assign is on) |
notes |
string | no | |
requested_product |
string | no | semaglutide or tirzepatide (GLP-1; enables default Rx ladder on approve) |
pharmacy_medication_id |
uuid | no | Optional catalog strength UUID |
questionnaire_type |
string | no | e.g. glp-1, trt, ed, peptides |
questionnaire |
object | no | Full assessment answers (see below) |
metadata |
object | no | Max 10 string values |
Medical questionnaire (clinical — Bespoke only): send on POST /visits or
PUT /visits/{id}/questionnaire. Providers review answers in the admin Visits screen
before completing the consult and issuing a prescription.
Never sent to the pharmacy network. The pharmacy receives only the resulting
prescription/order fields. Do not put questionnaire content in directions, metadata
on prescriptions, or any pharmacy payload.
Example questionnaire body:
{
"type": "glp-1",
"version": "1.0",
"title": "GLP-1 Medical Assessment",
"answers": [
{ "id": "bmi", "question": "BMI", "answer": "32.1" },
{ "id": "contraindications", "question": "Any contraindications?", "answer": "No" }
]
}
Consultations have three statuses. The JSON status field is the stored value:
| Status | Meaning | Stored status |
|---|---|---|
| Open | Awaiting or in review | pending or in_progress |
| Completed | Provider finished the consult | completed |
| Declined | Provider declined | declined |
Opening the consult in /prescriber may store in_progress; the status stays Open.
needs_information, cancelled, and abandoned are not product statuses.
Partners may withdraw an Open consult only while it is still stored pending
(cancelled on POST /visits/{id}/status). That is a partner API action, not a fourth
consultation status.
PUT /visits/{id}
Update consult fields without changing status: notes, clinic_id / nested clinic,
practitioner_id, metadata. Status changes use the endpoints below.
POST /visits/{id}/status
Partners may only withdraw an Open consult while it is still stored pending:
{ "status": "cancelled", "reason": "Patient withdrew" }
Partners cannot set Completed or Declined. A Bespoke provider reviews and completes
or declines in the consult UI. After a provider has opened the consult (stored in_progress)
or closed it, partners cannot withdraw it.
Terminal states cannot be reopened. Creating a prescription never changes visit status.
POST /visits/{id}/complete
Not available to partners. Completing (approving) a consult is a Bespoke provider action.
POST /visits/{id}/cancel
Shortcut for { "status": "cancelled" }. Works only while the consult is Open and still
stored pending. Optional body: { "reason": "…" }.
PUT /visits/{id}/questionnaire
Attach or replace the medical assessment on an existing consult (same questionnaire /
questionnaire_type fields as create). POST to the same path is accepted as an alias.
Clinical data stays on the visit for provider review and is never relayed to the pharmacy.
POST /visits/{id}/images
Attach optional clinical photos as partner-hosted HTTPS URLs. Send one or both:
| Field | Type | Required | Rules |
|---|---|---|---|
face_url |
string (URL) | no | Absolute http/https URL, max 2048 chars |
photo_id_url |
string (URL) | no | Absolute http/https URL — front of government/photo ID |
Aliases also accepted: face / photo_id, or nested under images (images.face, images.photo_id, …).
The same fields may be sent on POST /visits or PUT /visits/{id}.
Photos are not required to complete a consult. Re-submitting replaces the previous URL for that slot. Photos are clinical review data only — never sent to the pharmacy network.
curl -X POST "https://a-nirwan.bespoke-consults.designingit.co/v1/visits/consult-1001/images" \
-H "X-Api-Key: $BESPOKE_KEY" \
-H "X-Api-Secret: $BESPOKE_SECRET" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"face_url": "https://cdn.partner.example/patients/pt-1/face.jpg",
"photo_id_url": "https://cdn.partner.example/patients/pt-1/id-front.jpg"
}'
Visit responses include:
{
"images_complete": true,
"images": {
"face": {
"slot": "face",
"url": "https://cdn.partner.example/patients/pt-1/face.jpg",
"uploaded_at": "2026-08-28T12:00:00+00:00"
},
"photo_id": {
"slot": "photo_id",
"url": "https://cdn.partner.example/patients/pt-1/id-front.jpg",
"uploaded_at": "2026-08-28T12:00:00+00:00"
}
}
}
GET /visits/{id}/images/{slot}
Resolve one photo (face or photo_id) using the same API credentials.
Responds with a redirect to the partner-hosted source_url.
GET /visits/{id}/messages · POST /visits/{id}/messages
Medical messaging thread for one consult (patient ↔ provider). Requires medical messaging enabled on your partner account (admin toggle; off by default).
POST body:
| Field | Required | Notes |
|---|---|---|
message |
yes | Body text (alias: body) |
external_message_id |
no | Your idempotency id |
sender_type |
no | patient (default) or partner |
sent_at |
no | ISO-8601; defaults to now |
metadata |
no | Small JSON object |
Provider replies in the Bespoke Message center; when webhooks are enabled you receive
medical_message.created. Full contract: /docs/messaging.
GET /visits / GET /visits/{id}
List (filter ?status= / ?patient_ref= / brand) or retrieve one consult. ?status=
uses the stored value (pending, in_progress, completed, declined). Product
statuses are Open (pending / in_progress), Completed, and Declined.
The visit response includes questionnaire_type, questionnaire, and
questionnaire_submitted_at when present.
POST /clinics
Create or update a clinic under your brand. Upserts by external_ref when provided.
| Field | Type | Required | Rules |
|---|---|---|---|
brand_code / brand_id |
string | no | Same brand rules as patients |
external_ref |
string | no | Your clinic id; unique per brand |
name |
string | yes | |
phone |
string | no | |
address |
object | yes | Same address rules as patients |
Response includes clinic_id, external_ref, brand fields, name, phone, address.
GET /clinics / GET /clinics/{id}
List or retrieve clinics for your account. Lookup by clinic_id UUID or external_ref.
POST /practitioners
Create or update a provider / practitioner. Upserts by npi under the brand.
Must be linked to a clinic via clinic_id when known. Include state licenses where required.
| Field | Type | Required | Rules |
|---|---|---|---|
brand_code / brand_id |
string | no | |
external_ref |
string | no | Your external provider id |
clinic_id |
string (uuid) | no | Existing clinic |
npi |
string | yes | Exactly 10 digits; unique per brand |
first_name / last_name |
string | yes | |
phone |
string | yes | |
address |
object | no | Defaults from clinic when omitted |
licenses |
array | no | State licenses (see below) |
licenses[].state |
string | yes | 2-letter US state |
licenses[].license_number |
string | yes | |
licenses[].expires_at |
date | no | YYYY-MM-DD |
licenses[].is_active |
bool | no | Default true |
Response includes practitioner_id (internal public uuid), external_ref, clinic, NPI, name,
phone, address, and licenses.
Associate the provider with a consult via practitioner_id on POST /visits. Completing a
consult requires a provider. When a prescription is issued for that visit, the same
practitioner is returned on the submission (and may be inherited from the visit if you omit
prescriber / practitioner_id on the Rx).
GET /practitioners / GET /practitioners/{id}
List or retrieve practitioners. Lookup by practitioner_id UUID or external_ref.
Prescriptions
Partners cannot create prescriptions via the API. When a Bespoke provider approves a consult, Bespoke creates the fills for that visit (count is set on your account). Use GET to list and poll those submissions.
GET /prescriptions/{submission_id_or_external_prescription_id}
Retrieve one submission by Bespoke submission_id (UUID) or your
external_prescription_id (stored as external_ref). Optional ?brand_code= /
?brand_id= when looking up by external id under a multi-brand account.
Fetch one submission by Bespoke submission_id (UUID) or your
external_prescription_id.
curl "https://a-nirwan.bespoke-consults.designingit.co/v1/prescriptions/b4f0c1d2-9a76-4a1e-8f3b-2c5d7e9a0b11" \
-H "X-Api-Key: $BESPOKE_KEY" \
-H "X-Api-Secret: $BESPOKE_SECRET" \
-H "Accept: application/json"
200 OK (shipped example)
{
"submission_id": "b4f0c1d2-9a76-4a1e-8f3b-2c5d7e9a0b11",
"status": "shipped",
"patient_ref": "acme-patient-90210",
"medication": "Semaglutide",
"strength": "0.25mg",
"quantity": 4,
"refills": 0,
"tracking": {
"carrier": "UPS",
"number": "1Z999AA10123456784"
},
"timeline": [
{ "status": "queued", "at": "2026-08-12T11:14:03+00:00" },
{ "status": "accepted", "at": "2026-08-12T11:14:19+00:00" },
{ "status": "shipped", "at": "2026-08-13T16:02:44+00:00" }
],
"created_at": "2026-08-12T11:14:03+00:00",
"updated_at": "2026-08-13T16:02:44+00:00",
"metadata": {
"your_order_id": "ACME-88213"
}
}
200 OK (failed example)
{
"submission_id": "b4f0c1d2-9a76-4a1e-8f3b-2c5d7e9a0b11",
"status": "failed",
"patient_ref": "acme-patient-90210",
"medication": "Semaglutide",
"strength": "0.25mg",
"quantity": 4,
"refills": 0,
"error": {
"code": "pharmacy_rejected",
"message": "Prescriber NPI is invalid. Fields: {\"prescribedBy.npi\":\"must be a valid NPI\"}"
},
"timeline": [
{ "status": "queued", "at": "2026-08-12T11:14:03+00:00" },
{ "status": "failed", "at": "2026-08-12T11:14:25+00:00" }
],
"created_at": "2026-08-12T11:14:03+00:00",
"updated_at": "2026-08-12T11:14:25+00:00",
"metadata": {
"your_order_id": "ACME-88213"
}
}
| Response field | When present |
|---|---|
tracking |
After shipment, when a tracking number is available |
error |
When status is failed (and on some intermediate failures while retrying) |
error.message is actionable detail from the pharmacy network (vendor product names removed).
Use it to understand why fulfilment failed.
404 — unknown id, or a submission that belongs to another account / outside retention.
Pause and activate fills (pharmacy routing required)
These endpoints work only when pharmacy routing is activated on your account so prescriptions go through the Bespoke pharmacy channel (admin: Route to pharmacy network).
If routing is off, or your account is set to return prescriptions to you
(partner_return), pause and activate are not available. Those calls return
403 pharmacy_channel_required. Ask your account manager to turn on
pharmacy routing if you need this.
The consult must already be completed. Shipped and delivered fills cannot be held.
POST /visits/{id}/prescriptions/pause
Hold remaining fills on a completed consult (pharmacy routing must be on).
Queued fills are held in Bespoke and are not relayed. Accepted (not yet shipped)
fills are cancelled at the pharmacy and marked paused. Shipped and delivered
fills are skipped.
curl -X POST "https://a-nirwan.bespoke-consults.designingit.co/v1/visits/VISIT_ID/prescriptions/pause" \
-H "X-Api-Key: $BESPOKE_KEY" \
-H "X-Api-Secret: $BESPOKE_SECRET" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"reason": "Patient asked to hold remaining fills"}'
Optional prescription_id (UUID or your external_prescription_id) limits the
call to one fill. reason is optional.
200
{
"visit_id": "…",
"action": "paused",
"updated": 2,
"unchanged": 0,
"skipped": 1,
"prescriptions": [
{ "submission_id": "…", "status": "paused" }
]
}
403 pharmacy_channel_required — pharmacy routing is not activated for
the Bespoke pharmacy channel (account is partner_return, or fills are not
sent through us).
422 — consult is not completed, or every fill has already shipped / been delivered.
POST /visits/{id}/prescriptions/activate
Release paused fills. They return to queued and are sent to the pharmacy
again. Fills that were accepted (and therefore cancelled at the pharmacy when
paused) are submitted as a new pharmacy order.
Same optional prescription_id as pause.
curl -X POST "https://a-nirwan.bespoke-consults.designingit.co/v1/visits/VISIT_ID/prescriptions/activate" \
-H "X-Api-Key: $BESPOKE_KEY" \
-H "X-Api-Secret: $BESPOKE_SECRET" \
-H "Accept: application/json"
POST /prescriptions/{id}/pause and POST /prescriptions/{id}/activate do the
same thing for a single fill and return that submission.
GET /prescriptions
List your submissions, newest first. Scoped to your account and retention window (default 90 days).
| Query parameter | Type | Notes |
|---|---|---|
status |
string | Filter by status value |
patient_ref |
string | Filter by your patient.external_ref |
created_after |
date / datetime | Inclusive lower bound |
created_before |
date / datetime | Inclusive upper bound |
per_page |
integer | 1–100, default 25 |
cursor |
string | Opaque cursor from meta.next_cursor |
curl "https://a-nirwan.bespoke-consults.designingit.co/v1/prescriptions?status=failed&per_page=25" \
-H "X-Api-Key: $BESPOKE_KEY" \
-H "X-Api-Secret: $BESPOKE_SECRET" \
-H "Accept: application/json"
{
"data": [
{
"submission_id": "b4f0c1d2-9a76-4a1e-8f3b-2c5d7e9a0b11",
"status": "failed",
"patient_ref": "acme-patient-90210",
"medication": "Semaglutide",
"strength": "0.25mg",
"quantity": 4,
"refills": 0,
"error": {
"code": "pharmacy_rejected",
"message": "…"
},
"timeline": […],
"created_at": "2026-08-12T11:14:03+00:00",
"updated_at": "2026-08-12T11:14:25+00:00",
"metadata": {}
}
],
"meta": {
"per_page": 25,
"next_cursor": "eyJpZCI6NDgyMX0"
}
}
Pass cursor=<next_cursor> for the next page. When next_cursor is null, you are done.
Status lifecycle
queued → relaying → accepted → shipped → delivered
↘ failed
shipped → voided
(admin) → cancelled
| Status | Meaning | Terminal? |
|---|---|---|
queued |
Accepted by Bespoke; waiting for relay | No |
relaying |
Relay job in progress | No |
accepted |
Pharmacy network accepted the prescription | No |
shipped |
Dispensed and shipped; tracking often present | No |
delivered |
Delivered to the patient | Yes |
voided |
Shipment voided before delivery | Yes |
cancelled |
Cancelled by Bespoke operations | Yes |
failed |
Could not be relayed after retries | Yes |
Polling guidance
- Poll no more than once per minute per submission.
- Most submissions reach
acceptedwithin seconds when the queue is healthy. shippedtypically follows within a business day once fulfilment starts.- After
failed, do not keep polling the same id for recovery.
Errors
HTTP error shape
All API errors use one envelope:
{
"error": {
"code": "validation_failed",
"message": "The submission could not be processed because some fields are invalid.",
"request_id": "5c2a1f80-6f0e-4d3a-9d1b-77a0c2e6b4f9",
"fields": {
"patient.date_of_birth": ["Must be a valid date in the format YYYY-MM-DD."],
"prescription.quantity": ["Must be greater than zero."]
}
}
}
fields appears only on 422 validation_failed.
HTTP status codes
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 400 | malformed_request |
Body is not valid JSON | Fix the request |
| 401 | unauthenticated |
Credential headers missing | Add X-Api-Key / X-Api-Secret |
| 401 | invalid_credentials |
Key or secret wrong, or revoked | Check your secret store |
| 403 | account_paused |
Account temporarily paused | Contact your account manager |
| 403 | account_disabled |
Account closed | Contact your account manager |
| 403 | pharmacy_channel_required |
Pause/activate only for Bespoke pharmacy routing | Use your own pharmacy tools, or ask to switch routing |
| 404 | not_found |
No such submission for your account | Check the id |
| 410 | endpoint_gone |
This endpoint is no longer available | Stop calling it; use a documented endpoint |
| 422 | validation_failed |
One or more fields invalid | Fix fields and retry |
| 429 | rate_limited |
Too many requests | Honour Retry-After |
| 500 | server_error |
Unexpected failure on our side | Retry later |
| 503 | pharmacy_unavailable |
Pharmacy network unreachable | Retry later; submission may already be queued |
Submission-level failure codes
These appear on a failed submission under error.code:
| Code | Meaning |
|---|---|
pharmacy_rejected |
Pharmacy network declined the prescription |
patient_rejected |
Patient could not be created downstream |
prescriber_invalid |
Prescriber details were not accepted |
relay_failed |
Repeated delivery failures with no successful relay |
pharmacy_unavailable |
Downstream unavailable after retries exhausted |
Read error.message for fulfilment failures.
Recommended integration flow
- Call
GET /pingfrom your deploy pipeline with sandbox credentials. - Optionally
GET /products(or/medications) to pick catalog ids. - Optionally
POST /patientsonce per person (upsert by yourexternal_ref). POST /visitswith questionnaire (photo URLs optional); laterPUT /visits/{id}.- A Bespoke provider reviews and approves the consult. Bespoke then creates the prescriptions.
- Poll
GET /prescriptions(filter bypatient_ref) orGET /prescriptions/{id}until terminal status. - If
failed, showerror.messageto your operators.
Minimal happy-path sequence
GET /v1/ping
GET /v1/products → catalog (optional)
POST /v1/patients → 201 (optional; upsert by external_ref)
POST /v1/visits → 201 Open / stored pending (+ questionnaire; photo URLs optional)
provider approves consult
GET /v1/prescriptions → fills created on approve
GET /v1/prescriptions/{id} → status=relaying | accepted | …
GET /v1/prescriptions/{id} → status=shipped (+ tracking)
GET /v1/prescriptions/{id} → status=delivered
Partner dashboard
In addition to the API, your team can sign in to the partner dashboard to:
- View Patients, Visits / consults, Assessments (questionnaires), and Prescriptions
- Browse Products, Brands, Clinics, and Practitioners
- Inspect failure messages and submission status history
- Rotate API credentials
Dashboard access is separate from API keys (email/password for your users). Ask your account manager to invite operators.
Sandbox vs live
| Sandbox | Live | |
|---|---|---|
| Key prefix | bsk_test_ |
bsk_live_ |
GET /ping → environment |
sandbox |
live |
| Fulfilment | Test / non-production pharmacy path | Real fulfilment |
Use sandbox credentials until your payloads consistently reach accepted. Then switch to live
keys and the production base URL your account manager provides.
Integration checklist
- Credentials stored in a secret manager (never in source control)
- All calls made server-to-server over HTTPS
- Consults submitted with questionnaire (photo URLs optional)
-
X-Request-Idcaptured in your logs - Retries with backoff on
429,500, and503 - Polling capped (≤ 1/minute per submission)
- Tested end-to-end against sandbox before go-live
Support
Contact your account manager, or email support@bespoke.example.
Always include:
submission_idand/orrequest_id(X-Request-Id)- Approximate time of the request (UTC)
- Whether you are on sandbox or live