Public developer reference Base URL https://bespokeprescriptions.com/v1 Updated Aug 15, 2026

Bespoke Prescription API — Developer Guide

Version 1

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.

What the API does

You submit one prescription for one patient. Bespoke:

  1. Validates your payload
  2. Creates (or reuses) the patient record at the pharmacy network
  3. Transmits the prescription for fulfilment
  4. Updates status as the order is accepted, shipped, and delivered

You track progress by polling GET /prescriptions/{submission_id} (or listing recent submissions). Outbound webhooks to your system are planned for a later version.

Your system  →  POST /v1/prescriptions  →  Bespoke  →  Pharmacy network
Your system  ←  GET  /v1/prescriptions/{id}  ←  status updates

You do not call a separate “create patient” endpoint. Patient creation is handled automatically when the prescription is relayed.


Base URL

https://bespokeprescriptions.com/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

POST /prescriptions must include a unique Idempotency-Key header:

Idempotency-Key: 8f14e45f-ea5e-4f1a-b2c3-9d1e0a7c5b62

Use a UUID, or any stable value unique to that prescription in your system (for example your order id). Keys are scoped to your account and retained for 30 days.

Scenario Behaviour
First request with a key Processed normally
Same key + same body Original response is replayed (Idempotent-Replay: true). No second prescription
Same key + different body 409 idempotency_conflict — use a new key
Validation failure (4xx) Key is not consumed — fix the payload and retry with the same key
Network timeout / no response Safe to retry with the same key
Submission later failed Terminal. Fix data and POST again with a new key

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
POST /prescriptions Submit a prescription
GET /prescriptions/{submission_id} Retrieve one submission
GET /prescriptions List submissions

GET /ping

Smoke-test credentials (useful in deploy pipelines).

curl "https://bespokeprescriptions.com/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

POST /prescriptions

Submit one prescription for one patient. Returns 202 Accepted: the request is queued for relay to the pharmacy network; it is not yet confirmed by the pharmacy.

curl -X POST "https://bespokeprescriptions.com/v1/prescriptions" \
  -H "X-Api-Key: $BESPOKE_KEY" \
  -H "X-Api-Secret: $BESPOKE_SECRET" \
  -H "Idempotency-Key: 8f14e45f-ea5e-4f1a-b2c3-9d1e0a7c5b62" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d @prescription.json

Example request body

{
  "patient": {
    "external_ref": "acme-patient-90210",
    "first_name": "Jane",
    "last_name": "Doe",
    "date_of_birth": "1984-01-16",
    "gender": "female",
    "email": "jane.doe@example.com",
    "phone": "3055550142",
    "address": {
      "line1": "123 Main Street",
      "line2": "Apt 4",
      "city": "Miami",
      "state": "FL",
      "postal_code": "33101"
    }
  },
  "prescriber": {
    "npi": "1104970417",
    "first_name": "Patrick",
    "last_name": "Kavanagh",
    "phone": "+17864964094",
    "address": {
      "line1": "450 Medical Plaza",
      "line2": "",
      "city": "Anthem",
      "state": "AZ",
      "postal_code": "85086"
    }
  },
  "prescription": {
    "medication": "Semaglutide",
    "strength": "0.25mg",
    "directions": "Inject 0.25 mg subcutaneously once weekly",
    "quantity": 4,
    "days_supply": 28,
    "refills": 0
  },
  "shipping_address": {
    "line1": "123 Main Street",
    "line2": "Apt 4",
    "city": "Miami",
    "state": "FL",
    "postal_code": "33101"
  },
  "metadata": {
    "your_order_id": "ACME-88213"
  }
}

Field reference

patient (required)
Field Type Required Rules
external_ref string yes Your patient id. Reuse across submissions for the same person. Max 191
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, or f (any common capitalisation)
email string no Valid email, max 255
phone string yes US phone; punctuation accepted and normalised. Max 50
address object yes See address fields below
patient.address / prescriber.address / shipping_address
Field Type Required Rules
line1 string yes Max 255
line2 string no Max 255
city string yes Max 255
state string yes Exactly 2 letters (US state code), e.g. FL
postal_code string yes Exactly 5 digits
prescriber (required)

You supply the prescribing clinician. Bespoke does not assign a prescriber.

Field Type Required Rules
npi string yes Exactly 10 digits
first_name string yes Max 255
last_name string yes Max 255
phone string yes Prefer E.164, e.g. +17864964094. Max 50
address object yes Same rules as patient address
prescription (required)
Field Type Required Rules
medication string yes Product name only. Do not append strength here
strength string no e.g. 0.25mg. Max 100
directions string yes Sig / directions. Max 1000
quantity number yes Must be greater than zero
days_supply integer no 1–365
refills integer yes 0–11
shipping_address (optional)

Where the medication is shipped. If omitted, the patient address is used. Same field rules as other addresses.

metadata (optional)

Your own key-value bag. Returned unchanged on every read.

Rule Limit
Max keys 10
Values strings, max 255 characters each

Do not put clinical data here. Useful for your internal order id.

Response — 202 Accepted

{
  "submission_id": "b4f0c1d2-9a76-4a1e-8f3b-2c5d7e9a0b11",
  "status": "queued",
  "patient_ref": "acme-patient-90210",
  "medication": "Semaglutide",
  "strength": "0.25mg",
  "quantity": 4,
  "refills": 0,
  "created_at": "2026-08-12T11:14:03+00:00",
  "updated_at": "2026-08-12T11:14:03+00:00",
  "metadata": {
    "your_order_id": "ACME-88213"
  },
  "timeline": [
    {
      "status": "queued",
      "at": "2026-08-12T11:14:03+00:00"
    }
  ]
}

Persist submission_id. It is the only identifier you need for subsequent GETs.


GET /prescriptions/{submission_id}

Fetch one submission by the submission_id returned from POST.

curl "https://bespokeprescriptions.com/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 correct your data and submit again with a new Idempotency-Key.

404 — unknown id, or a submission that belongs to another account / outside retention.


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://bespokeprescriptions.com/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 — create a new submission.

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
404 not_found No such submission for your account Check the id
409 idempotency_conflict Key reused with a different body Use a new key
422 validation_failed One or more fields invalid Fix fields and retry (same key OK)
429 rate_limited Too many requests Honour Retry-After
500 server_error Unexpected failure on our side Retry with the same idempotency key
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 (not as the HTTP response to POST, which was already 202):

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, correct the payload, and submit again with a new Idempotency-Key.


  1. Call GET /ping from your deploy pipeline with sandbox credentials.
  2. On each order, generate a unique Idempotency-Key and POST /prescriptions.
  3. Store submission_id (and X-Request-Id) against your order.
  4. Poll GET /prescriptions/{submission_id} until a terminal status, or list by patient_ref.
  5. If failed, show error.message to your operators, fix data, submit a new request.
  6. If the HTTP call times out, retry the same key — do not invent a second prescription.

Minimal happy-path sequence

GET  /v1/ping
POST /v1/prescriptions          → 202, status=queued, save submission_id
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 submission history and status
  • Inspect failure messages
  • 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 /pingenvironment 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
  • Unique Idempotency-Key per prescription; reuse only for safe retries of the same body
  • submission_id persisted on your order record
  • X-Request-Id captured in your logs
  • Retries with backoff on 429, 500, and 503
  • Failed submissions handled with a new key after fixing error.message
  • 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