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.
# 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"
Upsert products and selling prices in single calls or batches. New products enter Pending Approval and publish once reviewed. Deduplication by SKU or your own external product id.
Catalog APIs →Mirror warehouse stock from your own system and manage the warehouses you own. Stale-snapshot protection means out-of-order updates never overwrite newer stock.
Inventory APIs →Receive order webhooks when your products sell and report shipment tracking back to MedGrid. Available to every vendor class — not just pharmacies.
Fulfillment APIs →MedGrid sends signed webhook events to your system — product approval results and new orders. Verify the X-MedGrid-Signature header on every inbound request.
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.*.
Open your Vendor Profile → API Integration → Inbound 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.
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.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.
Authorization: Bearer <access_token> Content-Type: application/json
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.
{
"client_id": "mgk_175c4b0033b2bcb6cd0dc22b",
"client_secret": "your-client-secret"
}
| Field | Type | Description | |
|---|---|---|---|
client_id | string | required | Public client identifier (mgk_…). |
client_secret | string | required | The secret shown once at generation. |
{
"message": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "sync"
}
}
"sync pharmacy". Tokens expire after 1 hour — refresh proactively or request a new one when you receive a 401.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.
No body required. Pass your current token in the Authorization header.
Authorization: Bearer <current_access_token> Content-Type: application/json
{
"message": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "sync"
}
}
auth.token. Refresh is recorded in the Vendor API Log.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.Patterns shared by every vendor endpoint.
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.
Every response wraps the return value in a message key.
{ "message": { ...result... } }
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.
Datetimes use ISO 8601 (2026-06-26T10:00:00); dates use YYYY-MM-DD.
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.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).
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.
{
"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"
}
]
}
| Field | Type | Description | |
|---|---|---|---|
sku / item_code | string | required* | Item code. *Either sku or vendor_product_id is required. |
vendor_product_id | string | required* | Your external product id, used for dedupe. |
name | string | optional | Item display name. |
description | string | optional | Long description. |
image | string | optional | Image URL. |
group | string | optional | Item group (auto-created if new). |
price | number | optional | Standard rate. |
cost | number | optional | Valuation rate. |
price_list | string | optional | If set together with price, a selling Item Price is written in the same call. |
{
"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).
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.Upsert a selling Item Price for one or more products. An unknown price list is rejected with 417.
{
"prices": [
{ "sku": "SKY_SKU_1", "price": 24.50, "price_list": "Standard Selling" }
]
}
| Field | Type | Description | |
|---|---|---|---|
sku | string | required | Must be an existing Item owned by your vendor account. |
price_list | string | required | Must exist, else 417. |
price | number | required | Selling price. Must be ≥ 0 — negative values are rejected with 417. |
{ "message": { "sku": "SKY_SKU_1", "price_list": "Standard Selling", "price": 24.5 } }
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.
{ "sku": "SKY_SKU_1" }
{
"items": [
{ "sku": "SKY_SKU_1" },
{ "vendor_product_id": "EXT-1002" }
]
}
| Field | Type | Description | |
|---|---|---|---|
sku | string | required* | Item code. *Either sku or vendor_product_id required per item. |
vendor_product_id | string | required* | Your external product id. |
items | array | optional | Batch lookup — max 500. When present, wraps response in a results array. |
{
"message": {
"sku": "SKY_SKU_1",
"item_name": "Nitrile Gloves L",
"workflow_state": "Approved",
"published": true,
"found": true
}
}
{
"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 }
]
}
}
"found": false. You only need to poll here if you prefer not to receive product.approved / product.rejected webhooks.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.
Set on-hand quantity for one of your warehouses. The warehouse is auto-created on first use. Older snapshots never overwrite newer stock.
{
"inventory": [
{
"sku": "SKY_SKU_1",
"warehouse": "SKY_WH_1 - MG",
"qty": 100,
"snapshot_at": "2026-06-26T10:00:00"
}
]
}
| Field | Type | Description | |
|---|---|---|---|
sku | string | required | Must be an existing Item owned by your vendor account. |
warehouse | string | required | A warehouse you own. Auto-created if it doesn't exist; 403 if owned by another vendor. |
qty | number | required | New on-hand quantity. Must be ≥ 0 — negative values are rejected with 417. |
snapshot_at | datetime | optional | ISO 8601 timestamp. Defaults to now. Older than the stored snapshot → rejected (stale). |
{
"message": {
"sku": "SKY_SKU_1", "warehouse": "SKY_WH_1 - MG",
"qty": 100.0, "applied": true, "snapshot_at": "2026-06-26T10:00:00"
}
}
{
"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
}
}
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.
name){}
// →
{
"message": {
"vendor": "s299avsi27",
"warehouses": [
{ "name": "SKY_WH_1 - MG", "warehouse_name": "SKY_WH_1", "company": "MedGrid", "disabled": 0 }
]
}
}
name{ "name": "SKY_WH_2" }
// →
{ "message": { "warehouse": "SKY_WH_2 - MG", "created": true } }
| Field | Type | Description | |
|---|---|---|---|
name | string | optional | If given, creates (or returns) a warehouse you own. If omitted, lists your warehouses. The company abbreviation is appended automatically to the stored name. |
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.
Always a single object — the caller's own profile.
{
"details": {
"webhook_url": "https://skydell.test/hook",
"phone": "555-0100",
"address": "1 Main St",
"zip": "10001"
}
}
| Field | Type | Pharmacy-only | Description |
|---|---|---|---|
webhook_url | string | No | Where MedGrid posts HMAC-signed outbound events. |
phone | string | No | Contact phone. |
address | string | No | Street address. |
zip | string | No | Postal code. |
npi | string | Yes | 10-digit National Provider Identifier. |
dea_number | string | Yes | DEA registration number. |
ncpdp_id | string | Yes | NCPDP identifier. |
state_license | string | Yes | State pharmacy license number. |
license_expiry | date | Yes | License expiry date. |
controlled_substance | bool | Yes | Handles controlled substances. |
{ "message": { "vendor": "s299avsi27", "updated": ["address", "phone", "webhook_url", "zip"] } }
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.
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").
{
"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"
}
}
| 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 auto-derive a tracking URL when tracking_url is blank. |
tracking_url | string | optional | Auto-derived from carrier + tracking_number if blank. |
status | string | optional | shipped · in_transit · delivered · exception — mapped to a MedGrid tracking stage. |
estimated_delivery_date | date | optional | Expected delivery date (YYYY-MM-DD). |
{
"message": {
"sales_order": "SO-2026-00456",
"tracking_number": "1Z999AA10123456784",
"tracking_status": "Shipped"
}
}
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.
Restricted to 503A/503B vendors (422 otherwise). You may only report on orders containing your own products (403 otherwise).
{
"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"
}
}
| Field | Type | Description | |
|---|---|---|---|
sales_order | string | required | Must exist and contain one of your items. |
tracking_number | string | optional | Defaults to "Awaiting tracking" if omitted. |
carrier | string | optional | Used to derive a tracking URL. |
tracking_url | string | optional | Auto-derived from carrier + number if blank. |
status | string | optional | Mapped to a MedGrid tracking stage. |
estimated_delivery_date | date | optional | ETA. |
{
"message": {
"sales_order": "SO-2026-00456",
"tracking_number": "1Z999AA10123456784",
"tracking_status": "Shipped"
}
}
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.
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)
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 */ }
}
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.product.approvedFired 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"
}
}
product.rejectedFired 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
}
}
| Field | Type | Notes |
|---|---|---|
sku | string | MedGrid Item code. |
vendor_product_id | string or blank | The stable key you sent in the sync call. |
workflow_state | string | New state: Approved or Rejected. |
previous_state | string | State before the admin action (e.g. Pending). |
rejection_reason | string | Human-readable reason set by the Admin. Present only on product.rejected. |
sync.product_status instead of waiting for these webhooks — useful during initial integration testing.order.newFired 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"
}
]
}
}
| Field | Type | Notes |
|---|---|---|
sales_order | string | Pass this to sync.fulfillment when you ship. |
ship_to | object | Empty object if no shipping address is set on the order. |
line_items | array | Only items belonging to your vendor account. |
rate / amount | number | Selling price per unit and line total in currency. |
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.order.cancelledFired 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"
}
]
}
}
| Field | Type | Notes |
|---|---|---|
sales_order | string | The same order name sent in order.new. |
cancelled_at | datetime | UTC timestamp of cancellation. |
line_items | array | Only your items — same shape as order.new. |
Idempotency, batch processing, error codes and rate limits.
Make any sync or pharmacy call safe to retry by sending an Idempotency-Key header.
| Behaviour | Result |
|---|---|
| Replay — same key + same body | Returns the original response with "idempotent_replay": true added. |
| Conflict — same key + different body | Returns 409. |
| TTL | Stored results expire after 24 hours. |
{
"message": {
"sku": "SKY_IDEM", "created": true,
"workflow_state": "Pending", "published": false,
"idempotent_replay": true
}
}
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.
{
"products": [
{ "sku": "SKY_B_OK", "name": "Good Row", "price": 5 },
{ "sku": "", "name": "Bad Row" }
]
}
{
"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" }
]
}
}
| Status | Meaning | Typical cause |
|---|---|---|
200 | OK | Request succeeded. |
401 | Unauthorized | Missing, malformed, invalid or expired bearer token; bad client credentials. |
403 | Forbidden | API disabled / vendor not Approved / suspended; warehouse owned by another vendor; order not yours; wrong HTTP method. |
409 | Conflict | Idempotency-Key reused with a different request body. |
417 | Expectation Failed | Validation error — missing/blank body, unknown item, unknown price list, unknown warehouse. |
422 | Unprocessable Entity | Pharmacy-only field or endpoint used by a non-pharmacy vendor. |
429 | Too Many Requests | Rate limit exceeded or brute-force lockout active. Check the Retry-After response header. |
{
"exception": "frappe.exceptions.PermissionError: Warehouse SKY_WH_1 - MG is not owned by your vendor account",
"exc_type": "PermissionError"
}
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.
| Rule | Limit |
|---|---|
| Inbound API calls per vendor | 120 requests / minute |
| Batch records per request | 500 items max (sync.product_status); no hard limit on product/price/inventory batches but kept reasonable |
| Webhook delivery timeout | 10 seconds per attempt; 3 attempts max |
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.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