List orders
curl "https://api.affinityrx.com/v1/orders?status=shipped&limit=25" \
-H "Authorization: Bearer $AFFINITY_API_KEY" \
-H "Affinity-Version: 2026-10-28"The list contains the orders of your organization in the mode of your key, newest first. Each response reads one snapshot. All returned orders match your filters in that snapshot. Each page reads a new snapshot.
| Parameter | Result |
|---|---|
practiceId | Limit results to the practice with this prac_ ID, within your authorized practices. |
externalOrderId | Match the external order ID exactly, including case. Combine with practiceId when your platform reuses IDs across practices. |
updatedAfter | Return orders whose updatedAt is strictly later than this ISO 8601 timestamp. Sorting stays by creation time and ID unless sort is supplied. Keep this cutoff unchanged while paging. |
patientId | Limit results to the patient with this pat_ ID. |
status | Return only orders with this status. Omit to include all statuses. |
cancelRequested | true returns only submitted or processing orders with a cancel the pharmacy has not confirmed yet (cancelRequestedAt set). false leaves those orders out. Omit to include both. |
recentlyShipped | true returns only orders whose status is shipped and that shipped in the last 60 days; delivered orders are excluded. false leaves those orders out. Omit to include both. |
query | Search saved order snapshots: patient names, email, phone and external ID; order external ID; prescriber name and NPI; medication names and brands; pharmacy name. Whole ord_ and pat_ IDs and six-character order numbers also match. Text matches ignore case and allow substrings, word prefixes and similar spellings for alphanumeric words of four or more characters. Every one of the first eight words containing letters or numbers must match; punctuation-only words are ignored. An empty or punctuation-only query leaves the list unfiltered. Date of birth, separate strength fields, practice names, prac_ IDs and rx_ IDs are not searched. Restart pagination when filters change. |
sort | patient sorts the lowercase text firstName + space + lastName A to Z, then order ID; created sorts oldest first. A leading - reverses the order. Defaults to -created. Restart pagination after changing sort. |
limit | Maximum records per page, from 1 to 100. Defaults to 25. |
startingAfter | Page forward using the previous response's nextCursor. Keep the same filters, sort, organization and mode. Use only one cursor parameter. |
endingBefore | Page backward before this opaque cursor. Keep the same filters, sort, organization and mode. Send either startingAfter or endingBefore. |
sort=patient sorts the saved patient’s first and last name as one lowercase string, first name first.
The order ID breaks ties. sort=-patient reverses both. sort=created sorts oldest first; the default is -created.
query searches the saved order snapshot:
- Patient names, email, phone and external ID; order external ID.
- Prescriber name and NPI; medication names and brands; pharmacy name.
- Whole
ord_andpat_IDs. - Six-character order numbers and parts of those numbers, including prefixes.
Text matches ignore case and support substrings, word prefixes and similar spellings for alphanumeric words of four or more characters.
Every one of the first eight words containing letters or numbers must match. Punctuation-only words are ignored.
An empty or punctuation-only query leaves the list unfiltered.
Search does not match date of birth, separate strength fields, practice names, prac_ IDs or rx_ IDs.
The response is a list: { "object": "list", "url", "data", "hasMore", "nextCursor" }.
Responses carry only nextCursor.
When hasMore is true, send nextCursor as startingAfter to get the next page.
endingBefore takes a cursor you saved while paging forward.
Use the same filters on each page. A cursor with different filters returns 400 invalid_cursor.
To recover after missed webhooks, poll GET /v1/orders with a saved updatedAfter cutoff.
Follow every nextCursor. Keep the cutoff fixed for the whole traversal.
The default order stays newest creation time first, with the order ID as a tie-breaker.
Updates do not move that cursor.
Deduplicate each returned order by its order ID and updatedAt.
A slow transaction can commit an update with a slightly earlier updatedAt than a later-committed
change.
After all pages finish, save the run’s start time as the next cutoff.
For the next poll, send that cutoff minus one minute as updatedAfter.
The one-minute overlap catches those late commits.
Keep your clock synchronized.
Increase the margin if clock skew can exceed one minute.
Repeated orders are expected.
Use externalOrderId for an exact lookup after an uncertain create response.
curl "https://api.affinityrx.com/v1/orders?updatedAfter=2026-10-21T00%3A00%3A00Z&limit=25" \
-H "Authorization: Bearer $AFFINITY_API_KEY" \
-H "Affinity-Version: 2026-10-28"Read an order
Send GET /v1/orders/{orderId}. The response is the whole order, the same as the
create response. An order of another organization returns
404.
statusHistory lists every status of the order, oldest first:
"statusHistory": [
{ "status": "submitted", "createdAt": "2026-10-28T15:04:05.000Z" },
{ "status": "processing", "createdAt": "2026-10-28T16:10:00.000Z" },
{ "status": "shipped", "createdAt": "2026-10-29T18:30:00.000Z" }
]shipments lists the packages, oldest first. A split shipment or a reship adds a shipment. A voided
label removes it. carrier and trackingUrl can be null.
Each shipment’s trackingUrl is the carrier’s own tracking page for that package, with the
tracking number filled in (FedEx, UPS, USPS and OnTrac). For another carrier it is the pharmacy’s
tracking link, or null. The order’s trackingUrl is the newest package’s link, or null before
the order ships. You can send these links to the patient.
The order status comes from the pharmacy. Affinity does not change the status from carrier scans:
an order becomes delivered when the pharmacy reports the delivery.
"shipments": [
{ "carrier": "UPS", "trackingNumber": "1Z999AA10123456784", "trackingUrl": "https://www.ups.com/track?tracknum=1Z999AA10123456784" }
]Status timing
Pharmacies push status updates to Affinity. Affinity also checks orders on a schedule based on their status, with less frequent checks as orders age.
If the pharmacy never reports a result, the order stays open. Affinity staff follow up on Live orders open longer than three business days.
Cancel an order
A processing order can already be filled at the pharmacy. PerfectRx refuses cancellation after filling and verification.
A default Test order reaches this cutoff about 2 minutes after the test pharmacy receives it.
Use Test outcomes to test a refusal without waiting for that cutoff.
curl -X POST https://api.affinityrx.com/v1/orders/ord_01j9z3k4m5n6p7q8r9s0t1v2w7/cancel \
-H "Authorization: Bearer $AFFINITY_API_KEY" \
-H "Affinity-Version: 2026-10-28" \
-H "Idempotency-Key: cancel-order-42"Send no body with a cancel request. Cancelling an already-cancelled order returns 200 with the order and sends no second order.cancelled event.
If the pharmacy never received the order, Affinity cancels it and returns 200 with status: "cancelled".
If Affinity is sending the order at that moment, you get 202. If the send did not reach the pharmacy,
Affinity cancels the order within seconds and sends order.cancelled.
Otherwise, Affinity sends the cancel to the pharmacy. The response tells you what the pharmacy did:
| Response | Meaning | Then |
|---|---|---|
200, status: "cancelled" |
Affinity cancelled an unsent order, or the pharmacy confirmed the cancel. | Affinity sends order.cancelled on the first confirmation only. |
202, cancelRequestedAt set |
The pharmacy did not confirm yet, or it is not available. | Affinity tries again. You get order.cancelled when the pharmacy confirms. If the pharmacy refuses, cancelRequestedAt becomes null and you get order.updated. |
409 |
The cancel failed, the pharmacy refused, or it is too late. | The order keeps its status. An immediate 409 cancel_refused also sends one order.updated. |
If Affinity cannot reach the pharmacy for some minutes, the cancel stays pending. Affinity staff then
resolve it with the pharmacy, and you get order.cancelled or order.updated. To find orders with a
pending cancel, list orders with cancelRequested=true.
cancelRequestedAt returns to null when the cancel is confirmed or refused. Read status to
tell the results apart. A null value alone does not mean refusal.
200 and 202 return the whole order. A 409 has the code cancel_refused (the cancel request failed or the pharmacy refused)
or cannot_cancel (the order is shipped, delivered or failed).
If the response is uncertain, retry with the same Idempotency-Key. If the first response was 200 or 202, the retry returns that status code with the order as it is now.
If the first response was 409, or the request stopped before it answered, the retry computes its
answer from the current order and cancel job. Its status code can differ from the first response.
The retry does not ask the pharmacy again or send another event.
Read Retry safely.
A cancel with a new key on an order whose cancel is already waiting returns 202 and does not ask
the pharmacy again.
After the pharmacy refuses a cancel, a request with a new Idempotency-Key asks the pharmacy again.
A pharmacy can ship after it confirms cancellation. Continue to apply newer shipment and delivery events to a cancelled order. Check the latest order before you send a replacement.