Medication Cart
The MedCart flow lets a clinician stage several prescriptions during one encounter and approve them all in a single signing session—instead of opening a separate widget per drug. The flow is:
- Add draft
MedicationRequests to the cart one at a time as the clinician selects drugs. - Checkout—stage all drafts on ScriptSure's MedCart and get back a single widget URL.
- Prescriber opens the widget, reviews every drug, and signs them all at once.
- Remove or clear if the clinician changes their mind before signing.
The patient must be synced before calling checkout. See Sync a Patient.
React hook: useScriptSureCart
Package: @medplum/scriptsure-react · GitHub Source Code
A thin wrapper around useMedicationCart from @medplum/react-hooks that exposes all cart operations in a single hook.
import { useState } from 'react';
import { useScriptSureCart } from '@medplum/scriptsure-react';
import type { MedicationRequest } from '@medplum/fhirtypes';
function CartPanel({ patientId }: { patientId: string }) {
const { addToCart, adding, checkout, removeFromCart, clearCart } = useScriptSureCart();
const [approvalUrl, setApprovalUrl] = useState<string>();
const [draftIds, setDraftIds] = useState<string[]>([]);
async function handleAdd(medicationRequest: MedicationRequest) {
const created = await addToCart(medicationRequest);
setDraftIds((prev) => [...prev, created.id!]);
}
async function handleCheckout() {
const result = await checkout({ patientId, medicationRequestIds: draftIds });
const queued = result.items.filter((i) => i.status === 'queued');
if (queued.length > 0) {
setApprovalUrl(result.approvalUrl);
}
}
if (approvalUrl) {
return <iframe src={approvalUrl} width="100%" height="800px" />;
}
return (
<div>
<button onClick={handleCheckout} disabled={adding || draftIds.length === 0}>
Review & sign all ({draftIds.length})
</button>
<button onClick={() => clearCart({ patientId })}>Clear cart</button>
</div>
);
}
addToCart
Creates a draft MedicationRequest in Medplum via plain FHIR createResource—no custom operation is called. Vendor staging (ScriptSure MedCart) happens later at checkout. The adding flag is true while any addToCart call is in flight; checkout refuses to proceed while adding is true.
Parameters:
| Parameter | Type | Description |
|---|---|---|
medicationRequest | MedicationRequest | The draft MedicationRequest to persist. Must have status: 'draft' and intent: 'order'—$checkout-medications rejects any MR that does not meet these requirements. |
Returns: Promise<MedicationRequest>—the created resource with its Medplum-assigned id.
checkout
Calls POST /fhir/R4/MedicationRequest/$checkout-medications. Stages each draft on ScriptSure's MedCart and returns a single widget URL where the prescriber reviews and signs all medications at once.
Per-line failures are returned in items rather than thrown—check items[].status before opening the widget. Each successfully staged draft is stamped with a medcart-rx-id identifier and pending-order-status: in-cart. When the prescriber sends from the widget, the prescription webhook bot reconciles each draft to active.
Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
patientId | string | yes | Medplum Patient resource id |
medicationRequestIds | string[] | yes | Draft MedicationRequest ids to stage. Each must have status: 'draft' and intent: 'order'—others are returned as status: 'failed' in items. |
appId | string | Optional MedCart widget template id |
Response fields:
| Field | Type | Description |
|---|---|---|
approvalUrl | string | MedCart widget URL—embed as an iframe for the prescriber to review and sign all staged items |
vendorPatientId | number | Vendor-side patient id |
items | MedicationCheckoutItemResult[] | Per-line result (see below). Always returned even when some lines failed. |
Each items entry:
| Field | Type | Description |
|---|---|---|
medicationRequestId | string | Medplum MedicationRequest id |
vendorLineId | string | MedCart rxId for the staged item |
status | 'queued' | 'failed' | queued = successfully staged; failed = see error |
duplicate | boolean | true when the drug was already in the MedCart; the existing item is reused |
error | string | Present when status === 'failed' |
Do not open the approvalUrl if every item has status: 'failed'.
removeFromCart
Calls POST /fhir/R4/MedicationRequest/$remove-cart-medication. Removes one item from the MedCart and deletes the corresponding draft MedicationRequest from Medplum.
Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
patientId | string | yes | Medplum Patient resource id |
medicationRequestId | string | yes | Draft MedicationRequest id to remove |
action | 'remove' | yes (direct calls) | Required by the bot to identify which operation to run. The hook sets this automatically; callers invoking the operation directly with plain JSON must pass "action": "remove". |
Response fields:
| Field | Type | Description |
|---|---|---|
vendorPatientId | number | Vendor-side patient id |
removedCount | number | Number of items successfully removed (0 or 1) |
items | MedicationCartItemResult[] | Single-item array with status: 'removed' or status: 'failed' |
clearCart
Calls POST /fhir/R4/MedicationRequest/$clear-cart. Removes every item from the MedCart and deletes all draft MedicationRequests for the patient. Per-line failures are returned in items rather than thrown—failed drafts are kept so the caller can retry.
Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
patientId | string | yes | Medplum Patient resource id |
action | 'clear' | yes (direct calls) | Required by the bot to identify which operation to run. The hook sets this automatically; callers invoking the operation directly with plain JSON must pass "action": "clear". |
Response fields:
| Field | Type | Description |
|---|---|---|
vendorPatientId | number | Vendor-side patient id |
removedCount | number | Number of items successfully removed |
items | MedicationCartItemResult[] | Per-line result with status: 'removed' or status: 'failed' |
getCart
Calls POST /fhir/R4/MedicationRequest/$get-cart. Reads the MedCart as ScriptSure holds it and reconciles it against the patient's draft MedicationRequests. Read-only—it never writes to Medplum and never mutates the MedCart.
Use this whenever a cart count or list is shown to a prescriber. Counting drafts alone gives a lower bound: it misses items staged through another UI, cannot confirm whether a failed removeFromCart actually left the vendor item behind, and gives no drift check before checkout—the mismatch otherwise surfaces only when the prescriber is already looking at the signing widget.
Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
patientId | string | yes | Medplum Patient resource id |
Response fields:
| Field | Type | Description |
|---|---|---|
vendorPatientId | number | Vendor-side patient id |
vendorTotal | number | Number of items ScriptSure holds—what the prescriber will see in the widget |
draftCount | number | Number of draft MedicationRequests in Medplum |
locked | boolean | true when ScriptSure reports the cart locked; a write may conflict |
items | MedicationCartLine[] | One reconciled line per item either side holds (see below) |
Each items entry:
| Field | Type | Description |
|---|---|---|
status | MedicationCartLineStatus | Reconciliation verdict (see below) |
medicationRequestId | string | Medplum draft id; absent for vendor-only lines |
vendorLineId | string | MedCart rxId; absent for not-staged drafts |
drugName, ndc, rxnorm, quantity | Display fields off the vendor line; absent for not-staged drafts | |
validationState | string | Vendor readiness hint (Ready, Incomplete)—echoed from what was submitted, not a guarantee the line will send |
createdBy, createdAt | string | Who staged the line and when, per the vendor |
status values:
| Value | Meaning |
|---|---|
in-sync | Vendor line matched a draft. The normal case. |
vendor-only | ScriptSure holds a line with no Medplum draft—staged elsewhere. The prescriber will still see it in the widget. |
not-staged | Draft not checked out yet, so ScriptSure legitimately does not hold it. Benign, not drift. |
missing-from-vendor | Draft stamped with an rxId ScriptSure no longer returns—the line is gone and the draft is stale. |
const { getCart } = useScriptSureCart();
const cart = await getCart({ patientId });
const badgeCount = cart.vendorTotal + cart.items.filter((i) => i.status === 'not-staged').length;
const unexpected = cart.items.filter((i) => i.status === 'vendor-only');
A failed vendor read throws rather than returning an empty cart—reporting "the cart is empty" when ScriptSure is merely unreachable is the failure this operation exists to prevent.