ERP Integration API Contract
How an ERP or pharmacy system integrates with the MDB Medication Data Bank to screen prescriptions for drug, food, disease and pandemic interactions.
Overview
MDB exposes a small, stable HTTP API. You send the drugs a patient is taking; MDB returns any interactions it finds, each with a severity and clinical guidance. All requests and responses are JSON over HTTPS.
Drug ↔ Drug
Pairwise screening across every drug in the request.
Drug ↔ Food
Dietary interactions, e.g. grapefruit with carbamazepine.
Drug ↔ Disease
Contraindications against a patient's existing conditions.
Drug ↔ Pandemic
Guidance tied to an active pandemic protocol.
Base URL
# Production
https://api.mdb.com.saAll endpoints below are relative to this host. HTTPS only — plain HTTP is not served.
Integration model
An organization holds the contract and the request quota. Under it you create one sub-client per branch, pharmacy or store, each with its own API key — so usage is attributable per location and a key can be revoked without disturbing the rest of the estate.
Organization "Demo Pharmacy Group" quota 10,000 requests │ master key: DEM-MASTER-xxxxxxxx │ ├── Sub-client "Cairo Branch" key: DEM-SUB-xxxxxxxx limit 4,000 ├── Sub-client "Alexandria Branch" key: DEM-SUB-xxxxxxxx limit 2,500 └── Sub-client "Giza Branch" key: DEM-SUB-xxxxxxxx no limit → shares the pool
| Key type | Prefix | Use it for | Counts against |
|---|---|---|---|
| Master | *-MASTER-* |
Back-office jobs and central integrations | Organization quota |
| Sub-client | *-SUB-* |
One per branch / POS / store | Its own limit if set, and the organization quota |
Quick start
- Get your keyYour MDB administrator creates the organization and issues a master key; you create a sub-client per branch from the ERP dashboard.
- Resolve drug identifiersSend either MDB
drug_ids(UUIDs) or the localregistration_numbersyou already store. - Call the checkerPOST the drug list on every prescription save or dispense.
- Act on the resultBlock or warn on CRITICAL, surface a soft warning on WARNING.
- Watch your quotaPoll usage stats, or handle
429and alert before you run dry.
Authentication
API key (recommended)
Send the key in the x-api-key header on every request. The server also
accepts it as Authorization: Bearer <key>, but prefer
x-api-key — it keeps API-key traffic clearly separate from JWT traffic.
curl -X POST https://api.mdb.com.sa/api/v1/check-interactions \ -H "x-api-key: DEM-SUB-626e533d87f38185" \ -H "Content-Type: application/json" \ -d '{"drug_ids":["000a22da-add0-4278-85df-9a5fb1cde722"]}'
JWT (ERP dashboards)
Exchange the organization's email and password for a bearer token, then send it as
Authorization: Bearer <token>. Tokens are short-lived — re-authenticate
when you receive a 401.
Check interactions
The core drug–drug screening call. Counts as one request against your quota.
Request body
| Field | Type | Description | |
|---|---|---|---|
drug_ids | string[] (UUID) | ONE OF | MDB drug identifiers. |
registration_numbers | string[] | ONE OF | National registration numbers, if you don't store MDB UUIDs. |
drug_names | string[] | OPTIONAL | Free-text names. Least reliable — prefer an identifier. |
drug_ids or registration_numbers, not both.
At least two distinct drugs are required — identifiers that resolve
to the same active ingredient collapse to one, so sending two brands of the same
molecule returns 400 ValidationError.
Optional headers
| Header | Value | Effect |
|---|---|---|
Accept-Language | en, ar, fr, es, ru, zh | Localizes status messages and clinical text. |
x-include-food-interactions | true | Also return food interactions. |
x-include-disease-interactions | true | Also return disease interactions. |
x-include-pandemic-interactions | true | Also return pandemic interactions. |
Example
curl -X POST https://api.mdb.com.sa/api/v1/check-interactions \ -H "x-api-key: $MDB_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept-Language: en" \ -d '{ "drug_ids": [ "000a22da-add0-4278-85df-9a5fb1cde722", "0016ae1e-0b19-4a0a-8084-d7ae025bba0d" ] }'
const res = await fetch(`${BASE}/api/v1/check-interactions`, { method: "POST", headers: { "x-api-key": process.env.MDB_API_KEY, "Content-Type": "application/json", "Accept-Language": "en" }, body: JSON.stringify({ drug_ids: drugIds }) }); if (res.status === 429) throw new Error("MDB quota exhausted"); const result = await res.json(); if (result.result === "MAJOR") blockDispense(result.interactions);
import os, requests resp = requests.post( f"{BASE}/api/v1/check-interactions", headers={ "x-api-key": os.environ["MDB_API_KEY"], "Accept-Language": "en", }, json={"drug_ids": drug_ids}, timeout=10, ) if resp.status_code == 429: raise RuntimeError("MDB quota exhausted") result = resp.json()
$ch = curl_init("$base/api/v1/check-interactions"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "x-api-key: " . getenv("MDB_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode(["drug_ids" => $drugIds]), ]); $result = json_decode(curl_exec($ch), true);
Response 200 OK
{
"success": true,
"interactions": [],
"result": "NONE",
"message": "✅ No interaction detected.",
"response_time_ms": 109
}| Field | Type | Description |
|---|---|---|
success | boolean | Request was processed. |
result | string | Highest severity found: NONE, MINOR, MODERATE, MAJOR, DUPLICATION. |
message | string | Human-readable summary, localized. |
interactions | object[] | One entry per interaction found; empty when clear. |
response_time_ms | number | Server-side processing time. |
success: true only means the check ran. Always branch on
result / status — never treat a 200 as "no interactions".
Unified check
Screens all four interaction types in a single call and bills a single request against your quota — the efficient choice when you also know the patient's conditions or diet.
Request body
| Field | Type | Description | |
|---|---|---|---|
drug_ids | string[] (UUID) | ONE OF | MDB drug identifiers. |
registration_numbers | string[] | ONE OF | Mutually exclusive with drug_ids. |
diseases | string[] | OPTIONAL | Patient conditions, e.g. ["Kidney Disease"]. |
food | string | OPTIONAL | Food item, e.g. "grapefruit". Omit to return all food interactions. |
pandemic | string | OPTIONAL | Active pandemic protocol name. |
curl -X POST https://api.mdb.com.sa/api/unified-interactions/check-unified-erp \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "drug_ids": ["000a22da-...", "0016ae1e-..."], "diseases": ["Kidney Disease"], "food": "grapefruit" }'
Response 200 OK
{
"success": true,
"status": "WARNING",
"status_message": "Drug interaction warning",
"details": {
"drugs_checked": [
{ "id": "0016ae1e-…",
"trade_name": "TEGRETOL 2% oral suspension",
"scientific_name": "CARBAMAZEPINE" }
],
"total_interactions": 1,
"interactions_by_type": {
"drug": 0, "food": 1,
"disease": 0, "pandemic": 0
},
"interactions": [
{
"type": "FOOD",
"severity": "Moderate",
"drug": { "scientific_name": "CARBAMAZEPINE" },
"food_name": "grapefruit",
"description": "…grapefruit juice increased plasma drug
concentrations by approximately 40%…"
}
]
}
}
type is one of DRUG, FOOD,
DISEASE, PANDEMIC, so you can route each finding to the
right part of your UI.
SFDA alerts
Every checker response (check-interactions-erp, check-unified-erp,
and the food and disease checks) carries an sfda_alerts object: official SFDA
safety alerts and withdrawals tied to the products you sent. Each entry in
drugs_checked also carries its own has_sfda_alert flag.
{
"sfda_alerts": {
"has_sfda_alert": true,
"alert_count": 1,
"alerts": [
{
"drug_id": "ed61546d-…",
"trade_name": "IMURAN",
"has_sfda_alert": true,
"alert_type": "Safety Alert",
"title": "Safety Signal of Azathioprine and the Risk of Optic neuritis",
"source_url": "https://sfda.gov.sa/sites/default/files/…/Azathioprine….pdf",
"publication_date": "2026-08-…",
"matched_via": "PRODUCT",
"batch_numbers": []
}
]
}
}| Field | Meaning |
|---|---|
has_sfda_alert | true when at least one checked product has an active SFDA notice. |
alert_type | "Safety Alert", "Withdrawal" or "Recall". |
title | The SFDA's own title (Arabic when you request lang=ar and SFDA published one). |
source_url | Direct link to the official advisory: the safety alert's document, or the withdrawal circular's page. |
matched_via | PRODUCT: the notice names this product. ACTIVE_INGREDIENT: a safety alert about a substance this product contains (see ingredient). |
batch_numbers | Affected lots, when the notice is batch-specific. |
status, result or any interaction count. Only notices MDB has
confirmed against a product are returned. Withdrawals are product-specific and stop being
returned once the product is reinstated. Safety alerts apply to every product containing the
named substance, including FDA and Health Canada products.
Usage statistics
Returns remaining quota. The shape depends on which key you present. This call is free.
With a master key
{
"success": true,
"data": {
"organization": "Demo Pharmacy Group",
"usage_limit": 10000,
"current_usage": 3,
"remaining_usage": 9997,
"usage_percentage": 0
}
}With a sub-client key
{
"success": true,
"data": {
"organization": "Demo Pharmacy Group",
"branch": "Cairo Branch",
"organization_usage_limit": 10000,
"organization_current_usage": 3,
"branch_current_usage": 0,
"remaining_usage": 9997,
"usage_percentage": 0
}
}ERP login
Exchanges organization credentials for a bearer token used by the unified endpoint.
curl -X POST https://api.mdb.com.sa/api/erp-auth/login \ -H "Content-Type: application/json" \ -d '{"email":"erpadmin@example.com","password":"••••••••"}'
Response 200 OK
{
"success": true,
"message": "Login successful",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
"erpClient": {
"id": "9223d5e5-…",
"name": "Demo Pharmacy Group",
"email": "erpadmin@example.com",
"usage_limit": 10000,
"active": true
}
}
}data.token. Read it from there
rather than assuming a top-level field.
Status & severity
The unified endpoint returns an overall status; the classic endpoint returns
the highest result severity. Map them to your own UI as follows.
| Status | Raised when | Suggested handling |
|---|---|---|
| CRITICAL | Any MAJOR or CRITICAL interaction |
Block the dispense; require pharmacist override. |
| WARNING | Any MODERATE or MINOR interaction |
Show a dismissible warning with the guidance text. |
| NONE | Nothing found | Proceed normally. |
Severity values on individual interactions
MAJOR · MODERATE · MINOR ·
DUPLICATION · NONE. DUPLICATION flags
therapeutic duplication — two drugs sharing an active ingredient.
Quotas & limits
Every successful interaction check consumes one request from the organization quota. Sub-clients may optionally hold a reserved slice of that quota.
| Scope | Enforced when | Response |
|---|---|---|
| Organization | current_usage ≥ usage_limit |
429 with "Usage limit exceeded" |
| Sub-client | Its own limit is set and reached | 429 with scope: "sub_client" |
A sub-client with no limit is uncapped on its own and simply draws from the shared organization pool. Allocations can never be over-subscribed: the sum of all sub-client limits is validated against the organization quota.
{
"success": false,
"error": "Sub-client usage limit exceeded",
"scope": "sub_client",
"usage_limit": 4000,
"current_usage": 4000
}429 means the check did not run. Never let a quota error fall
through as "no interactions found" — queue the request or surface a hard error to the
pharmacist.
Subscription term
A quota is an allowance within a term. The term itself is a separate gate, and
the two fail differently: a spent allowance returns 429 and clears when the
term is renewed or reset, while an ended term returns 402 and clears only
when someone renews the subscription.
| State | type | Status | Clears when |
|---|---|---|---|
| Term ended | SubscriptionExpired | 402 |
The subscription is renewed. |
| Suspended | SubscriptionSuspended | 403 |
An administrator lifts the suspension. |
| Cancelled | SubscriptionCancelled | 403 |
The account is re-subscribed. |
| Allowance spent | UsageLimitExceeded | 429 |
The term is renewed, resetting the counter. |
EXPIRED (hourly), which is what reporting
and the admin screens read. An integrator should treat the 402 as
authoritative and never infer entitlement from a status field that may be up to an hour
behind.
Terms are open-ended unless a billing period is set. An open-ended term never returns
402; only its usage allowance applies.
Error reference
| Status | Meaning | What to do |
|---|---|---|
400 | Validation failed — missing or conflicting fields | Fix the payload. Do not retry unchanged. |
401 | Missing/invalid API key, or missing/expired JWT | Re-authenticate; verify the key. |
402 | Subscription term has ended (SubscriptionExpired) |
Renew the subscription. Waiting will not clear it — unlike 429, nothing resets. |
403 | Organization or sub-client deactivated, or the subscription is
suspended / cancelled (SubscriptionSuspended, SubscriptionCancelled) |
Contact your MDB administrator. |
404 | Resource not found — unrecognised identifier | Re-resolve the identifier. |
409 | Conflict — resource already exists, or still referenced | Reconcile state before retrying. |
429 | Quota exhausted (organization or sub-client) | Stop sending; alert operations. |
500 | Unexpected server error | Retry once with backoff, then alert. |
success is present
Authentication and validation errors include success: false, but errors
raised inside a controller (404, 409, 500) return
only error and type. Branch on the HTTP status code, and read
error for the message — treating a missing success field as
success will silently swallow failures.
Authentication error — 401
API-key failures and JWT failures do not share an envelope:
// API key missing or wrong (x-api-key routes) { "success": false, "error": "Invalid API key" } // JWT missing or invalid (bearer routes) — no "success" field { "error": "Authentication failed" }
Validation error — 400
Every failed rule is listed in errors; error repeats the first.
{
"success": false,
"error": "Email is required",
"errors": [
"Email is required",
"Password is required"
]
}Request error — 400 / 403 / 404 / 409 / 500
Raised inside the request handler. Carries a machine-readable
type, but no success field.
// too few drugs { "error": "Provide at least two drug IDs or registration numbers", "type": "ValidationError" } // unknown registration numbers { "error": "Drugs not found for registration numbers: NOPE-1, NOPE-2", "type": "NotFoundError" }
type | Status | Cause |
|---|---|---|
ValidationError | 400 | Fewer than two distinct drugs supplied. |
NotFoundError | 404 | Identifier did not resolve to a drug. |
ForbiddenError | 403 | ERP client deactivated. |
UsageLimitExceeded | 429 | Quota exhausted. |
DuplicateError / ConflictError | 409 | Resource exists, or still referenced. |
Localization
Send Accept-Language to localize status messages and clinical guidance.
Supported: en, ar, fr, es,
ru, zh. Defaults to en.
| Localized | Always English |
|---|---|
| Status messages, description, monitoring and management guidance | Drug names, references and headers — kept in English for clinical accuracy |
Go-live checklist
- API keys stored server-side in a secret manager — never in client code or version control.
- One sub-client key per branch, so usage is attributable and revocable.
429handled as a hard failure, never as "no interactions".- Branch on
status/result, not on the HTTP code alone. - Request timeout set (10s is comfortable) with one bounded retry on
5xx. - Quota monitored via
/api/v1/usage-statswith an alert before exhaustion. - Drug identifiers reconciled against MDB, preferring UUIDs or registration numbers.
- Pharmacist override path defined for
CRITICALresults.