Reference
Orders
Price a basket, place a referral, and follow it. An order is a request for clinical review, not a charge and not a dispatch.
An order is a referral
The order lands aspending_approval, unpaid, with no pharmacy assigned. A clinician reviews it, writes the prescription and routes it. Nothing in this API charges a card or sends anything to a pharmacy.How an order is made#
- Quote. Send one patient, one clinic if your key holds several, and the products and quantities. The response fixes the lines, the amount and a 30 minute expiry.
- Order. Send the quote id. The quote is consumed in the same transaction that creates the order, so it can fund exactly one order.
- Follow. Read one order, or list orders changed since a timestamp. There are no webhooks yet, so polling is the supported path.
- Cancel, while it is still in review. A cancellation is accepted only before approval and only while unpaid.
Create a quote#
/v1/quotesScopes: orders:write, catalog:read, catalog:read:pricingThe Idempotency-Key header is required. Use a new key for each intended quote. A retry with the same key and the same body returns the first quote.
POST /api/partner/v1/quotes
Authorization: Bearer tfk_test_…
Idempotency-Key: quote-2026-11-07-0001
Content-Type: application/json
{
"patient_id": "0b7c…",
"clinic_id": "dd47…",
"items": [
{ "product_id": "0999dff2-506e-4d0d-a17a-c4772d1be9d4", "quantity": 2 }
]
}| Field | Required | Notes |
|---|---|---|
patient_id | Yes | The id returned by POST /v1/patients. The patient must be linked to the clinic the quote is for. |
clinic_id | No | Only when the key holds more than one clinic. It must be one of the key's grants. With one clinic, omit it. |
items | Yes | One to 20 lines. Each line is a product_id and an integer quantity from 1 to 20. A repeated product is refused. |
A product is orderable only when it is active for the clinic in the catalogue and has an agreed price. If any line is not, the whole quote is refused with 400 validation_failed and nothing is created. Check the catalogue first: a product with priced: false cannot be quoted.
The response is 201 for a new quote and 200 for a replay:
{
"ok": true,
"request_id": "…",
"quote": {
"id": "c0f1…",
"status": "open",
"currency": "USD",
"patient_id": "0b7c…",
"clinic_id": "dd47…",
"subtotal_cents": 75200,
"total_cents": 75200,
"expires_at": "2026-11-07T15:30:00.000Z",
"created_at": "2026-11-07T15:00:00.000Z",
"order_id": null,
"lines": [
{
"product_id": "0999dff2-506e-4d0d-a17a-c4772d1be9d4",
"name": "*International - Semax Nasal Spray - 10mg, 15mL",
"quantity": 2,
"unit": "each",
"unit_amount_cents": 37600,
"line_amount_cents": 75200
}
]
},
"idempotent": false
}Place the order#
/v1/ordersScope: orders:writeThe body carries the quote id and nothing else. The quote already names the patient, the clinic, the lines and the amount, so this endpoint never re-prices.
POST /api/partner/v1/orders
Authorization: Bearer tfk_test_…
Idempotency-Key: order-2026-11-07-0001
Content-Type: application/json
{ "quote_id": "c0f1…" }The response is 201 for a new order and 200 for a replay:
{
"ok": true,
"request_id": "…",
"order": {
"id": "8a3d…",
"order_number": "TF-2026-00042",
"status": "pending_approval",
"origin": "partner",
"patient_id": "0b7c…",
"clinic_id": "dd47…",
"quote_id": "c0f1…",
"total_cents": 75200,
"currency": "USD",
"payment_status": "pending",
"item_count": 1,
"created_at": "2026-11-07T15:01:00.000Z",
"updated_at": "2026-11-07T15:01:00.000Z",
"sent_at": null,
"shipped_at": null,
"delivered_at": null,
"tracking_number": null,
"carrier": null,
"ship_status": null,
"pharmacy_name": null
},
"idempotent": false
}One quote, one order
A quote can fund exactly one order, even if the second request uses a different key. A reused quote returns409 quote_consumed. Create a new quote for a second order.Read an order#
/v1/orders/{id}Scope: orders:read, plus orders:read:clinical for linesThe shell carries the number, status, origin, dates, totals, payment status, tracking and pharmacy. Add orders:read:clinical to receive the lines as well.
{
"ok": true,
"request_id": "…",
"order": {
"id": "8a3d…",
"order_number": "TF-2026-00042",
"status": "pending_approval",
"origin": "partner",
"patient_id": "0b7c…",
"clinic_id": "dd47…",
"quote_id": "c0f1…",
"total_cents": 75200,
"currency": "USD",
"payment_status": "pending",
"item_count": 1,
"created_at": "2026-11-07T15:01:00.000Z",
"updated_at": "2026-11-07T15:01:00.000Z",
"sent_at": null,
"shipped_at": null,
"delivered_at": null,
"tracking_number": null,
"carrier": null,
"ship_status": null,
"pharmacy_name": null,
"items": [
{
"id": "…",
"product_id": "0999dff2-506e-4d0d-a17a-c4772d1be9d4",
"medication_name": "*International - Semax Nasal Spray - 10mg, 15mL",
"dosage": "As directed",
"quantity": 2,
"unit": "each",
"refills": 0,
"days_supply": null,
"line_type": "medication",
"unit_amount_cents": 37600,
"line_amount_cents": 75200
}
]
}
}Without orders:read:clinical, the items key is absent, not an empty array.
A key can read every order in its clinics, including one a staff member created. Cancellation is narrower: only a partner-created order can be withdrawn through this API.
List and poll#
/v1/ordersScope: orders:readNewest first, keyset paginated. updated_since is the polling filter: send the last time you checked and receive everything that changed after it. The cursor is where the previous page ended.
| Query | Notes |
|---|---|
limit | 1 to 100. Default 25. |
status | One of draft, pending_approval, approved, sent_to_pharmacy, shipped, delivered, cancelled, on_hold, a2z_cleared. |
updated_since | ISO 8601 instant with a zone, for example 2026-11-07T00:00:00Z. Orders with updated_at at or after it are returned. |
cursor | The next_cursor from the previous response. Treat it as opaque. An unusable cursor is 400, never a silent restart. |
{
"ok": true,
"request_id": "…",
"orders": [ { "id": "8a3d…", "order_number": "TF-2026-00042", "status": "pending_approval", "…": "…" } ],
"truncated": false,
"next_cursor": null
}The list is shell only, even when the key holds orders:read:clinical. Read one order to get its lines.
Cancel#
/v1/orders/{id}/cancelScope: orders:writeAccepted only for a partner-created order while it is draft or pending_approval and unpaid. After approval, after a payment, after a pharmacy has it, or for a staff-created order, the answer is 409 order_not_cancellable; contact the clinic.
{
"ok": true,
"request_id": "…",
"order": {
"id": "8a3d…",
"order_number": "TF-2026-00042",
"status": "cancelled",
"already_cancelled": false
}
}Cancelling twice is 200 with already_cancelled: true. The event and audit rows are written once, for the transition.
Retries and idempotency#
| Situation | Result |
|---|---|
| Same key, same body | The first result, with idempotent: true. No second row. |
| Same key, different body | 409 idempotency_conflict. |
| Different key, same quote | The first order stands; the second is 409 quote_consumed. |
| Key whose request was refused | The key is removed in the same transaction, so you can retry it after fixing the quote or creating a new one. |
Keys are 8 to 128 visible characters with no whitespace. A UUID, a request id or a date-stamped sequence all work. Use a new key for a new intention, not a new key for a retry of the same intention.
Prices and expiry#
A quote is valid for 30 minutes. The amount in the quote is the clinic-agreed amount from the catalogue, fixed at the moment the quote is created.
At order time the platform checks that every line is still active and still priced the same. If the price moved, the order is refused with 409 quote_price_changedrather than billing a different number. Create a new quote and show the new amount.
An expired quote returns 409 quote_expired. A quote that was already used returns 409 quote_consumed.
Order amounts are part of the order. orders:read carries the total, and orders:read:clinical carries the line breakdown. The catalogue's catalog:read:pricing gates the catalogue price, not an order.
Errors#
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_failed | The body, query, idempotency key or one of the quote lines is not valid, or a key holding several clinics did not name one. |
| 403 | forbidden_scope | The key lacks a required scope, or the named clinic is not in its grants. |
| 403 | orders_not_enabled | Production only. The signed BAA and DPA, or the operator's order switch, are not in place. |
| 404 | not_found | The patient is not linked to the clinic, or the quote or order is not in the key's clinics. The same answer for every reason. |
| 409 | idempotency_conflict | The same key was used for a different body. |
| 409 | quote_expired | The 30 minute window passed. Create a new quote. |
| 409 | quote_consumed | The quote already funded an order. |
| 409 | quote_price_changed | An agreed price or the formulary status moved. Create a new quote. |
| 409 | order_not_cancellable | The order is approved, paid, already with a pharmacy, or was created by staff rather than by the partner. |
| 500 | internal_error | Our bug. Report it with the request id. |
| 503 | service_unavailable | The credential or the order gate could not be checked. Retry with backoff. |
What is not returned#
- Patient name, email, phone or address. Use the patient endpoints and the PHI scope.
- The prescription sig, diagnosis codes, internal notes, override reasons and pharmacy cost. Those are clinical or commercial data the order API does not serve.
- Card data, Stripe ids and payment failure messages.
- Orders from a clinic the key does not hold.
Sandbox and production#
Orders are available in the sandbox now. Production is gated three ways: the key must carry the scope, the BAA and DPA must be signed and recorded, and an operator must enable orders for the account after the lawful-supply review. A production key without all three gets 403 orders_not_enabled.
The lawful-supply review is real
The sandbox accepts the sandbox catalogue without a state-by-state supply decision. Live orders do not, and the switch is the point at which that decision is recorded. Do not ask an operator to enable live orders before counsel and the clinical lead have answered which products are lawful in which states.