Customer API · catalog & ordering

Your catalog, by API.

Browse your catalog, pull your contract pricing, and check stock — programmatically. One REST surface — OAuth-style bearer auth, JSON out. Read your catalog, pricing and stock, and place orders straight from your own system. Every endpoint here ships in the live MedGrid codebase and is scoped to your account.

terminal — authenticate, then call
# 1 · exchange credentials for a 1-hour token
curl -X POST -d "client_id=…" -d "client_secret=…" \
  "https://medgrid.com/api/method/medgrid.customer_api.auth_token"

# 2 · call any endpoint with that token
curl -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://medgrid.com/api/method/medgrid.customer_api.whoami"
What you can do

Read, price, and order.

Start here

Authentication

The Customer API authenticates with a short-lived Bearer token. Your credentials — a client ID and secret — are issued by a MedGrid administrator from your Customer record; there is no self-service signup. You exchange them once for a 1-hour access token, then send that token on every call. This is the only accepted method — a raw token client_id:client_secret header is rejected with a 401.

Environments

Build against UAT first, then change one base URL to go live. The two are separate installations with separate databases, so credentials issued on one are unknown to the other — ask your MedGrid administrator for UAT credentials as well as production ones.

EnvironmentBase URLUse it for
UAThttps://uat.medgrid.comBuilding and testing your integration.
Productionhttps://medgrid.comReal orders, real money.
Every example on this page is written against https://medgrid.com, the site you are reading it on — so you can copy any of them as-is.

Step 1 — Get an access token

POST your client_id and client_secret to auth_token. You get back a signed token valid for 1 hour. Treat the secret like a password — never ship it in client-side code.

POST · auth_token
curl -X POST \
  -d "client_id=CLIENT_ID" \
  -d "client_secret=CLIENT_SECRET" \
  "https://medgrid.com/api/method/medgrid.customer_api.auth_token"

Response · 200

json
{ "message": {
    "ok": true,
    "access_token": "eyJhbGciOi…",
    "token_type": "bearer",
    "expires_in": 3600,
    "scope": "customer_api",
    "customer": "CUST-00042"
} }

Step 2 — Call with the Bearer token

Send the access_token in the Authorization header on every request. When it expires you'll get a friendly 401 — just repeat Step 1 for a fresh one.

request header
Authorization: Bearer ACCESS_TOKEN

A full call

terminal
curl -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://medgrid.com/api/method/medgrid.customer_api.whoami"
Getting credentials. A MedGrid admin opens your Customer record → Customer API tab → Generate Credentials, and shares the client ID + secret with you securely. They can Reveal the secret again, Regenerate it (which invalidates the old one), or Revoke access entirely at any time.
Cache the token — don't fetch one per call. A token is good for its full hour, so hold it and reuse it, and only call auth_token again when a call comes back 401. This matters more than it looks: auth_token is limited to 20 requests a minute per source IP, and that budget is counted per IP rather than per account — so every server behind the same egress address shares it. An integration that authenticates before each request will lock itself out inside a minute.
First integration

Quickstart

One order, end to end. Every call below is a complete request against https://medgrid.com, and running them in order places a real draft order and follows it to settlement. Step 6 is the one that creates something — point these at UAT while you are still building (see Environments).

1 — Get a token

Hold on to access_token; it is good for an hour and every later call needs it.

curl -X POST \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  "https://medgrid.com/api/method/medgrid.customer_api.auth_token"

2 — Confirm what your account can see

whoami echoes the price list your prices come from, your currency, and the warehouses your stock is counted across. If any of it looks wrong, stop here and talk to your administrator — everything downstream is computed from it.

curl -H "Authorization: Bearer $TOKEN" \
  "https://medgrid.com/api/method/medgrid.customer_api.whoami"

3 — Pull your catalog

Page until a response comes back with fewer than page_length products — there is no total to count against.

curl -H "Authorization: Bearer $TOKEN" \
  "https://medgrid.com/api/method/medgrid.customer_api.get_catalog?page=1&page_length=200"

# page 1 → 200 products, page 2 → 200, page 3 → 47  ← short page, you are done

4 — Price the basket you intend to order

Catalog prices are already yours, but re-checking immediately before ordering is what stops a stale cached price surprising you. get_availability does the same for stock.

curl -H "Authorization: Bearer $TOKEN" \
  "https://medgrid.com/api/method/medgrid.customer_api.get_pricing?skus=VD3-5000-120"

5 — Ask what shipping the basket qualifies for

The shipping_rule you get back is exactly what step 6 expects; anything else is rejected.

curl -G -H "Authorization: Bearer $TOKEN" \
  --data-urlencode 'items=[{"sku":"VD3-5000-120","qty":2}]' \
  "https://medgrid.com/api/method/medgrid.customer_api_orders.get_shipping_options"

6 — Place the order

Choose external_ref deliberately: it is your own order id, it is how you look this order up later, and re-sending it is what makes a retry safe. Nothing is charged by this call — MedGrid emails the customer a payment link.

curl -X POST -H "Authorization: Bearer $TOKEN" \
  --data-urlencode 'items=[{"sku":"VD3-5000-120","qty":2}]' \
  -d "shipping_rule=Standard Shipping" \
  -d "external_ref=PO-2026-0184" \
  "https://medgrid.com/api/method/medgrid.customer_api_orders.create_order"
Store the order you get back against your own external_ref. You can look the order up by either, but keeping both is what lets you reconcile if a request ever times out before you saw the response.

7 — Poll until it is paid

The customer pays the emailed link; that is what releases the order. Poll payment_status until it reaches a terminal value — Paid, or Failed if a bank debit bounced — and stop there. After Paid, follow per_delivered_qty and per_billed_qty for fulfilment.

curl -H "Authorization: Bearer $TOKEN" \
  "https://medgrid.com/api/method/medgrid.customer_api_orders.get_order_status?external_ref=PO-2026-0184"
Poll every few seconds at most. These calls share the 20-per-minute ordering budget with create_order, so a tight loop will start returning 429 and block your next order.
Steps 1–5 are read-only and safe to repeat while you experiment. Only step 6 creates anything, and even then nothing is charged until the customer pays the link — so a draft raised by mistake can simply be left unpaid.
Good to know

Conventions

A few things that hold true across every endpoint.

Request & response shape

The catalog endpoints are GET with parameters in the query string. auth_token, create_order and request_payment_link are POST-only with a form body; get_shipping_options and get_order_status are reads and accept GET. Responses are JSON, wrapped by the framework in a message envelope, and always carry an ok flag.

Success

{ "message": { "ok": true, /* … endpoint data … */ } }

Failure — never a raw stack trace

{ "message": { "ok": false, "error": "Human-readable message.",
              "code": "PermissionError", "status": 403 } }
The customer parameter is optional. Your token already identifies your account, so you normally omit it. Admin tokens that manage several customers can pass customer= to target a specific one.
Reference

Catalog endpoints

Six read endpoints, all under /api/method/medgrid.customer_api.*. Each is auth-gated and scoped to your account — you only ever see your own catalog, pricing and warehouses. The ordering endpoints live under medgrid.customer_api_orders.*.

GET whoami AuthRead-only

Connectivity check. Echoes the resolved customer, price list, currency, markup, and allowed warehouses. Start here.

https://medgrid.com/api/method/medgrid.customer_api.whoami

Request

curl -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://medgrid.com/api/method/medgrid.customer_api.whoami"

Response · 200

{
  "ok": true,
  "customer": "CUST-00042",
  "customer_name": "Sunrise Medical Clinic",
  "status": "Active",
  "item_selection": "All",
  "item_groups": [], "item_tags": [], "item_skus": [],
  "price_list": "Sunrise Wholesale",
  "currency": "USD",
  "markup_pct": 0.0,
  "warehouses": ["Main Warehouse - MG", "East DC - MG"],
  "server_time": "2026-06-26 12:00:00"
}

GET get_catalog AuthRead-only

Paginated, priced catalog of the items available to your account — stock summed across your allowed warehouses.

https://medgrid.com/api/method/medgrid.customer_api.get_catalog
ParameterTypeRequiredDescription
pageintno1-based page number. Default 1.
page_lengthintnoItems per page. Default 50, max 200.
in_stock_only0 / 1noWhen 1, only items with available stock.
item_groupstringnoFilter to one item group.
searchstringnoMatch on item name or code.
customerstringnoAdmin-only override; omit for your own account.

Request

curl -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://medgrid.com/api/method/medgrid.customer_api.get_catalog?page=1&page_length=50&in_stock_only=1&search=vitamin"

Response · 200

{
  "ok": true,
  "customer": "CUST-00042",
  "price_list": "Sunrise Wholesale",
  "currency": "USD",
  "page": 1, "page_length": 50, "count": 1,
  "products": [
    {
      "sku": "VD3-5000-120",
      "name": "Vitamin D3 5000 IU (120 ct)",
      "description": "High-potency cholecalciferol.",
      "group": "Supplements", "brand": "MedGrid",
      "image": "/files/vd3.png", "uom": "Unit",
      "price": 18.75, "currency": "USD",
      "compare_at_price": 24.0, "on_sale": true,
      "available_qty": 340, "in_stock": true
    }
  ]
}
count is the number of products on this page; there is no grand total. Page until a response comes back with fewer than page_length products.

GET get_catalog_item AuthRead-only

Full detail for a single product — including per-warehouse stock — but only if that SKU is in your catalog.

https://medgrid.com/api/method/medgrid.customer_api.get_catalog_item
ParameterTypeRequiredDescription
skustringyesThe item code to look up.
customerstringnoAdmin-only override; omit for your own account.

Request

curl -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://medgrid.com/api/method/medgrid.customer_api.get_catalog_item?sku=VD3-5000-120"

Response · 200

{
  "ok": true,
  "customer": "CUST-00042",
  "currency": "USD",
  "product": {
    "sku": "VD3-5000-120",
    "name": "Vitamin D3 5000 IU (120 ct)",
    "description": "High-potency cholecalciferol.",
    "group": "Supplements", "brand": "MedGrid",
    "image": "/files/vd3.png", "uom": "Unit",
    "price": 18.75, "currency": "USD",
    "compare_at_price": 24.0, "on_sale": true,
    "available_qty": 340, "in_stock": true,
    "warehouses": [
      { "warehouse": "Main Warehouse - MG", "available_qty": 200 },
      { "warehouse": "East DC - MG", "available_qty": 140 }
    ]
  }
}
If the SKU isn't in your catalog you get a 404 "Item … is not available in your catalog." Omit sku and you get a friendly 417 asking you to provide one — never a stack trace.

GET get_pricing AuthRead-only

Your account's prices. Pass skus to price a specific set (each flagged as in your catalog or not), or omit it to price the whole catalog (capped at 500).

https://medgrid.com/api/method/medgrid.customer_api.get_pricing
ParameterTypeRequiredDescription
skusCSV / JSON listnoe.g. VD3-5000-120,OMEGA3-1000-90. Omit for the whole catalog.
customerstringnoAdmin-only override; omit for your own account.

Request

curl -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://medgrid.com/api/method/medgrid.customer_api.get_pricing?skus=VD3-5000-120,OMEGA3-1000-90"

Response · 200

{
  "ok": true,
  "customer": "CUST-00042",
  "price_list": "Sunrise Wholesale",
  "currency": "USD",
  "count": 2,
  "prices": [
    { "sku": "VD3-5000-120", "price": 18.75, "currency": "USD", "in_catalog": true },
    { "sku": "OMEGA3-1000-90", "price": null, "currency": "USD", "in_catalog": false }
  ]
}

GET get_availability AuthRead-only

Available-to-sell quantities (projected stock) per warehouse. A requested warehouse must be one of your allowed warehouses, else 403.

https://medgrid.com/api/method/medgrid.customer_api.get_availability
ParameterTypeRequiredDescription
skusCSV / JSON listnoLimit to these SKUs. Omit for the whole catalog (capped).
warehousestringnoOne of your allowed warehouses. Omit for all of them.
customerstringnoAdmin-only override; omit for your own account.

Request

curl -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://medgrid.com/api/method/medgrid.customer_api.get_availability?skus=VD3-5000-120&warehouse=Main%20Warehouse%20-%20MG"

Response · 200

{
  "ok": true,
  "customer": "CUST-00042",
  "warehouses": ["Main Warehouse - MG"],
  "count": 1,
  "levels": [
    { "sku": "VD3-5000-120", "warehouse": "Main Warehouse - MG", "available_qty": 200 }
  ],
  "totals": { "VD3-5000-120": 200 }
}
A requested SKU that is not in your catalog is simply left out of levels and totals — no error, unlike get_pricing, which flags it with in_catalog: false. A SKU with no stock row anywhere is also absent rather than reported as 0.

GET get_warehouses AuthRead-only

The warehouses you may report availability for — the valid values for the warehouse parameter above.

https://medgrid.com/api/method/medgrid.customer_api.get_warehouses

Request

curl -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://medgrid.com/api/method/medgrid.customer_api.get_warehouses"

Response · 200

{
  "ok": true,
  "customer": "CUST-00042",
  "count": 2,
  "warehouses": [
    { "name": "Main Warehouse - MG", "warehouse_name": "Main Warehouse" },
    { "name": "East DC - MG", "warehouse_name": "East DC" }
  ]
}
Reference

Ordering endpoints

Four endpoints under /api/method/medgrid.customer_api_orders.* that let your system place a real MedGrid order. They use the same Bearer token as the catalog endpoints, but must be switched on separately — ask your MedGrid administrator to tick Allow API Ordering on your Customer record. Until then every call here returns a 403.

How an order flows

You submit the order; the customer pays the link we email them. No API call you make ever charges anyone.

StepWhoWhat happens
1get_shipping_options
you
Tells you which shipping methods your basket qualifies for, and what each costs.
2create_order
you
We validate the SKUs against your catalog, check your credentials, check stock, price every line and build a draft order with the exact total. Nothing is charged and no stock moves.
3MedGridThe same call immediately emails that draft to the account as a quotation, carrying a secure payment link (payment_url in the response) that is valid for 48 hours.
4customerThey open the link and pay by card or bank/ACH. That payment is what releases the order: it is submitted into fulfilment, an invoice is raised and the confirmation email goes out — automatically, with no further call from you.
5request_payment_link
you
Only if the 48 hours lapse unpaid (or the first email failed): issues a fresh link and re-sends it. The order and its prices are untouched.
6get_order_status
you
Poll for payment, then for delivery and billing progress.
Payment releases the order — not an API call. There is deliberately no customer-callable confirm endpoint. Until the emailed link is paid, the order sits as an unpaid draft and get_order_status reports "status": "Draft", "docstatus": 0 and "payment_status": "Pending" ("Unpaid" only while no link has been emailed yet).
The link expires after 48 hours. Every response tells you exactly when: payment_link_expires_on, plus a payment_link_expired boolean. An expired link shows the customer an "expired" page instead of a payment form — call request_payment_link and they get a fresh one by email.
A MedGrid administrator can still step in. They may release an order by hand (charging the card on file) or reject it with a written reason — in which case get_order_status returns "status": "Rejected" and a rejection_reason, we email your account the same text, and nothing is charged.
Before you can order. Your account needs (a) Allow API Ordering enabled, (b) an email on file to receive the payment link, (c) a verified NPI — and DEA or state licence for the regulated classes you're buying — and (d) a saved shipping address. These are exactly the same requirements as checking out on the website; the API does not relax any of them.

GET get_shipping_options AuthOrdering opt-in

The shipping methods available for a specific basket, with the exact amount that will be charged. The shipping_rule value it returns is what create_order expects.

https://medgrid.com/api/method/medgrid.customer_api_orders.get_shipping_options
ParameterTypeRequiredDescription
itemsJSON arrayyesThe basket, e.g. [{"sku":"VD3-5000-120","qty":2}].
customerstringnoAdmin-only override; omit for your own account.

Request

curl -H "Authorization: Bearer ACCESS_TOKEN" \
  --data-urlencode 'items=[{"sku":"VD3-5000-120","qty":2}]' -G \
  "https://medgrid.com/api/method/medgrid.customer_api_orders.get_shipping_options"

Response · 200

{
  "ok": true,
  "customer": "CUST-00042",
  "currency": "USD",
  "count": 2,
  "options": [
    { "shipping_rule": "Standard Shipping", "label": "Standard Shipping", "amount": 25.0 },
    { "shipping_rule": "Overnight Shipping", "label": "Overnight Shipping", "amount": 50.0 }
  ]
}
Amounts already include the per-warehouse multiple when your basket ships from more than one location. A basket containing a cellular or acellular biologic collapses to the single cold-chain overnight method — that is the only legal way to ship those products.

POST create_order AuthWriteOrdering opt-in

Creates a draft order, returns its full server-computed total, and emails the customer a payment link for it straight away. Nothing is charged and no stock moves until they pay.

https://medgrid.com/api/method/medgrid.customer_api_orders.create_order
ParameterTypeRequiredDescription
itemsJSON arrayyesOrder lines: [{"sku":"…","qty":2}]. Repeated SKUs are merged. Max 200 lines.
shipping_rulestringyesA value from get_shipping_options.
external_refstringyesYour own unique order id, 100 characters or fewer. This is the idempotency key — see below.
shipping_addressstringnoAn Address on your account. Defaults to your saved shipping address.
po_nostringnoYour purchase-order number, printed on the invoice.
notesstringnoFree text stored as a comment on the order.
customerstringnoAdmin-only override; omit for your own account.

Request

curl -X POST -H "Authorization: Bearer ACCESS_TOKEN" \
  --data-urlencode 'items=[{"sku":"VD3-5000-120","qty":2}]' \
  -d "shipping_rule=Standard Shipping" \
  -d "external_ref=PO-2026-0184" \
  "https://medgrid.com/api/method/medgrid.customer_api_orders.create_order"

Response · 200

{
  "ok": true,
  "order": "SO-2026-0015",
  "external_ref": "PO-2026-0184",
  "status": "Draft", "docstatus": 0,
  "payment_status": "Pending",
  "currency": "USD",
  "net_total": 37.50,
  "total_taxes_and_charges": 25.30,
  "grand_total": 62.80,
  "transaction_date": "2026-08-17",
  "delivery_date": "2026-08-24",
  "shipping_rule": "Standard Shipping",
  "lines": [
    { "sku": "VD3-5000-120", "item_name": "Vitamin D3 5000 IU (120 ct)",
      "qty": 2, "rate": 18.75, "amount": 37.50,
      "warehouse": "Main Warehouse - MG" }
  ],
  "payment_url": "https://medgrid.com/so-pay?so=SO-2026-0015&t=9f3c…",
  "payment_link_sent_on": "2026-08-17 14:02:11",
  "payment_link_expires_on": "2026-08-19 14:02:11",
  "payment_link_expired": false,
  "payment_email": "success",
  "duplicate_of_request": false,
  "possible_duplicate": false,
  "duplicate_of_order": "",
  "next_step": "Emailed to the customer with a payment link valid for 48 hours. Nothing is charged until they pay it; the order is released into fulfilment the moment they do. Poll medgrid.customer_api_orders.get_order_status, and call medgrid.customer_api_orders.request_payment_link if the link expires unpaid."
}
The payment email is automatic. The account receives the draft as a quotation PDF with a Pay now button pointing at payment_url — the same page a MedGrid rep's order uses, taking card or bank/ACH. You may also surface payment_url in your own UI; it is the identical link.
payment_email tells you whether it went out. The order itself is valid either way — never create a second one to compensate.
"success" — sent.
"no_email" — your account has no email on file. Add one, then call request_payment_link.
"email_failed" (or "failed") — a mail problem on our side. Call request_payment_link to try again.
"blocked" — the order cannot be paid yet: something on the account, such as an unverified NPI, would stop it at checkout. next_step names the reason, and a resend is refused until it is fixed on MedGrid.
Idempotency. Re-send the same external_ref and you get the original order back with duplicate_of_request: true — a timed-out request is always safe to retry, and you will never be charged twice. If you send the same basket under a different external_ref within 10 minutes we still create the order, but flag it with possible_duplicate: true and email your account to review both on the live system.
Stock is all-or-nothing. If any line exceeds what's available across your warehouses, nothing is created and the error names the SKU, what you asked for, and what's actually available — so your system can re-submit a valid quantity. We never partially fill or silently backorder.

When create_order fails

Every failure below leaves nothing created and nothing charged. What matters is which ones you may simply retry, and which need something changed first — retrying those in a loop just burns your rate limit.

What you getWhat to do
No response — timeout, dropped connectionRetry with the same external_ref. If the order did reach us you get it back with duplicate_of_request: true; if it didn't, it is created now. Never retry a timeout under a new ref — that is how you end up with two orders.
417 not enough stockThe message names each short SKU, what you asked for and what is actually available. Re-submit with quantities that fit, or drop the line — MedGrid never partially fills or backorders.
417 invalid shipping ruleCall get_shipping_options for this exact basket and use a shipping_rule it returns. The valid set depends on the basket — a biologic collapses it to one cold-chain method.
403 credentials not clearedYour account is missing a verified NPI, DEA or state licence for something in the basket. A person has to fix that on MedGrid; retrying changes nothing.
403 ordering not enabledOrdering is a separate opt-in from catalog access. Ask your administrator to tick Allow API Ordering.
429 rate limitedWait the Retry-After seconds, then retry — same external_ref as always.
A successful call that reports "payment_email": "blocked" is not a failure: the order exists and is valid, it just cannot be paid until the account issue in next_step is resolved. Don't create a second order — fix the account, then call request_payment_link.

MedGrid review No call from you

Payment is the normal way an order is released. Separately, a MedGrid administrator may release or refuse one by hand — there is nothing for your system to call here, just poll get_order_status.

When the order is released — by the customer paying the link, or by an administrator charging the card on file — it is submitted, an invoice is raised and you receive the same confirmation email as a website order. get_order_status then reports:

{
  "ok": true,
  "order": "SO-2026-0015",
  "status": "To Deliver and Bill", "docstatus": 1,
  "payment_status": "Paid",
  "rejection_reason": "",
  "grand_total": 62.80
}

If an administrator rejects it. Nothing is charged, the payment link stops working and the draft is closed. We email your account the reason, and your system can read the same text:

{
  "ok": true,
  "order": "SO-2026-0015",
  "status": "Rejected", "docstatus": 0,
  "payment_status": "Pending",
  "rejection_reason": "Out of allocation for this quarter.",
  "grand_total": 62.80
}
A rejected order is final. It cannot be re-submitted, re-confirmed or re-linked for payment. Resolve whatever the reason names and call create_order again with a new external_ref — reusing the old one just replays the rejected order.
Cards that need a challenge. The emailed payment page runs in the customer's browser, so a 3-D Secure prompt is completed there normally. It is only the administrator's off-session charge against a card on file that cannot answer one — if the bank asks, that charge fails, the order stays a safe unpaid draft and the customer can still pay the link.

GET get_order_status AuthOrdering opt-in

Current state of one of your orders — by MedGrid order id or by your own external_ref. This is how you learn whether the payment link has been paid, so poll it after create_order.

https://medgrid.com/api/method/medgrid.customer_api_orders.get_order_status

Request

curl -H "Authorization: Bearer ACCESS_TOKEN" \
  "https://medgrid.com/api/method/medgrid.customer_api_orders.get_order_status?external_ref=PO-2026-0184"

Response · 200

{
  "ok": true,
  "order": "SO-2026-0015",
  "external_ref": "PO-2026-0184",
  "status": "To Deliver and Bill", "docstatus": 1,
  "payment_status": "Paid",
  "payment_url": "https://medgrid.com/so-pay?so=SO-2026-0015&t=9f3c…",
  "payment_link_expires_on": "2026-08-19 14:02:11",
  "payment_link_expired": false,
  "rejection_reason": "",
  "grand_total": 62.80,
  "per_delivered_qty": 0.0,
  "per_billed_qty": 100.0,
  "invoices": ["ACC-SINV-2026-00031"],
  "lines": [ /* … as above … */ ]
}
payment_status has two endings, and only one of them is Paid. Stop polling when you reach any terminal value.
Unpaid — no payment link has been emailed yet.
Pending — the link is out and nobody has paid it. This does not mean a payment is on its way.
Processing — the customer confirmed a payment and the debit is clearing. Card payments normally settle straight to Paid; this state is what a bank/ACH debit sits in, and it can last days.
Paid — terminal. The money landed, docstatus is now 1 and the order is in fulfilment.
Failed — terminal. A debit that was clearing bounced. Nothing is charged and the order will not proceed on its own, so stop waiting and have the customer pay again; MedGrid alerts its admins too.
Expired — the payment session lapsed unpaid. Call request_payment_link for a fresh one.
Refunded — a settled payment was returned afterwards.
Orders that aren't yours return a plain 404 — we never confirm that another account's order id exists.
How prices are computed

Pricing model

Every price you see is computed from your contract price list — never another customer's, and never the underlying vendor cost.

The formula

For each item, MedGrid takes the rate from your account's price list and applies your agreed channel markup.

channel_price = price_list_rate × (1 + markup_pct / 100)
InputWhere it comes from
price_list_rateThe Item Price on your account's price list (your Customer's Default Price List).
markup_pctYour account's agreed channel markup. Often 0. Visible in whoami.
Example. A list rate of 15.00 with a 25% markup → 15.00 × 1.25 = 18.75. Prices are rounded to 2 decimal places and returned in your account currency.
When things go wrong

Errors & status codes

Failures return a clean, human-readable message — never a raw stack trace. The HTTP status and a stable code let you branch in software; the error string is safe to show a person.

Error shape

{
  "message": {
    "ok": false,
    "error": "Warehouse East DC - MG is not available to this customer.",
    "code": "PermissionError",
    "status": 403
  }
}
StatusCodeWhat it means
200—Success. Read ok: true and your data.
401AuthenticationErrorMissing/invalid credentials, or wrong client secret.
403PermissionErrorAccess inactive, a warehouse you're not allowed, ordering not switched on for your account, or a credential (NPI/DEA/licence) that doesn't clear for the items ordered.
404DoesNotExistErrorThe SKU isn't in your catalog, or the order isn't one of yours.
417ValidationErrorA required parameter is missing or malformed, not enough stock, or an invalid shipping rule.
429TooManyRequestsErrorToo many token requests, reads, or orders — see Rate limits below.
500InternalErrorUnexpected server error. Safe to retry; contact us if it persists.

Rate limits

Each limit is a fixed 60-second window. Go over one and you get 429 with a Retry-After header naming the seconds until that window rolls over.

CallsLimitCounted per
auth_token20 / minutesource IP
Every authenticated call — catalog and ordering120 / minuteaccount
The four ordering endpoints, additionally20 / minuteaccount
request_payment_link5 / minuteaccount
The two budgets stack; they are not separate pools. An ordering call is checked against the 120/minute limit first, then against the 20/minute one — so it spends both. Heavy catalog traffic therefore blocks ordering: burn 120 reads in a minute and the very next create_order comes back 429 without ever reaching its own allowance. Budget your polling with that in mind — get_order_status costs against both too. Poll every few seconds at most, never in a tight loop.
FAQ

Frequently asked

The short answers integrators ask for most.

Can I place orders through this API?

Yes, once it's switched on for you. Ordering is a separate opt-in: a MedGrid administrator ticks Allow API Ordering on your Customer record. You also need an email on file to receive the payment link, plus the same verified credentials (NPI, and DEA or a state licence for regulated classes) you'd need to check out on the storefront. You submit the order with create_order; we email the customer a payment link straight away, and paying it releases the order. Poll get_order_status, and use request_payment_link if the 48-hour link lapses unpaid. See Ordering endpoints. If ordering isn't enabled, those calls return 403 and the catalog endpoints keep working exactly as before.

What stops a network retry creating two orders?

Your external_ref. Send the same one and you get the original order back unchanged — no second order, no second charge. If you send a different ref but the same basket within 10 minutes, we still create the order (you may genuinely want two) but flag it with possible_duplicate: true and email your account so someone can review it on the live system. Re-issuing the payment link is safe to retry too — it re-opens the same link on the same order rather than creating another, and a paid order refuses a new link outright.

What happens if the payment link expires, or the card is declined?

Either way nothing is charged and the order stays a safe unpaid draft — it is never cancelled for you. The link is valid for 48 hours from the moment it is emailed (payment_link_expires_on in every response); after that it shows an "expired" page. Call request_payment_link and the customer gets a fresh 48-hour link by email, on the same order at the same prices. A declined card behaves the same: the customer can simply pay the link again, or with a different card or by bank/ACH.

How do I get my credentials?

A MedGrid administrator generates them from your Customer record (Customer API tab → Generate Credentials) and shares the client ID and secret with you securely. There's no self-service signup. They can reveal, regenerate or revoke them at any time.

Will I ever see another customer's prices or a vendor's cost?

Never. Prices are computed only from your own contract price list plus your agreed markup. The API has no path to other customers' data or to underlying vendor costs.

Do I need to send the customer parameter?

Normally no — your token identifies your account. It exists for admin tokens that manage multiple customers and need to target a specific one.

Need help integrating?

Our team will help you get credentials and test your integration — reach out through the contact page and choose "Platform Support".

Contact the integrations team