https://api.casexchange.com/api/public/v1 in production. Build and test against UAT first: see Test in UAT.
Authentication: pass your key in the X-API-Key header on every request. Responses use the standard envelope described in Response Format.
Field-by-field request and response schemas live in the API tab under Endpoints → MedEx Pharmacy. This page explains how those endpoints fit together.
Before you start
Your key
A
read_only key that tries a write gets 403 insufficient_tier. The law firm on the case can read your ledger, but can’t change it.
Your practice must be set up as a pharmacy
CaseXchange marks a practice as a pharmacy. If yours isn’t marked, ledger writes on your referral return403 pharmacy_only, and so does GET /med-cases/{caseId}/prescriptions. Ledger reads still work and come back empty. If you get pharmacy_only, ask help@casexchange.com to check your setup.
A restricted-access practice gets 403 restricted_access on every ledger endpoint.
When you can write
You write to the ledger of your own referral on a case. The referral’s status decides whether new records are accepted:
Reads work in every status. Changes to existing fills and transactions follow the same rule.
A pharmacy referral normally goes
sent → under_review → accepted and stays accepted until the firm closes it. start-treatment and complete-treatment don’t apply to pharmacies (see Pharmacies). accept also acknowledges a sent referral, so a single call opens the ledger.
Resending a record you’ve already created succeeds in any status. See Idempotency.
Test in UAT
Build against UAT, the environment for testing integrations. It runs the same API against test firms and patients, so you can acknowledge, accept and reject referrals and write to the ledger without touching a real case.
UAT and production keys are separate. A production key doesn’t work in UAT, and a UAT key doesn’t work in production. To get a UAT key and test referrals, email help@casexchange.com. See Base URLs.
Finding work
1
List the cases referred to you
GET /med-cases returns your pharmacy’s cases, newest first. It takes page, limit (up to 100, default 20), search (matches patient name, reference number or date of birth) and includeArchived=true.2
Find your referral and check its status
GET /med-cases/{id}/referrals lists your pharmacy’s referrals on the case. Note the referral id, which every ledger call needs, and check its status against When you can write.404.3
Accept the referral
The ledger opens once your referral is See The referral lifecycle for
accepted. Accept it in MedEx, or with your standard or full key. From sent, accept acknowledges the referral first, so one call is enough:notify and rejection.4
Find the prescription routed to you
When the treating practice routes a prescription to your pharmacy,
GET /med-cases/{caseId}/prescriptions lists it. Use its id as prescriptionId when you record the fill.- The list holds only prescriptions routed to your pharmacy.
prescriberis the practice that wrote the prescription. The list isn’t paginated. - You see the list only while your referral on the case is
under_review,accepted,in_treatmentortreatment_complete, and isn’t archived. Otherwisedatais an empty array.
sent doesn’t produce a webhook.referral.sent arrives when a firm sends you a referral. prescription.created arrives when a prescription is routed to you while your referral is under_review, accepted, in_treatment or treatment_complete. A prescription that’s re-routed to you from another pharmacy arrives as prescription.updated. Prescriptions routed while your referral was sent produce no webhook. See Webhooks.
Fills
A fill is one dispensing of a prescription: what you dispensed, when, and what you billed for it. Fills are corrected or voided, never deleted.Record a fill
201 Created:
An unknown field, or a value in the wrong format, returns
400 validation_error with each problem listed in error.details.errors. A prescriptionId that isn’t routed to you on this case, or a claimDocumentId that isn’t on this case or that you can’t see, returns 400 invalid_reference.
NDC format
Send the 11-digit NDC in the 5-4-2 layout. CaseXchange removes hyphens and spaces, then requires exactly 11 digits, and returns the NDC without hyphens. Removing hyphens does not pad a 10-digit NDC. NDCs printed on packages in the 4-4-2, 5-3-2 or 5-4-1 layout have 10 digits. Convert them to 5-4-2 by adding a leading zero to the short segment:
An unhyphenated 10-digit NDC can’t be converted reliably, because you can’t tell which segment is short. Convert from the hyphenated form, or send the 11-digit NDC your system already stores.
A 10-digit NDC returns
400 validation_error. The entry for body.ndc in error.details.errors has the message NDC must be 11 digits (hyphens allowed).
Read fills
GET .../pharmacy/fills lists the referral’s fills, newest fillDate first. It accepts:
fillDateFromandfillDateTo(YYYY-MM-DD, inclusive)externalId, to find the fill you created with that IDincludeVoided(trueby default; passfalseto hide voided fills)pageandlimit(up to 100, default 20)
GET .../pharmacy/fills/{fillId} returns one fill plus:
transactions: the transactions linked to that fill, oldest firsttotals: the fill’s ownbilled,paid,adjustmentsandcomputedBalance, in the same shape as the balance.reportedanddifferenceare alwaysnullhere. For a voided fill,billedis"0.00"andfillCountis0, but its transactions still count.
Correct a fill
PATCH .../pharmacy/fills/{fillId} changes any of these fields: rxNumber, fillDate, ndc, drugName, quantity, daysSupply, prescriberName, billedAmount, prescriptionId, claimDocumentId and notes. Send at least one. Send null to clear an optional field.
200 with the updated fill. The change is recorded in the case history, and a change to billedAmount records both the old and new amounts.
- You can’t change
externalIdor the void fields: sending them returns400 validation_error. - A voided fill can’t be changed:
409 fill_voided. - The referral must be in a status that accepts writes.
After you PATCH a fill, sending its original
POST again with the same externalId returns 409 external_id_conflict, because the fill no longer matches that body. Once you have a fill’s CaseXchange id, update it with PATCH rather than resending the POST. See Idempotency.Void a fill
Void a fill that was entered in error or reversed in your system:reason is required (1–1,000 characters). The response is 200 with the fill, with voidedAt and voidReason set. A voided fill:
- no longer counts towards
billedorfillCount; - stays in the fills list and keeps its history, so the firm still sees it, marked as voided;
- can’t be changed or voided again (
409 fill_voided); - keeps its transactions, and they still count. If money was posted against it, post reversals to back that money out.
externalId.
Transactions
Every payment, adjustment, write-off, refund and reversal is a transaction. The transaction ledger is append-only: a transaction’s money never changes after it’s recorded, and transactions are never deleted. To correct money, post a reversal.The sign convention
A positiveamount reduces the balance, and a negative amount increases it. The balance is billed − paid − adjustments.
An amount of zero is allowed.
Record a transaction
201 Created:
Record the fill before any transaction that names it: a
fillId or reversesTransactionId that isn’t on this referral returns 400 invalid_reference.
GET .../pharmacy/transactions lists the referral’s transactions, newest transactionDate first. You can filter by fillId, type, dateFrom, dateTo (inclusive) and externalId, and page with page and limit (up to 100). GET .../pharmacy/transactions/{transactionId} returns one transaction.
What you can change afterwards
PATCH .../pharmacy/transactions/{transactionId} changes only payer, reference, note and fillId. Use fillId to move a transaction to another fill, or send null to un-apply it.
type, amount, transactionDate and reversesTransactionId never change. A PATCH that names any of them, even with the value it already has, returns 400 immutable_field, and error.details.fields lists the fields:
Reversals
A reversal undoes an earlier transaction on the same referral. Post aREVERSAL with reversesTransactionId set to that transaction, usually for the negative of its amount:
- A reversal counts in the same bucket as the transaction it reverses. Reversing a
WRITE_OFFchangesadjustments, and reversing aPAYMENTchangespaid. - A reversal of a reversal follows the chain back to the original transaction and counts in that transaction’s bucket. Use it when the reversal itself was the mistake.
- A
REVERSALwith noreversesTransactionIdcounts inpaid. - CaseXchange doesn’t check the reversal amount against the original, and doesn’t stop you reversing the same transaction twice. Before you post one, list the referral’s reversals (
?type=REVERSAL) and check theirreversesTransactionId. - To correct an amount, reverse the original and record a new transaction with its own new
externalId.
Worked example
FillRX-100245 is billed at 120.00. A second fill, RX-100246 for 45.00, was entered in error and voided, so it doesn’t count. These eight transactions are then posted against RX-100245:
A negative balance, as after T3, is a credit. T7 reverses a write-off, so it counts in
adjustments. T8 is a reversal of a reversal: it follows T7 back to T6, a WRITE_OFF, so it also counts in adjustments.
Balance
GET .../pharmacy/balance returns the referral’s totals as CaseXchange computes them, next to the balance your system last reported. This is the referral from the worked example, after the snapshot of 2026-10-17 described below:
The firm sees both balances in MedEx.
Report your balance
Your system’s balance is the figure the firm will usually ask about. Record it as a snapshot:
The response is
201 with the snapshot. Snapshots are append-only and can’t be changed or deleted. GET .../pharmacy/balance-snapshots lists them, latest asOf first, with page and limit.
difference compares your latest snapshot with the ledger as it is now, not as it was at asOf. In the worked example:
- The snapshot above reports 20.00 as of 2026-10-15, which matches the ledger after T5.
- After T6, T7 and T8, the computed balance is 0.00, so
differenceis"20.00", even though both systems agreed when the snapshot was taken. - Reporting again (
"balance": "0.00","asOf": "2026-10-17T23:00:00Z", with a newexternalId) bringsdifferenceback to"0.00", as in the response above.
difference means your system says more is owed than CaseXchange computes. List the referral’s transactions and compare them with your records.
Idempotency
Send anexternalId on every create: fills, transactions, balance snapshots and document uploads. It’s your own permanent ID for the record, and it makes every create safe to retry.
A replay looks like this:
data is the full record.) A conflict looks like this:
The rules
-
Scope. An
externalIdis unique across your whole pharmacy, separately for each kind of record. Your fill IDs are checked against your fills on every case and referral, your transaction IDs against your transactions, and so on. A fill and a transaction may share an ID, but two fills may not. -
The referral is part of the content. Reusing an
externalIdon another referral returns409 external_id_conflict. Build IDs that are unique across your pharmacy. For balance snapshots, put the referral and theasOfinstant in the ID. -
A conflict can point at another referral. If fetching
existingIdunder this referral returns404, theexternalIdis already used on another of your referrals. A PATCH can’t move a record between referrals, so choose a differentexternalId. LookexistingIdup in your own map first. -
What counts as the same content. The incoming request is compared with the record as it is now, after small formatting differences are ignored:
- money in any precision (
"40"and"40.00"); - NDC hyphens and spaces;
quantityprecision ("30"and"30.000");- a missing field and
null; - spaces around text values;
- the same
asOfinstant written in different time zones.
- money in any precision (
-
Send CaseXchange IDs in lower case, exactly as they were returned. For fills, transactions and snapshots, the IDs in the URL and the body are compared exactly. A retry that sends the same referral, prescription, document, fill or transaction ID in upper case returns
409 external_id_conflict. If your database stores GUIDs in upper case, lower-case them before you send them. -
After a PATCH, the original create conflicts. Once you’ve changed a record, resending its original
POSTreturns409. Keep your own map from eachexternalIdto the CaseXchangeidit returned, and update existing records withPATCHinstead of resending the create. -
A voided fill still replays. Voiding isn’t part of a fill’s content. Resending the create for a fill you’ve since voided returns
200and the voided fill, withvoidedAtset. It doesn’t restore the fill or create a new one, so checkvoidedAt. -
Replays work in any status. The
externalIdis looked up before the referral status is checked and before linked IDs are validated. A replay therefore still returns200after the firm has closed or withdrawn the referral. Only a genuinely new record is refused withreferral_not_writable. A replay still needs a key that can reach the case and referral, on a referral of a pharmacy. -
Retries of the same request at the same moment never create two records. One creates the record and the other gets the replay, or
409if their contents differ. -
Links that disappear. If the prescription or document a fill links to is deleted, the fill’s
prescriptionIdorclaimDocumentIdbecomesnull. No fill webhook is sent. A resend of the fill’s originalPOSTthen returns409. Don’t PATCH the old ID back, because it returns400 invalid_reference. Link a current document, or leave the field empty. -
Format. 1–200 characters after spaces at either end are trimmed. An empty value returns
400 validation_error. You can’t change anexternalIdafter creating the record. -
Finding a record by
externalId. UseGET .../pharmacy/fills?externalId=...orGET .../pharmacy/transactions?externalId=....
Prescription and claim documents
Upload documents withPOST /med-cases/{caseId}/documents, as described in MedEx for medical providers. Two categories suit pharmacies:
The upload also takes three optional form fields, which work for every practice:
Upload a claim form and link it to the fill
201 Created:
externalId. Keep the returned id, then set it on the fill as claimDocumentId, either when you create the fill or later with PATCH .../pharmacy/fills/{fillId}.
Replace a document
To send a corrected claim form, upload it withsupersedesDocumentId set to the one it replaces, and give it a new externalId:
- The document you replace must be one your organisation uploaded directly to this case, and it must not already be replaced. Otherwise the upload returns
409 invalid_supersede. If it was already replaced,error.details.supersededByDocumentIdnames the replacement; supersede that one instead. - The original stays listed and downloadable.
GET /med-cases/{caseId}/documentsshows it withsupersededAtandsupersededByDocumentIdset, and MedEx marks it as superseded. - If the replacement is later deleted, the original becomes the current version again. A replay of the replacement upload after the original has been deleted returns
409 external_id_conflict, because the replaced document is part of the upload’s content. - If you delete a document, its
externalIdis released. Resending that upload stores a new document instead of replaying. - Repoint the fill with
PATCH .../pharmacy/fills/{fillId}and{ "claimDocumentId": "<the new id>" }.
Webhooks
A webhook subscription created with your pharmacy’s key receives the MedEx events described in Webhooks for providers. It covers delivery, signatures and the bare-payload body, with the event name in theX-CaseXchange-Event header. Pharmacies also receive these ledger events:
- The events go only to the pharmacy whose referral it is, never to the firm or to other practices on the case.
- They carry IDs only. Fetch the record with
GETfor its details. - Every event carries the
externalIdthe record was created with, whoever made the change. Anupdatedorvoidedevent for a fill you created can come from someone at your pharmacy or from CaseXchange support, editing it in the web app. externalIdisnullwhen the record was created without one: in the web app, or by an API call that didn’t send one.- A replay sends no webhook.
pharmacy_fill.created, pharmacy_transaction.created or pharmacy_balance.reported event whose externalId is in your map, because you created that record. Don’t skip updated or voided events on that basis: fetch the record and compare it with your system.
Prescription events
prescription.created, prescription.updated and prescription.deleted also go to the pharmacy a prescription is routed to, as well as to the practice that wrote it.
- When you receive them. Only while your referral on the case isn’t archived and is
under_review,accepted,in_treatmentortreatment_complete. This is the same rule asGET /med-cases/{caseId}/prescriptions. - Exceptions. Restricted-access practices don’t receive them. If you also wrote the prescription, you receive each event once.
- Bodies.
prescription.createdandprescription.updatedcarry{ "prescriptionId", "medCaseId" }.prescription.deletedcarries{ "id", "medCaseId" }, whereidis the prescription ID. - Re-routing. If a prescription is re-routed from your pharmacy to another one, you receive
prescription.deletedfor it.
Errors
Errors use the standard envelope witherror.code, error.message and, for these codes, error.details. See Error Handling for the general rules and MedEx for medical providers for the authentication and rate-limit codes.
A suggested sync loop
This is one way to keep CaseXchange in step with a pharmacy management system such as PioneerRx. It uses only the behaviour described on this page, so adapt it to how your system stores fills and payments. Keep a local map from eachexternalId you send to the CaseXchange id that comes back.
1
Choose permanent external IDs
Derive each
externalId from your system’s own permanent key for the record, with a prefix for the record type. It must be unique across your whole pharmacy. If you run more than one store or database under one CaseXchange pharmacy, include a store code:Never send the same
externalId with different content.2
Load your referrals once, then follow webhooks
On the first run, page through
GET /med-cases and GET /med-cases/{caseId}/referrals with limit=100, and keep a local list of your referrals and their statuses. Keep the ones whose status accepts writes.After that, update the list from webhooks instead of re-scanning: referral.sent, referral.rejected, referral.closed and referral.withdrawn, plus prescription.created, prescription.updated and prescription.deleted. Webhooks have no backfill, so repeat the full pass occasionally to catch anything you missed.Call GET /med-cases/{caseId}/prescriptions only for referrals that are under_review, accepted, in_treatment or treatment_complete, including once just after you accept. For other referrals it returns an empty list and still counts as a request.3
Fills first
Send only fills that are new or changed since your last successful sync. Track changes in your own system. The
fillDateFrom and dateFrom filters work on the fill or transaction date, not on when the record was recorded or changed, so they can’t find changes for you.- If your map already has the fill’s CaseXchange
id,PATCHit if it changed. If it didn’t change, skip it. - Otherwise
POSTit with itsexternalId, and store theidfrom the201. - Don’t resend creates to sync. A resend is for retrying a request that timed out, and a replay still counts as a write towards your rate limit.
- On
409 external_id_conflict, lookerror.details.existingIdup in your map. If it’s there,PATCHthat fill. If it isn’t, see Idempotency. - If your system cancels the fill,
POST .../voidwith a reason. On409 fill_voidedit’s already voided. If the fill is dispensed again, record a new fill with a newexternalId.
4
Then transactions
Send transactions after the fills they name, so that
fillId resolves. POST each new payment, adjustment, write-off or refund with its externalId.Never try to change money. If your system changes a transaction’s amount, date or type, post a REVERSAL of the old one and a new transaction, each with its own new externalId, such as prx-txn-55001-rev and prx-txn-55001-v2. If only the payer, reference, note or fill changed, PATCH it. On 409 external_id_conflict, find existingId in your map and apply the same rule.5
Report the balance
Report a snapshot for each referral you wrote to in this run, and for any referral whose balance changed in your system, with
asOf set to when your system computed the balance. Then GET .../pharmacy/balance for those referrals. If difference isn’t "0.00", list the referral’s transactions, compare them with your records, and post whatever is missing.6
Upload claim documents
Upload each claim form once per file version, with
category=claim_form and an externalId for that version, and set the returned id as the fill’s claimDocumentId. A replay re-sends the whole file, so don’t re-upload files you’ve already sent. Send a corrected form with supersedesDocumentId. When loading a backlog, send notify=false.7
Retry safely
On a timeout, a
429 or a 5xx, resend the same request with the same externalId. You get 201 if the first attempt never landed, or a 200 replay if it did. A PATCH is safe to resend.Wait for Retry-After after a 429. A request rejected with 429 still counts towards the window. Read X-RateLimit-Remaining and slow down before you reach the limit. Writes count whether they create, replay, conflict or fail validation. The limits are in Rate limits.A 400, 403 or 404 won’t succeed without a change to the request. A 409 isn’t retryable as it is:referral_not_writablewaits for the referral to be accepted;external_id_conflictfollows the rules under Idempotency;fill_voidedmeans the void already happened.
8
Handle echoes and web-app changes
Your writes come back to your subscriptions as
pharmacy_* webhooks. Verify each event’s signature. Skip a *.created or pharmacy_balance.reported event whose externalId is in your map. For pharmacy_fill.updated, pharmacy_fill.voided and pharmacy_transaction.updated, fetch the record and compare it with your system, because someone at your pharmacy may have changed it in the CaseXchange web app.MedEx for medical providers
Provider keys, the referral lifecycle and webhook delivery.
Error Handling
Error envelope, status codes and troubleshooting.

