Skip to main content
This guide is for pharmacies that connect a pharmacy management system to Med Xchange (MedEx). A law firm refers its client to your pharmacy. You record what you dispensed and billed, every payment, adjustment and refund against it, and the balance your own system shows. The firm sees the totals and the detail in MedEx, beside the rest of its client’s medical care. Everything here sits on your pharmacy’s referral, not on an appointment. You don’t create appointments or billing lines to record a fill. Read MedEx for medical providers first. It covers provider keys, the referral lifecycle and webhook delivery, which this guide builds on. Base URL: 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 return 403 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 is for synthetic data only. Never send real patient, prescription, payment or claim data or documents to UAT, and don’t point a copy of your live pharmacy database at it.
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.
The example is shortened. Each case has the fields shown in MedEx for medical providers, and the firm decides which of them to fill in.
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.
The ledger lives under that referral:
Another practice’s referral returns 404.
3

Accept the referral

The ledger opens once your referral is accepted. Accept it in MedEx, or with your standard or full key. From sent, accept acknowledges the referral first, so one call is enough:
See The referral lifecycle for 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. prescriber is 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_treatment or treatment_complete, and isn’t archived. Otherwise data is an empty array.
After the firm closes or withdraws your referral, this list is empty, but you can still record fills and payments. A prescriptionId you saved earlier is still accepted on new fills, so save each routed prescription’s id in your own system as soon as you see it.
List the prescriptions again after you acknowledge or accept. A prescription routed to you while your referral was still sent doesn’t produce a webhook.
Webhooks tell you about new work as it happens. 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

Response 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:
  • fillDateFrom and fillDateTo (YYYY-MM-DD, inclusive)
  • externalId, to find the fill you created with that ID
  • includeVoided (true by default; pass false to hide voided fills)
  • page and limit (up to 100, default 20)
GET .../pharmacy/fills/{fillId} returns one fill plus:
  • transactions: the transactions linked to that fill, oldest first
  • totals: the fill’s own billed, paid, adjustments and computedBalance, in the same shape as the balance. reported and difference are always null here. For a voided fill, billed is "0.00" and fillCount is 0, 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.
It returns 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 externalId or the void fields: sending them returns 400 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 billed or fillCount;
  • 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.
A void can’t be undone. If the fill was real after all, record it again as a new fill with a new 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 positive amount reduces the balance, and a negative amount increases it. The balance is billed − paid − adjustments.
CaseXchange stores each amount exactly as you send it and doesn’t check its sign against its type, because real ledgers contain unusual rows. A refund sent as "10.00" lowers the balance instead of raising it. Map your system’s sign convention once in your integration, and test each type in UAT.
An amount of zero is allowed.

Record a transaction

Response 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 a REVERSAL 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_OFF changes adjustments, and reversing a PAYMENT changes paid.
  • 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 REVERSAL with no reversesTransactionId counts in paid.
  • 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 their reversesTransactionId.
  • To correct an amount, reverse the original and record a new transaction with its own new externalId.

Worked example

Fill RX-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 difference is "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 new externalId) brings difference back to "0.00", as in the response above.
Report a new snapshot whenever your system’s balance changes, and after a sync that wrote to this referral. A difference that remains after that means the two ledgers really disagree, which usually means a transaction one side has and the other doesn’t. A positive difference means your system says more is owed than CaseXchange computes. List the referral’s transactions and compare them with your records.

Idempotency

Send an externalId 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:
(The example is shortened; data is the full record.) A conflict looks like this:

The rules

  • Scope. An externalId is 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 externalId on another referral returns 409 external_id_conflict. Build IDs that are unique across your pharmacy. For balance snapshots, put the referral and the asOf instant in the ID.
  • A conflict can point at another referral. If fetching existingId under this referral returns 404, the externalId is already used on another of your referrals. A PATCH can’t move a record between referrals, so choose a different externalId. Look existingId up 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;
    • quantity precision ("30" and "30.000");
    • a missing field and null;
    • spaces around text values;
    • the same asOf instant written in different time zones.
    Anything else that differs is a conflict.
  • 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 POST returns 409. Keep your own map from each externalId to the CaseXchange id it returned, and update existing records with PATCH instead 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 200 and the voided fill, with voidedAt set. It doesn’t restore the fill or create a new one, so check voidedAt.
  • Replays work in any status. The externalId is looked up before the referral status is checked and before linked IDs are validated. A replay therefore still returns 200 after the firm has closed or withdrawn the referral. Only a genuinely new record is refused with referral_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 409 if their contents differ.
  • Links that disappear. If the prescription or document a fill links to is deleted, the fill’s prescriptionId or claimDocumentId becomes null. No fill webhook is sent. A resend of the fill’s original POST then returns 409. Don’t PATCH the old ID back, because it returns 400 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 an externalId after creating the record.
  • Finding a record by externalId. Use GET .../pharmacy/fills?externalId=... or GET .../pharmacy/transactions?externalId=....
Always send an externalId. Without one, a request that timed out after CaseXchange saved it, followed by your retry, records the same fill or payment twice.

Prescription and claim documents

Upload documents with POST /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:
Response 201 Created:
The response doesn’t include 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 with supersedesDocumentId 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.supersededByDocumentId names the replacement; supersede that one instead.
  • The original stays listed and downloadable. GET /med-cases/{caseId}/documents shows it with supersededAt and supersededByDocumentId set, 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 externalId is released. Resending that upload stores a new document instead of replaying.
  • Repoint the fill with PATCH .../pharmacy/fills/{fillId} and { "claimDocumentId": "<the new id>" }.
When you load a backlog of claim forms, send notify=false so the firm isn’t emailed for every file. The uploads still appear in the case history.

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 the X-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 GET for its details.
  • Every event carries the externalId the record was created with, whoever made the change. An updated or voided event for a fill you created can come from someone at your pharmacy or from CaseXchange support, editing it in the web app.
  • externalId is null when 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.
Echoes of your own changes. Your own API writes send these events to your own subscriptions. Handle an event only after you verify its signature, and treat the body as a prompt to fetch the record, not as the record itself. You can skip a 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_treatment or treatment_complete. This is the same rule as GET /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.created and prescription.updated carry { "prescriptionId", "medCaseId" }. prescription.deleted carries { "id", "medCaseId" }, where id is the prescription ID.
  • Re-routing. If a prescription is re-routed from your pharmacy to another one, you receive prescription.deleted for it.

Errors

Errors use the standard envelope with error.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 each externalId 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, PATCH it if it changed. If it didn’t change, skip it.
  • Otherwise POST it with its externalId, and store the id from the 201.
  • 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, look error.details.existingId up in your map. If it’s there, PATCH that fill. If it isn’t, see Idempotency.
  • If your system cancels the fill, POST .../void with a reason. On 409 fill_voided it’s already voided. If the fill is dispensed again, record a new fill with a new externalId.
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_writable waits for the referral to be accepted;
  • external_id_conflict follows the rules under Idempotency;
  • fill_voided means 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.