Vendors integrating their own system — suppliers, distributors, manufacturers and 503A/503B pharmacies.
Vendor API
Sync your catalog, prices and stock, receive signed order webhooks, and report fulfilment.
Authentication
Generate credentials once on your Vendor Profile, exchange them for a one-hour bearer token, then call the sync endpoints with it. Your profile must be API-enabled, Approved and not suspended — status is re-checked on every request, so revocation is immediate.
Two scopes. sync is available to every API-enabled vendor. pharmacy is restricted to 503A/503B vendors and unlocks the Rx product attributes plus the pharmacy fulfilment callback. A non-pharmacy vendor using either is rejected with 422.
After 10 failed auth.token requests in a 15-minute window, further attempts for that client_id return HTTP 401 (AuthenticationError) with Retry-After. Wait the indicated number of seconds before retrying, even with valid credentials. The window starts with the first failure; a successful authentication clears the failure count before lockout.
Exchange client credentials for a one-hour bearer JWT scoped to your vendor account.
https://medgrid.com/api/method/medgrid.api.v1.auth.token
Parameters
| Field | Type | | Description |
|---|
client_id | string | required | Public client identifier (mgk_…). |
client_secret | string | required | The secret shown once at generation. |
Response
{ "message": {
"access_token": "eyJhbGciOiJIUzI1NiIs…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "sync"
} }
For a 503A/503B pharmacy the scope is "sync pharmacy".
Exchange a still-valid token for a fresh one without re-sending your client_secret. The existing token must not have expired.
https://medgrid.com/api/method/medgrid.api.v1.auth.refresh
Catalog sync
New products enter Pending and are not published until approved, unless your account is set to auto-approve. For products, prices and inventory, put a single record or a list of records under the endpoint's named wrapper key. A list returns a per-record breakdown, and one failing record never rolls back the others.
Send the request examples as JSON with Content-Type: application/json and Authorization: Bearer <token>. Keep the outer products, prices, inventory or data key shown in each example; the field tables describe the records inside it. Replace sample SKUs, price lists, warehouses and order ids with values from your account.
Batches are capped at 500 records on every batch endpoint — products, prices, inventory and product_status alike. A larger list is rejected outright with 429; split it into 500-record calls.
Create or update one or more products. Deduplication is by SKU, then by (vendor, vendor_product_id).
https://medgrid.com/api/method/medgrid.api.v1.sync.products
Product fields (inside products)
| Field | Type | | Description |
|---|
sku | string | required* | Item code. Either sku or vendor_product_id is required. item_code is accepted as an alias. |
vendor_product_id | string | required* | Your external product id, used for dedupe. |
name | string | optional | Item display name. |
description | string | optional | Long description. |
group | string | optional | Item group, auto-created if new. |
price | number | optional | Standard rate. |
cost | number | optional | Valuation rate. |
price_list | string | optional | With price, writes a selling Item Price in the same call. |
uom | string | optional | Set only when the item is first created; later calls cannot change it. An unknown unit is refused with 417 rather than silently swapped. Defaults to Nos. |
Request
{
"products": [
{ "sku": "EXAMPLE-SKU", "name": "Example product", "uom": "Nos" }
]
}
Rx attributes (ndc, dosage_form, route, dea_schedule, cold_chain, lot_number, batch_number, coa_url, is_rx, is_controlled, strength) are accepted only from 503A/503B vendors; anyone else sending one gets 422.
Upsert a selling Item Price. An unknown price list is rejected with 417.
https://medgrid.com/api/method/medgrid.api.v1.sync.prices
Price fields (inside prices)
| Field | Type | | Description |
|---|
sku | string | required | Must be an existing Item owned by your account. |
price_list | string | required | Must exist, else 417. |
price | number | required | Must be zero or greater. |
Request
{
"prices": [
{ "sku": "EXAMPLE-SKU", "price_list": "Your Selling Price List", "price": 25.00 }
]
}
The price is written against the item's stock UOM, echoed back as uom.
Approval and publish state for one or more of your items — the polling alternative to the product webhooks.
https://medgrid.com/api/method/medgrid.api.v1.sync.product_status
Branch on the shape, not on what you sent: a batch that resolves to exactly one item returns that item directly, with no total and no results wrapper.
Items owned by another vendor come back as found: false rather than an error.
Inventory & warehouses
Set on-hand quantity for one of your warehouses. The warehouse is auto-created on first use.
https://medgrid.com/api/method/medgrid.api.v1.sync.inventory
Inventory fields (inside inventory)
| Field | Type | | Description |
|---|
sku | string | required | An Item owned by your account. |
warehouse | string | required | A warehouse you own; one owned by another vendor is 403. |
qty | number | required | New on-hand quantity, zero or greater. |
snapshot_at | datetime | optional | ISO 8601. Defaults to now. Older than the stored snapshot is rejected as stale. |
Request
{
"inventory": { "sku": "EXAMPLE-SKU", "warehouse": "Your Warehouse", "qty": 20 }
}
An older snapshot never overwrites newer stock — it comes back applied: false with reason: "stale_snapshot".
List the warehouses you own, or create one by sending a name.
https://medgrid.com/api/method/medgrid.api.v1.sync.warehouses
Orders & fulfilment
Recent orders containing your items, newest first. This is the recovery path for order webhooks your endpoint missed — each order carries the same body the webhook would have delivered.
https://medgrid.com/api/method/medgrid.api.v1.sync.orders
Parameters
| Field | Type | | Description |
|---|
since | date / datetime | optional | Defaults to the last 7 days. Reach further back explicitly after a long outage. |
status | string | optional | submitted or cancelled. Omit for both. |
page | number | optional | 1-based. Default 1. |
page_length | number | optional | Default 20, capped at 100 — a larger value is clamped, not rejected. |
Filtering is on last modified, not order date, so an order cancelled today appears in a recent window even if it was placed months ago. That is what makes it work as a catch-up.
Report shipment and tracking for a Sales Order. You may only report on orders containing your own products.
https://medgrid.com/api/method/medgrid.api.v1.sync.fulfillment
Fulfilment fields (inside data)
| Field | Type | | Description |
|---|
sales_order | string | required | Must exist and contain at least one of your items. |
tracking_number | string | optional | Defaults to "Awaiting tracking" if omitted. |
carrier | string | optional | Used to derive a tracking URL when tracking_url is blank. |
tracking_url | string | optional | Auto-derived from carrier and number if blank. |
status | string | optional | Your carrier status text, matched to a MedGrid stage — see below. |
estimated_delivery_date | date | optional | Expected delivery date. |
notify | bool | optional | Defaults to true, which emails the customer whenever a tracking number is present (patient and provider on a 503A order). Send false when backfilling historical tracking. |
Request
{
"data": {
"sales_order": "EXAMPLE-SO",
"tracking_number": "YOUR-TRACKING-NUMBER",
"carrier": "UPS",
"status": "shipped"
}
}
tracking_status in the response is the stage MedGrid resolved, not the status you sent — "shipped" comes back as "Dispatched".
Functionally identical to sync.fulfillment but gated to 503A/503B vendors (422 otherwise). New integrations should prefer sync.fulfillment.
https://medgrid.com/api/method/medgrid.api.v1.pharmacy.fulfillment
Request
{
"data": {
"sales_order": "EXAMPLE-SO",
"tracking_number": "YOUR-TRACKING-NUMBER",
"carrier": "UPS",
"status": "shipped"
}
}
Use the same fulfilment fields as sync.fulfillment, inside the required data object.
Status vocabulary
Your status text is matched by keyword against the six MedGrid stages, so most carrier wording lands correctly without translation on your side. Matching ignores case and folds underscores and hyphens to spaces. The first row that matches wins.
| Stage | Matched when your status contains |
|---|
| Exception | undeliverable, not delivered, cancel, refund, fail, exception, hold, error — checked first, so a failure never completes an order |
| Out for Delivery | out for delivery |
| Delivered | delivered anywhere in the text; or exactly delivery complete / delivery completed |
| In Transit | transit |
| Dispatched | ship, dispatch, label |
| Pending | anything else, including a blank or omitted status |
An unrecognised status becomes Pending rather than an error — "Picked Up", for instance. Check the tracking_status you get back rather than assuming your wording was understood.
Webhooks
MedGrid posts signed JSON events to the Webhook URL on your Vendor Profile: product.approved, product.rejected, order.new, order.cancelled and webhook.test. Verify every request before processing it.
Signature verification
import hmac, hashlib
def verify(secret: str, body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)
Respond with any 2xx within 10 seconds. There are three delivery attempts in total — the original plus two retries, the first about 60 seconds later and the second about 5 minutes after that. All attempts are logged in the Vendor API Log.
created_at and cancelled_at are server-local timestamps with no offset. Do not parse them as UTC.
Deliver a signed webhook.test event to your configured URL right now and get your own endpoint's HTTP response back. Use it to prove signature verification works before a real order depends on it.
https://medgrid.com/api/method/medgrid.api.v1.webhooks.test_fire
Synchronous and never retried, so the result you get back is the whole story. With no webhook URL or secret set it returns ok: false with a reason rather than an error — check ok, not the HTTP status of the call itself.
Idempotency & limits
Send an Idempotency-Key header to make any write safe to retry: sync.products, sync.prices, sync.inventory, sync.vendor_details, sync.fulfillment, pharmacy.fulfillment, and creating a warehouse. The reads ignore it — they are already repeatable. Stored results expire after 24 hours, and a replay returns the original response with idempotent_replay: true added.
| Rule | Limit |
|---|
| Inbound API calls per vendor | 120 requests / minute |
| Batch records per request | 500, on every batch endpoint |
| Webhook delivery | 10 seconds per attempt; 3 attempts — the original, then ~60 s and ~5 min |
409 means two different things and the message says which: either the Idempotency-Key was reused with a different body, or a request carrying that key is still running (over 8 seconds). For the second, retry with the same key to collect the stored response.