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.
# 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"
Pull the curated list of products available to your account — paginated, searchable, filterable by group, each item already priced for you with live stock totals.
get_catalog →Get the exact prices your account pays — your contract price list plus any agreed markup. Price a list of SKUs, or fetch the whole catalog at once.
get_pricing →See available-to-sell quantities per warehouse for the locations you're authorised to ship from — so you only promise what you can actually fulfil.
get_availability →Send a basket straight from your own system. MedGrid prices it, checks stock and your credentials, returns the exact total, and emails the customer a payment link straight away. Paying it releases the order.
create_order →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.
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.
| Environment | Base URL | Use it for |
|---|---|---|
| UAT | https://uat.medgrid.com | Building and testing your integration. |
| Production | https://medgrid.com | Real orders, real money. |
https://medgrid.com, the site you are reading it on — so you can copy any of them as-is.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.
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
{ "message": {
"ok": true,
"access_token": "eyJhbGciOi…",
"token_type": "bearer",
"expires_in": 3600,
"scope": "customer_api",
"customer": "CUST-00042"
} }
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.
Authorization: Bearer ACCESS_TOKEN
A full call
curl -H "Authorization: Bearer ACCESS_TOKEN" \ "https://medgrid.com/api/method/medgrid.customer_api.whoami"
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.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).
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"
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"
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
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"
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"
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"
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.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"
create_order, so a tight loop will start returning 429 and block your next order.A few things that hold true across every endpoint.
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 } }
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.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.*.
Connectivity check. Echoes the resolved customer, price list, currency, markup, and allowed warehouses. Start here.
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"
}
Paginated, priced catalog of the items available to your account — stock summed across your allowed warehouses.
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | int | no | 1-based page number. Default 1. |
| page_length | int | no | Items per page. Default 50, max 200. |
| in_stock_only | 0 / 1 | no | When 1, only items with available stock. |
| item_group | string | no | Filter to one item group. |
| search | string | no | Match on item name or code. |
| customer | string | no | Admin-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.Full detail for a single product — including per-warehouse stock — but only if that SKU is in your catalog.
| Parameter | Type | Required | Description |
|---|---|---|---|
| sku | string | yes | The item code to look up. |
| customer | string | no | Admin-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 }
]
}
}
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.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).
| Parameter | Type | Required | Description |
|---|---|---|---|
| skus | CSV / JSON list | no | e.g. VD3-5000-120,OMEGA3-1000-90. Omit for the whole catalog. |
| customer | string | no | Admin-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 }
]
}
Available-to-sell quantities (projected stock) per warehouse. A requested warehouse must be one of your allowed warehouses, else 403.
| Parameter | Type | Required | Description |
|---|---|---|---|
| skus | CSV / JSON list | no | Limit to these SKUs. Omit for the whole catalog (capped). |
| warehouse | string | no | One of your allowed warehouses. Omit for all of them. |
| customer | string | no | Admin-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 }
}
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.The warehouses you may report availability for — the valid values for the warehouse parameter above.
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" }
]
}
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.
You submit the order; the customer pays the link we email them. No API call you make ever charges anyone.
| Step | Who | What happens |
|---|---|---|
| 1 | get_shipping_optionsyou | Tells you which shipping methods your basket qualifies for, and what each costs. |
| 2 | create_orderyou | 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. |
| 3 | MedGrid | The 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. |
| 4 | customer | They 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. |
| 5 | request_payment_linkyou | 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. |
| 6 | get_order_statusyou | Poll for payment, then for delivery and billing progress. |
get_order_status reports "status": "Draft", "docstatus": 0 and "payment_status": "Pending" ("Unpaid" only while no link has been emailed yet).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.get_order_status returns "status": "Rejected" and a rejection_reason, we email your account the same text, and nothing is charged.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.
| Parameter | Type | Required | Description |
|---|---|---|---|
| items | JSON array | yes | The basket, e.g. [{"sku":"VD3-5000-120","qty":2}]. |
| customer | string | no | Admin-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 }
]
}
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.
| Parameter | Type | Required | Description |
|---|---|---|---|
| items | JSON array | yes | Order lines: [{"sku":"…","qty":2}]. Repeated SKUs are merged. Max 200 lines. |
| shipping_rule | string | yes | A value from get_shipping_options. |
| external_ref | string | yes | Your own unique order id, 100 characters or fewer. This is the idempotency key — see below. |
| shipping_address | string | no | An Address on your account. Defaults to your saved shipping address. |
| po_no | string | no | Your purchase-order number, printed on the invoice. |
| notes | string | no | Free text stored as a comment on the order. |
| customer | string | no | Admin-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."
}
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.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.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 get | What to do |
|---|---|
| No response — timeout, dropped connection | Retry 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 stock | The 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 rule | Call 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 cleared | Your 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 enabled | Ordering is a separate opt-in from catalog access. Ask your administrator to tick Allow API Ordering. |
429 rate limited | Wait the Retry-After seconds, then retry — same external_ref as always. |
"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.Re-opens the payment link on an order that is still unpaid for a fresh 48-hour window, and emails it to the account again. Use it when the 48-hour link from create_order lapsed, or when that first email never went out.
| Parameter | Type | Required | Description |
|---|---|---|---|
| order | string | one of | The MedGrid order id, e.g. SO-2026-0015. |
| external_ref | string | one of | Your own order id, as sent to create_order. |
| customer | string | no | Admin-only override; omit for your own account. |
Request
curl -X POST -H "Authorization: Bearer ACCESS_TOKEN" \ -d "external_ref=PO-2026-0184" \ "https://medgrid.com/api/method/medgrid.customer_api_orders.request_payment_link"
Response · 200
{
"ok": true,
"order": "SO-2026-0015",
"external_ref": "PO-2026-0184",
"status": "Draft", "docstatus": 0,
"payment_status": "Pending",
"grand_total": 62.80,
"payment_url": "https://medgrid.com/so-pay?so=SO-2026-0015&t=9f3c…",
"payment_link_sent_on": "2026-08-20 09:15:40",
"payment_link_expires_on": "2026-08-22 09:15:40",
"payment_link_expired": false,
"payment_email": "success",
"next_step": "Emailed to the customer with a payment link valid for 48 hours. …"
}
417 with a plain reason ("Order SO-2026-0015 is already paid."). Orders that aren't yours return 404.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
}
create_order again with a new external_ref — reusing the old one just replays the rejected order.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.
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.404 — we never confirm that another account's order id exists.Every price you see is computed from your contract price list — never another customer's, and never the underlying vendor cost.
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)
| Input | Where it comes from |
|---|---|
| price_list_rate | The Item Price on your account's price list (your Customer's Default Price List). |
| markup_pct | Your account's agreed channel markup. Often 0. Visible in whoami. |
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.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
}
}
| Status | Code | What it means |
|---|---|---|
| 200 | — | Success. Read ok: true and your data. |
| 401 | AuthenticationError | Missing/invalid credentials, or wrong client secret. |
| 403 | PermissionError | Access 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. |
| 404 | DoesNotExistError | The SKU isn't in your catalog, or the order isn't one of yours. |
| 417 | ValidationError | A required parameter is missing or malformed, not enough stock, or an invalid shipping rule. |
| 429 | TooManyRequestsError | Too many token requests, reads, or orders — see Rate limits below. |
| 500 | InternalError | Unexpected server error. Safe to retry; contact us if it persists. |
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.
| Calls | Limit | Counted per |
|---|---|---|
auth_token | 20 / minute | source IP |
| Every authenticated call — catalog and ordering | 120 / minute | account |
| The four ordering endpoints, additionally | 20 / minute | account |
request_payment_link | 5 / minute | account |
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.The short answers integrators ask for most.
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.
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.
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.
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.
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.
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.
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