Public developer reference Base URL https://n-kumar.bespoke-consults.designingit.co/v1 Updated Sep 28, 2026

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

  1. POST https://n-kumar.bespoke-consults.designingit.co/v1/patients — create/upsert the patient (optional if nested on the visit).
  2. POST https://n-kumar.bespoke-consults.designingit.co/v1/visits — open a consult (pending). Include questionnaire + questionnaire_type, or attach them in the next step.
  3. PUT https://n-kumar.bespoke-consults.designingit.co/v1/visits/{id}/questionnaire — attach or replace answers after your intake form finishes.
  4. GET https://n-kumar.bespoke-consults.designingit.co/v1/visits/{id} — confirm what was stored (optional).
  5. Provider reviews in Bespoke admin → Consultation review.
  6. Wait for a Bespoke provider to review and approve the consult in Bespoke. Partners cannot complete consults via the API.
  7. When a provider approves the consult, Bespoke creates the prescriptions.
  8. GET https://n-kumar.bespoke-consults.designingit.co/v1/prescriptions/{id} — poll fulfilment status.
  9. Pause / activate remaining fills only if pharmacy routing is activated (Bespoke pharmacy channel). Then POST https://n-kumar.bespoke-consults.designingit.co/v1/visits/{id}/prescriptions/pause and …/activate after the consult is completed. If routing returns prescriptions to you, these calls return 403 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://n-kumar.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://n-kumar.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

FieldTypeNotes
typestringRecommended; e.g. glp-1, trt, ed, peptides
versionstringYour schema version, e.g. 1.0
titlestringDisplay title for providers
answersarrayList 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

  1. Questionnaire fields live only on the visit.
  2. Pharmacy transmit uses Rx / patient / shipping / prescriber only — never visit questionnaire, notes, or visit metadata.
  3. Completing a consult is independent of creating an Rx — Bespoke issues fills on approve.

After the questionnaire

  1. Optionally send photo URLs: POST https://n-kumar.bespoke-consults.designingit.co/v1/visits/{id}/images with JSON face_url and/or photo_id_url (or include them on visit create/update).
  2. Assign practitioner_id (PUT /visits/{id} or at create).
  3. Wait for a Bespoke provider to approve. Partners cannot complete consults via the API. You may POST /visits/{id}/cancel only while the consult is pending.
  4. On approve, Bespoke creates the prescriptions for the visit.
  5. 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://n-kumar.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_url and photo_id_url are optional; send one or both.
  • Same fields may be included on POST /visits or PUT /visits/{id}.
  • Re-submit replaces the previous URL for that slot.
  • Resolve later with GET https://n-kumar.bespoke-consults.designingit.co/v1/visits/{id}/images/face (or photo_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.

Open webhook documentation →

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.

Open messaging documentation →

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:

  1. Track those prescriptions with GET /prescriptions
  2. 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://n-kumar.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://n-kumar.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://n-kumar.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://n-kumar.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://n-kumar.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://n-kumar.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://n-kumar.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://n-kumar.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://n-kumar.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://n-kumar.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://n-kumar.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 accepted within seconds when the queue is healthy.
  • shipped typically 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.


  1. Call GET /ping from your deploy pipeline with sandbox credentials.
  2. Optionally GET /products (or /medications) to pick catalog ids.
  3. Optionally POST /patients once per person (upsert by your external_ref).
  4. POST /visits with questionnaire (photo URLs optional); later PUT /visits/{id}.
  5. A Bespoke provider reviews and approves the consult. Bespoke then creates the prescriptions.
  6. Poll GET /prescriptions (filter by patient_ref) or GET /prescriptions/{id} until terminal status.
  7. If failed, show error.message to 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-Id captured in your logs
  • Retries with backoff on 429, 500, and 503
  • 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_id and/or request_id (X-Request-Id)
  • Approximate time of the request (UTC)
  • Whether you are on sandbox or live