Vendor platform

Integrate with MedGrid.

Push products, prices, inventory, warehouses and your own profile into the MedGrid Marketplace. MedGrid pushes orders and approval results back to your system via signed webhooks. One REST surface, bearer-token auth, JSON in, JSON out. Every endpoint here ships in the live codebase.

terminal — your first call
# Exchange credentials for a 1-hour bearer token
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{ "client_id": "mgk_...", "client_secret": "..." }' \
  "https://uat.medgrid.com/api/method/medgrid.api.v1.auth.token"
What you can build

One vendor account, one API.

Start here

Authentication

Generate API credentials once on your Vendor Profile, exchange them for a 1-hour bearer token, then call the sync and pharmacy endpoints with that token. All endpoints live under https://uat.medgrid.com/api/method/medgrid.api.v1.*.

1 — Generate credentials

Open your Vendor ProfileAPI IntegrationInbound API (Vendor → MedGrid). Tick API Enabled and click Generate API Credentials. Copy all three values shown: client_id, client_secret, and webhook_secret — they are shown only once.

Store all three secrets immediately. None are retrievable after the dialog closes — only a 4-character preview is kept. Re-generating rotates all secrets and invalidates the previous ones. webhook_secret is used to verify the HMAC signature on every inbound webhook from MedGrid. Never expose any credential in browser code or version control.

2 — Exchange for a token

Token requests succeed only when your profile is API Enabled, in workflow_state = Approved, and not Suspended. Status is re-checked on every request, so revocation is immediate — not only at token issue.

request headers — every call after token
Authorization: Bearer <access_token>
Content-Type: application/json

POST auth.token public

Exchange your client credentials for a 1-hour bearer JWT scoped to your vendor account. Every token request (success or failure) is recorded in the Vendor API Log.

https://uat.medgrid.com/api/method/medgrid.api.v1.auth.token

Request

{
  "client_id": "mgk_175c4b0033b2bcb6cd0dc22b",
  "client_secret": "your-client-secret"
}

Parameters

FieldTypeDescription
client_idstringrequiredPublic client identifier (mgk_…).
client_secretstringrequiredThe secret shown once at generation.

Response

{
  "message": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "scope": "sync"
  }
}
For a 503A/503B pharmacy vendor the scope is "sync pharmacy". Tokens expire after 1 hour — refresh proactively or request a new one when you receive a 401.

POST auth.refresh public

Exchange a still-valid bearer token for a fresh 1-hour token — without re-sending your client_secret. The vendor's live status (Approved, not Suspended, API enabled) is re-checked on every refresh.

https://uat.medgrid.com/api/method/medgrid.api.v1.auth.refresh

Request

No body required. Pass your current token in the Authorization header.

request
Authorization: Bearer <current_access_token>
Content-Type: application/json

Response

{
  "message": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "scope": "sync"
  }
}
The existing token must not be expired. If it is, re-authenticate with auth.token. Refresh is recorded in the Vendor API Log.
Brute-force lockout. After 10 consecutive failed auth.token requests within a 15-minute window, the client_id is locked out for 15 minutes. Locked-out requests return 429 Too Many Requests with a Retry-After: <seconds> header. The counter resets on a successful authentication.
Getting started

Conventions

Patterns shared by every vendor endpoint.

HTTP method

Almost all vendor API endpoints are POST only. The one exception is sync.warehouses, which also accepts GET for listing warehouses. Calling any other endpoint with GET returns 403 Not permitted.

Response envelope

Every response wraps the return value in a message key.

{ "message": { ...result... } }

Single vs. batch

The product, price and inventory endpoints accept either a single object or a list. A list returns a per-record success/failure breakdown — see Batch operations.

Dates & times

Datetimes use ISO 8601 (2026-06-26T10:00:00); dates use YYYY-MM-DD.

Two scopes. sync is available to every API-enabled vendor. pharmacy is restricted to 503A / 503B pharmacy vendors and unlocks Rx product attributes plus the fulfillment callback. Non-pharmacy vendors using a pharmacy field or endpoint are rejected with 422.
Sync API

Catalog sync

Push your product catalog and selling prices into the marketplace. New products enter Pending Approval and are not published until approved (unless your account is set to auto-approve).

POST sync.products auth scope: sync

Create or update one or more products (Items). Deduplication is by SKU, then by (vendor, vendor_product_id). Accepts a single object or a list.

https://uat.medgrid.com/api/method/medgrid.api.v1.sync.products

Request

{
  "products": [
    {
      "sku": "SKY_SKU_1",
      "name": "Skydell Test Item",
      "description": "30-count bottle",
      "group": "Products",
      "price": 19.99,
      "cost": 8.50,
      "vendor_product_id": "EXT-1001",
      "price_list": "Standard Selling"
    }
  ]
}

Parameters (per product)

FieldTypeDescription
sku / item_codestringrequired*Item code. *Either sku or vendor_product_id is required.
vendor_product_idstringrequired*Your external product id, used for dedupe.
namestringoptionalItem display name.
descriptionstringoptionalLong description.
imagestringoptionalImage URL.
groupstringoptionalItem group (auto-created if new).
pricenumberoptionalStandard rate.
costnumberoptionalValuation rate.
price_liststringoptionalIf set together with price, a selling Item Price is written in the same call.

Response

{
  "message": {
    "total": 1, "succeeded": 1, "failed": 0,
    "results": [
      { "index": 0, "ok": true,
        "result": { "sku": "SKY_SKU_1", "created": true, "workflow_state": "Pending", "published": false } }
    ]
  }
}

Sending a single object instead of a list returns the bare result object (no batch wrapper).

Pharmacy-only fields. 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. A non-pharmacy vendor sending any of them gets 422.

POST sync.prices auth scope: sync

Upsert a selling Item Price for one or more products. An unknown price list is rejected with 417.

https://uat.medgrid.com/api/method/medgrid.api.v1.sync.prices

Request

{
  "prices": [
    { "sku": "SKY_SKU_1", "price": 24.50, "price_list": "Standard Selling" }
  ]
}

Parameters (per price)

FieldTypeDescription
skustringrequiredMust be an existing Item owned by your vendor account.
price_liststringrequiredMust exist, else 417.
pricenumberrequiredSelling price. Must be ≥ 0 — negative values are rejected with 417.

Response

{ "message": { "sku": "SKY_SKU_1", "price_list": "Standard Selling", "price": 24.5 } }

POST sync.product_status auth scope: sync

Check the MedGrid approval state of one or more products you own. Look up by sku (Item code) or vendor_product_id. Batch up to 500 items per call. Useful for polling after initial sync without subscribing to webhooks.

https://uat.medgrid.com/api/method/medgrid.api.v1.sync.product_status

Request — single item

{ "sku": "SKY_SKU_1" }

Request — batch

{
  "items": [
    { "sku": "SKY_SKU_1" },
    { "vendor_product_id": "EXT-1002" }
  ]
}

Parameters

FieldTypeDescription
skustringrequired*Item code. *Either sku or vendor_product_id required per item.
vendor_product_idstringrequired*Your external product id.
itemsarrayoptionalBatch lookup — max 500. When present, wraps response in a results array.

Response — single

{
  "message": {
    "sku": "SKY_SKU_1",
    "item_name": "Nitrile Gloves L",
    "workflow_state": "Approved",
    "published": true,
    "found": true
  }
}

Response — batch

{
  "message": {
    "results": [
      { "sku": "SKY_SKU_1", "item_name": "Nitrile Gloves L", "workflow_state": "Approved", "published": true, "found": true },
      { "vendor_product_id": "EXT-1002", "found": false }
    ]
  }
}
Only items belonging to your vendor account are returned. Items owned by other vendors return "found": false. You only need to poll here if you prefer not to receive product.approved / product.rejected webhooks.
Sync API

Inventory & warehouses

Mirror your warehouse stock and manage the warehouses you own. A vendor can only ever read or write its own warehouses — a warehouse owned by another vendor (or by the company) is rejected with 403.

POST sync.inventory auth scope: sync

Set on-hand quantity for one of your warehouses. The warehouse is auto-created on first use. Older snapshots never overwrite newer stock.

https://uat.medgrid.com/api/method/medgrid.api.v1.sync.inventory

Request

{
  "inventory": [
    {
      "sku": "SKY_SKU_1",
      "warehouse": "SKY_WH_1 - MG",
      "qty": 100,
      "snapshot_at": "2026-06-26T10:00:00"
    }
  ]
}

Parameters (per row)

FieldTypeDescription
skustringrequiredMust be an existing Item owned by your vendor account.
warehousestringrequiredA warehouse you own. Auto-created if it doesn't exist; 403 if owned by another vendor.
qtynumberrequiredNew on-hand quantity. Must be ≥ 0 — negative values are rejected with 417.
snapshot_atdatetimeoptionalISO 8601 timestamp. Defaults to now. Older than the stored snapshot → rejected (stale).

Response — applied

{
  "message": {
    "sku": "SKY_SKU_1", "warehouse": "SKY_WH_1 - MG",
    "qty": 100.0, "applied": true, "snapshot_at": "2026-06-26T10:00:00"
  }
}

Response — stale snapshot (not applied)

{
  "message": {
    "sku": "SKY_SKU_1", "warehouse": "SKY_WH_1 - MG",
    "applied": false, "reason": "stale_snapshot",
    "current_snapshot_at": "2026-06-26T10:00:00",
    "current_qty": 85.0
  }
}

POST sync.warehouses auth scope: sync

List the warehouses you own, or create a new one. Created warehouses are stamped with your vendor id (mg_vendor) — only you can write inventory into them.

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

List your warehouses — empty body (or omit name)

{}

// →
{
  "message": {
    "vendor": "s299avsi27",
    "warehouses": [
      { "name": "SKY_WH_1 - MG", "warehouse_name": "SKY_WH_1", "company": "MedGrid", "disabled": 0 }
    ]
  }
}

Create a warehouse — send a name

{ "name": "SKY_WH_2" }

// →
{ "message": { "warehouse": "SKY_WH_2 - MG", "created": true } }
FieldTypeDescription
namestringoptionalIf given, creates (or returns) a warehouse you own. If omitted, lists your warehouses. The company abbreviation is appended automatically to the stored name.
Sync API

Vendor profile

Update your own Vendor Profile. Pharmacy compliance fields are writable only by 503A/503B vendors — a non-pharmacy vendor sending any pharmacy-only field gets 422.

POST sync.vendor_details auth scope: sync

Always a single object — the caller's own profile.

https://uat.medgrid.com/api/method/medgrid.api.v1.sync.vendor_details

Request

{
  "details": {
    "webhook_url": "https://skydell.test/hook",
    "phone": "555-0100",
    "address": "1 Main St",
    "zip": "10001"
  }
}

Parameters

FieldTypePharmacy-onlyDescription
webhook_urlstringNoWhere MedGrid posts HMAC-signed outbound events.
phonestringNoContact phone.
addressstringNoStreet address.
zipstringNoPostal code.
npistringYes10-digit National Provider Identifier.
dea_numberstringYesDEA registration number.
ncpdp_idstringYesNCPDP identifier.
state_licensestringYesState pharmacy license number.
license_expirydateYesLicense expiry date.
controlled_substanceboolYesHandles controlled substances.

Response

{ "message": { "vendor": "s299avsi27", "updated": ["address", "phone", "webhook_url", "zip"] } }
All vendors

Fulfillment

After MedGrid sends you an order.new webhook, confirm and ship the order in your system, then POST tracking back here. Available to every vendor class — Supplier, Distributor, Manufacturer, 503A, 503B.

POST sync.fulfillment auth scope: sync

Report shipment and tracking for a Sales Order. You may only report on orders containing your own products (403 otherwise). Repeated calls with the same sales_order update the same tracking row (keyed on source: "Vendor API").

https://uat.medgrid.com/api/method/medgrid.api.v1.sync.fulfillment

Request

{
  "data": {
    "sales_order": "SO-2026-00456",
    "tracking_number": "1Z999AA10123456784",
    "carrier": "UPS",
    "status": "shipped",
    "tracking_url": "https://www.ups.com/track?tracknum=1Z999AA10123456784",
    "estimated_delivery_date": "2026-07-05"
  }
}

Parameters

FieldTypeDescription
sales_orderstringrequiredMust exist and contain at least one of your items.
tracking_numberstringoptionalDefaults to "Awaiting tracking" if omitted.
carrierstringoptionalUsed to auto-derive a tracking URL when tracking_url is blank.
tracking_urlstringoptionalAuto-derived from carrier + tracking_number if blank.
statusstringoptionalshipped · in_transit · delivered · exception — mapped to a MedGrid tracking stage.
estimated_delivery_datedateoptionalExpected delivery date (YYYY-MM-DD).

Response

{
  "message": {
    "sales_order": "SO-2026-00456",
    "tracking_number": "1Z999AA10123456784",
    "tracking_status": "Shipped"
  }
}
Pharmacy API — 503A / 503B only

Pharmacy fulfillment

503A/503B vendors may also use the pharmacy-scoped fulfillment endpoint below. It is functionally identical to sync.fulfillment above but is gated to the pharmacy scope. New integrations should prefer sync.fulfillment.

POST pharmacy.fulfillment auth scope: pharmacy

Restricted to 503A/503B vendors (422 otherwise). You may only report on orders containing your own products (403 otherwise).

https://uat.medgrid.com/api/method/medgrid.api.v1.pharmacy.fulfillment

Request

{
  "data": {
    "sales_order": "SO-2026-00456",
    "tracking_number": "1Z999AA10123456784",
    "carrier": "UPS",
    "status": "Shipped",
    "tracking_url": "https://www.ups.com/track?tracknum=1Z999AA10123456784",
    "estimated_delivery_date": "2026-06-30"
  }
}

Parameters

FieldTypeDescription
sales_orderstringrequiredMust exist and contain one of your items.
tracking_numberstringoptionalDefaults to "Awaiting tracking" if omitted.
carrierstringoptionalUsed to derive a tracking URL.
tracking_urlstringoptionalAuto-derived from carrier + number if blank.
statusstringoptionalMapped to a MedGrid tracking stage.
estimated_delivery_datedateoptionalETA.

Response

{
  "message": {
    "sales_order": "SO-2026-00456",
    "tracking_number": "1Z999AA10123456784",
    "tracking_status": "Shipped"
  }
}
MedGrid → Your system

Outbound webhooks

MedGrid posts signed JSON events to the Webhook URL on your Vendor Profile. Configure it via Vendor Profile → API Integration → Inbound API. Verify every request before processing it.

Signature verification

Every request carries an X-MedGrid-Signature header. Compute sha256=HMAC-SHA256(webhook_secret, raw_request_body) and compare. Reject any request where the values do not match.

# Python example
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)
Always verify signatures. Reject any request where the header is missing or the digest does not match your computed value. Use a constant-time comparison to prevent timing attacks.

Common envelope

All events share the same top-level envelope.

{
  "event":      "order.new",           // event type
  "vendor":     "VP-00023",            // your Vendor Profile name
  "created_at": "2026-06-29 14:02:11",  // UTC timestamp
  "attempt":    1,                      // delivery attempt number (1–3)
  "data":       { /* event-specific payload — see below */ }
}
MedGrid also sends an X-MedGrid-Attempt HTTP header with the attempt number. If your endpoint returns a non-2xx response or times out after 10 seconds, delivery is retried up to 3 times total with exponential backoff: first retry after 60 s, second after 5 min, third after 15 min. Always respond within 10 seconds — use a background queue if processing takes longer.

Event: product.approved

Fired when a MedGrid Admin changes an item's workflow state to Approved. The item is now live on the marketplace.

{
  "event": "product.approved",
  "vendor": "VP-00023",
  "created_at": "2026-06-29 14:02:11",
  "attempt": 1,
  "data": {
    "sku":               "ITM-00831",       // MedGrid Item code
    "item_name":         "Nitrile Gloves L",
    "vendor_product_id": "SKY-GLOVE-L",    // your stable key (blank if not set)
    "workflow_state":    "Approved",
    "previous_state":   "Pending"
  }
}

Event: product.rejected

Fired when a MedGrid Admin changes an item's workflow state to Rejected. The item is not published. Check rejection_reason and re-submit after correcting the issue.

{
  "event": "product.rejected",
  "vendor": "VP-00023",
  "created_at": "2026-06-29 14:09:44",
  "attempt": 1,
  "data": {
    "sku":               "ITM-00831",
    "item_name":         "Nitrile Gloves L",
    "vendor_product_id": "SKY-GLOVE-L",
    "workflow_state":    "Rejected",
    "previous_state":   "Pending",
    "rejection_reason":  "Missing NDC number"  // blank if no reason was recorded
  }
}
FieldTypeNotes
skustringMedGrid Item code.
vendor_product_idstring or blankThe stable key you sent in the sync call.
workflow_statestringNew state: Approved or Rejected.
previous_statestringState before the admin action (e.g. Pending).
rejection_reasonstringHuman-readable reason set by the Admin. Present only on product.rejected.
You can also poll sync.product_status instead of waiting for these webhooks — useful during initial integration testing.

Event: order.new

Fired when a Sales Order containing your products is submitted. The payload contains only your line items — other vendors' items are never exposed. Respond by fulfilling the order and calling sync.fulfillment with tracking.

{
  "event": "order.new",
  "vendor": "VP-00023",
  "created_at": "2026-06-29 14:05:33",
  "data": {
    "sales_order":   "SO-2026-00456",
    "order_date":    "2026-06-29",
    "delivery_date": "2026-07-05",
    "currency":      "USD",
    "ship_to": {
      "address_line1": "500 Main St",
      "address_line2": "",
      "city":          "Boston",
      "state":         "MA",
      "pincode":       "02101",
      "country":       "United States"
    },
    "line_items": [
      {
        "item_code":  "ITM-00831",
        "item_name":  "Nitrile Gloves L",
        "sku":        "ITM-00831",
        "qty":        2,
        "uom":        "Box",
        "rate":       14.99,
        "amount":     29.98,
        "warehouse":  "Finished Goods - MG"
      }
    ]
  }
}
FieldTypeNotes
sales_orderstringPass this to sync.fulfillment when you ship.
ship_toobjectEmpty object if no shipping address is set on the order.
line_itemsarrayOnly items belonging to your vendor account.
rate / amountnumberSelling price per unit and line total in currency.
Respond with any 2xx status within 10 seconds. If your endpoint is slow or unavailable, MedGrid retries delivery up to 3 times (60 s → 5 min → 15 min). All attempts are logged in the Vendor API Log.

Event: order.cancelled

Fired when a Sales Order is cancelled after being submitted. Stop any in-progress fulfillment and reconcile stock if goods have already shipped.

{
  "event": "order.cancelled",
  "vendor": "VP-00023",
  "created_at": "2026-06-29 15:30:00",
  "attempt": 1,
  "data": {
    "sales_order":   "SO-2026-00456",
    "cancelled_at":  "2026-06-29 15:29:58",
    "order_date":    "2026-06-29",
    "currency":      "USD",
    "line_items": [
      {
        "item_code":  "ITM-00831",
        "item_name":  "Nitrile Gloves L",
        "sku":        "ITM-00831",
        "qty":        2,
        "uom":        "Box",
        "rate":       14.99,
        "amount":     29.98,
        "warehouse":  "Finished Goods - MG"
      }
    ]
  }
}
FieldTypeNotes
sales_orderstringThe same order name sent in order.new.
cancelled_atdatetimeUTC timestamp of cancellation.
line_itemsarrayOnly your items — same shape as order.new.
Reference

Reference

Idempotency, batch processing, error codes and rate limits.

Idempotency

Make any sync or pharmacy call safe to retry by sending an Idempotency-Key header.

Idempotency-Key: <your-unique-key>
BehaviourResult
Replay — same key + same bodyReturns the original response with "idempotent_replay": true added.
Conflict — same key + different bodyReturns 409.
TTLStored results expire after 24 hours.

Replayed response

{
  "message": {
    "sku": "SKY_IDEM", "created": true,
    "workflow_state": "Pending", "published": false,
    "idempotent_replay": true
  }
}

Batch operations

Send a list to sync.products, sync.prices or sync.inventory to process many records in one call. Each record is isolated with a savepoint — one failure never rolls back the others.

Request

{
  "products": [
    { "sku": "SKY_B_OK", "name": "Good Row", "price": 5 },
    { "sku": "", "name": "Bad Row" }
  ]
}

Response

{
  "message": {
    "total": 2, "succeeded": 1, "failed": 1,
    "results": [
      { "index": 0, "ok": true,
        "result": { "sku": "SKY_B_OK", "created": true, "workflow_state": "Pending", "published": false } },
      { "index": 1, "ok": false,
        "error": "sku or vendor_product_id is required", "error_type": "ValidationError" }
    ]
  }
}

Error reference

StatusMeaningTypical cause
200OKRequest succeeded.
401UnauthorizedMissing, malformed, invalid or expired bearer token; bad client credentials.
403ForbiddenAPI disabled / vendor not Approved / suspended; warehouse owned by another vendor; order not yours; wrong HTTP method.
409ConflictIdempotency-Key reused with a different request body.
417Expectation FailedValidation error — missing/blank body, unknown item, unknown price list, unknown warehouse.
422Unprocessable EntityPharmacy-only field or endpoint used by a non-pharmacy vendor.
429Too Many RequestsRate limit exceeded or brute-force lockout active. Check the Retry-After response header.

Error shape

{
  "exception": "frappe.exceptions.PermissionError: Warehouse SKY_WH_1 - MG is not owned by your vendor account",
  "exc_type": "PermissionError"
}

Rate limits

Requests are rate-limited per authenticated vendor. Exceeding the limit returns 429 Too Many Requests with a Retry-After: <seconds> header indicating when you may retry.

RuleLimit
Inbound API calls per vendor120 requests / minute
Batch records per request500 items max (sync.product_status); no hard limit on product/price/inventory batches but kept reasonable
Webhook delivery timeout10 seconds per attempt; 3 attempts max
On 429, read the Retry-After header and wait that many seconds before retrying. Always combine retries with an Idempotency-Key so duplicate requests are safely de-duplicated.

Need help integrating?

Our integrations team will pair with you on schema, test data, and webhook setup — reach out through the contact page and choose "Platform Support".

Contact the integrations team