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.

POSTauth.token

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

FieldTypeDescription
client_idstringrequiredPublic client identifier (mgk_…).
client_secretstringrequiredThe 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".

POSTauth.refresh

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.

POSTsync.productsscope: sync

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)

FieldTypeDescription
skustringrequired*Item code. Either sku or vendor_product_id is required. item_code is accepted as an alias.
vendor_product_idstringrequired*Your external product id, used for dedupe.
namestringoptionalItem display name.
descriptionstringoptionalLong description.
groupstringoptionalItem group, auto-created if new.
pricenumberoptionalStandard rate.
costnumberoptionalValuation rate.
price_liststringoptionalWith price, writes a selling Item Price in the same call.
uomstringoptionalSet 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.

POSTsync.pricesscope: sync

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)

FieldTypeDescription
skustringrequiredMust be an existing Item owned by your account.
price_liststringrequiredMust exist, else 417.
pricenumberrequiredMust 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.

GETPOSTsync.product_statusscope: sync

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

POSTsync.inventoryscope: sync

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)

FieldTypeDescription
skustringrequiredAn Item owned by your account.
warehousestringrequiredA warehouse you own; one owned by another vendor is 403.
qtynumberrequiredNew on-hand quantity, zero or greater.
snapshot_atdatetimeoptionalISO 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".

GETPOSTsync.warehousesscope: sync

List the warehouses you own, or create one by sending a name.

https://medgrid.com/api/method/medgrid.api.v1.sync.warehouses

Orders & fulfilment

GETPOSTsync.ordersscope: sync

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

FieldTypeDescription
sincedate / datetimeoptionalDefaults to the last 7 days. Reach further back explicitly after a long outage.
statusstringoptionalsubmitted or cancelled. Omit for both.
pagenumberoptional1-based. Default 1.
page_lengthnumberoptionalDefault 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.

POSTsync.fulfillmentscope: sync

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)

FieldTypeDescription
sales_orderstringrequiredMust exist and contain at least one of your items.
tracking_numberstringoptionalDefaults to "Awaiting tracking" if omitted.
carrierstringoptionalUsed to derive a tracking URL when tracking_url is blank.
tracking_urlstringoptionalAuto-derived from carrier and number if blank.
statusstringoptionalYour carrier status text, matched to a MedGrid stage — see below.
estimated_delivery_datedateoptionalExpected delivery date.
notifybooloptionalDefaults 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".

POSTpharmacy.fulfillmentscope: pharmacy

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.

StageMatched when your status contains
Exceptionundeliverable, not delivered, cancel, refund, fail, exception, hold, error — checked first, so a failure never completes an order
Out for Deliveryout for delivery
Delivereddelivered anywhere in the text; or exactly delivery complete / delivery completed
In Transittransit
Dispatchedship, dispatch, label
Pendinganything 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.

POSTwebhooks.test_firescope: sync

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.

RuleLimit
Inbound API calls per vendor120 requests / minute
Batch records per request500, on every batch endpoint
Webhook delivery10 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.