Customers and sales channels reselling MedGrid products.
Customer API
Pull your own catalog and pricing, and place orders from your own system.
Authentication
Credentials are issued by a MedGrid administrator from your Customer record — there is no self-service signup. You exchange them once for a one-hour token, then send that token on every call. A raw token client_id:client_secret header is rejected with 401; the Bearer token is the only accepted method.
| Environment | Base URL | Use it for |
|---|
| UAT | https://uat.medgrid.com | Building and testing. |
| Production | https://medgrid.com | Real orders, real money. |
The two are separate installations with separate databases, so credentials issued on one are unknown to the other. Ask for UAT credentials as well as production ones.
Cache the token; don't fetch one per call. auth_token is limited to 20 requests a minute counted per source IP, not per account — every server behind the same egress address shares that budget. An integration that authenticates before each request locks itself out inside a minute. Hold the token for its hour and re-authenticate only on a 401.
Exchange client credentials for a one-hour Bearer token.
https://medgrid.com/api/method/medgrid.customer_api.auth_token
Parameters
| Field | Type | | Description |
|---|
client_id | string | required | Public client identifier. |
client_secret | string | required | The secret shown once at generation. |
Request
curl -X POST \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
"$BASE/api/method/medgrid.customer_api.auth_token"
Response
{ "message": {
"ok": true,
"access_token": "eyJhbGciOi…",
"token_type": "bearer",
"expires_in": 3600,
"scope": "customer_api",
"customer": "CUST-00042"
} }
An unknown client_id and a wrong client_secret return the same 401, so the endpoint cannot be used to discover which accounts exist.
Conventions
Every response is wrapped by the framework in a message key, so the body is {"message": <result>} and each result carries an ok flag. The catalog endpoints are GET. auth_token, create_order and request_payment_link are POST-only; get_shipping_options and get_order_status are reads and accept GET.
Failure — never a raw stack trace
{ "message": { "ok": false, "error": "Human-readable message.",
"code": "PermissionError", "status": 403 } }
The customer parameter on every endpoint is optional — your token already identifies your account. It exists for admin tokens that manage several customers.
Catalog
Seven read endpoints under medgrid.customer_api.*, each scoped to your account: you only ever see your own catalog, your own prices and your own warehouses.
Connectivity check. Echoes the price list your prices come from, your currency, your markup and your allowed warehouses. Start here — everything downstream is computed from it.
https://medgrid.com/api/method/medgrid.customer_api.whoami
Response
{ "message": {
"ok": true,
"customer": "CUST-00042",
"status": "Active",
"item_selection": "All",
"price_list": "Sunrise Wholesale",
"currency": "USD",
"markup_pct": 0.0,
"warehouses": ["Main Warehouse - MG", "East DC - MG"]
} }
Paginated, priced catalog of the items available to your account, with stock summed across your allowed warehouses.
https://medgrid.com/api/method/medgrid.customer_api.get_catalog
Parameters
| Field | Type | | Description |
|---|
page | int | optional | 1-based page number. Default 1. |
page_length | int | optional | Items per page. Default 50, maximum 200. |
in_stock_only | 0 / 1 | optional | When 1, only items with available stock. |
item_group | string | optional | Filter to one item group. |
search | string | optional | Match on item name or code. |
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 one product, including per-warehouse stock — only if that SKU is in your catalog.
https://medgrid.com/api/method/medgrid.customer_api.get_catalog_item
Parameters
| Field | Type | | Description |
|---|
sku | string | required | The item code to look up. |
A SKU outside your catalog returns 404, not an empty result. Omitting sku returns a 417 asking for one.
Your account's prices. Pass skus to price a specific set, or omit it to price the whole catalog (capped at 500).
https://medgrid.com/api/method/medgrid.customer_api.get_pricing
Parameters
| Field | Type | | Description |
|---|
skus | CSV / JSON list | optional | Omit for the whole catalog. |
A requested SKU outside your catalog comes back with price: null and in_catalog: false rather than an error.
Available-to-sell quantities per warehouse.
https://medgrid.com/api/method/medgrid.customer_api.get_availability
Parameters
| Field | Type | | Description |
|---|
skus | CSV / JSON list | optional | Limit to these SKUs. |
warehouse | string | optional | One of your allowed warehouses; anything else is 403. |
Unlike get_pricing, a SKU outside your catalog is silently omitted from levels and totals rather than flagged. A SKU with no stock row anywhere is absent too, not reported as 0.
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
Ordering
Four endpoints under medgrid.customer_api_orders.*. They use the same Bearer token as the catalog, but must be switched on separately — ask your administrator to tick Allow API Ordering. Until then every call here returns 403.
- 1
You call get_shipping_options
Tells you which shipping methods your basket qualifies for and what each costs.
- 2
You call create_order
MedGrid validates the SKUs against your catalog, checks stock and credentials, prices every line and builds a draft. Nothing is charged and no stock moves.
- 3
MedGrid emails the customer
The same call emails the draft as a quotation carrying a payment link, valid for 48 hours.
- 4
The customer pays the link
Payment triggers automatic order submission and invoicing. If submission fails after settlement, the payment stays Paid and the order remains a draft for recovery.
- 5
You poll get_order_status
Check payment_status and docstatus separately, then monitor the order's delivery and billing progress. A paid draft needs MedGrid support, not another payment.
There is no customer-callable confirm endpoint. Payment triggers submission automatically, but Paid alone does not prove that submission succeeded.
The shipping methods available for a specific basket, with the exact amount that will be charged.
https://medgrid.com/api/method/medgrid.customer_api_orders.get_shipping_options
Parameters
| Field | Type | | Description |
|---|
items | JSON array | required | The basket, e.g. [{"sku":"VD3-5000-120","qty":2}]. |
The shipping_rule value it returns is what create_order expects; anything else is rejected. A basket containing a cellular or acellular biologic collapses to the single cold-chain overnight method.
Creates a draft order, returns its server-computed total, and emails the customer a payment link. Nothing is charged until they pay.
https://medgrid.com/api/method/medgrid.customer_api_orders.create_order
Parameters
| Field | Type | | Description |
|---|
items | JSON array | required | Order lines. Repeated SKUs are merged. Maximum 200 lines. |
shipping_rule | string | required | A value from get_shipping_options. |
external_ref | string | required | Your own order id, 100 characters or fewer. This is the idempotency key. |
shipping_address | string | optional | An Address on your account. Defaults to your saved shipping address. |
po_no | string | optional | Your purchase-order number, printed on the invoice. |
notes | string | optional | Free text stored as a comment on the order. |
Request
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" \
"$BASE/api/method/medgrid.customer_api_orders.create_order"
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. Retrying under a NEW ref is how you end up with two orders.
Stock is all-or-nothing. If any line exceeds what is available, nothing is created and the error names the SKU, what you asked for and what is actually available.
Re-opens the payment link on an unpaid order for a fresh 48-hour window, and emails it again.
https://medgrid.com/api/method/medgrid.customer_api_orders.request_payment_link
Parameters
| Field | Type | | Description |
|---|
order | string | required* | The MedGrid order id. |
external_ref | string | required* | Your own order id, as sent to create_order. |
It is the same link. An order has exactly one payment URL for its whole life; each call restarts its 48-hour window rather than minting a second URL, so a customer still holding the earlier email can pay from it.
Refused with 417 for an order that is already paid, cancelled or rejected. Throttled to 5 calls a minute because it sends mail.
Current state of one of your orders, by MedGrid order id or by your own external_ref.
https://medgrid.com/api/method/medgrid.customer_api_orders.get_order_status
Orders that aren't yours return a plain 404 — we never confirm that another account's order id exists.
payment_status reports settlement; docstatus reports the order document: 0 is draft, 1 is submitted, 2 is cancelled. If payment_status is Paid while docstatus is 0, contact MedGrid support for recovery. Do not create or pay a replacement order.
Payment lifecycle
The terminal values below end payment polling. Order submission and fulfilment have their own state: continue checking docstatus and the order status separately.
| Value | Terminal | What it means |
|---|
| Unpaid | no | No payment link has been emailed yet. |
| Pending | no | The link is out and nobody has paid it. This does not mean a payment is on its way. |
| Processing | no | The customer confirmed a payment and the debit is clearing. Cards normally settle straight to Paid; this is where a bank/ACH debit sits, and it can last days. |
| Paid | yes | The money landed. Submission can still fail, leaving docstatus 0 (a paid draft); check docstatus separately before treating the order as submitted. |
| Failed | yes | A debit that was clearing bounced. Nothing is charged and the order will not proceed on its own. |
| Expired | yes | The payment session lapsed unpaid. Call request_payment_link for a fresh one. |
| Refunded | yes | A settled payment was returned afterwards. |
An integration that polls for Paid and nothing else waits forever on a bounced debit. Treat Failed and Expired as stop conditions and surface them to a human.
A paid draft needs recovery, not another charge. If payment_status is Paid and docstatus is 0, contact MedGrid support. A submitted order (docstatus 1) still needs fulfilment monitoring; payment settlement does not mean delivery is complete.
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.
| 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 |
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. Burn 120 catalog reads in a minute and the next create_order returns 429 before it reaches its own allowance. get_order_status polling costs against both too.
Errors
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.
| Status | Code | What it means |
|---|
| 401 | AuthenticationError | Missing or invalid credentials, or an expired token. |
| 403 | PermissionError | Access inactive, a warehouse you're not allowed, ordering not switched on, or a credential (NPI/DEA/license) 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 | See Rate limits above. |
| 500 | InternalError | Unexpected server error. Safe to retry. |
A successful create_order that reports payment_email: "blocked" is not a failure — the order exists and is valid, it just cannot be paid until the account issue named in next_step is resolved. Fix the account and call request_payment_link; don't create a second order.