Public prices of medicines in Egypt (14,700+ products, one record per product and pack), with normalised companies, ingredients, categories, ATC codes, barcodes, alternatives, price history and a delta feed (/price-changes) for POS and ERP synchronisation.
JSON, UTF-8, snake_case. Numbers are numbers; unknown values are null, never "".
Dates are ISO 8601 (YYYY-MM-DD); timestamps are UTC (YYYY-MM-DDTHH:MM:SSZ).
Authentication: Authorization: Bearer dp_live_…. The /sample/* endpoints need no key and return a fixed set of about 50 drugs, marked "sample": true.
Breaking changes only in /v2. Clients must ignore unknown fields and enum values.
Full guide: https://dwaprice.com/api/docs
Authentication
Every non-sample request needs Authorization: Bearer dp_live_…. Missing, malformed, unknown, revoked or expired keys answer 401 unauthorized (the same answer: we do not reveal which). A valid key whose plan, scope or IP allowlist does not allow the endpoint answers 403 forbidden with details.required_scope or details.plan.
Keys: dp_live_ + 40 base32 characters, generated from 25 random bytes; only a SHA-256 hash is stored. Scopes: drugs:read, changes:read, export, webhooks. Rotation gives the old key a 24-hour grace period; revocation is immediate.
Pagination
Lists take limit (1–100, default 20) and cursor, and return page: { limit, has_more, next_cursor }. The cursor is an opaque signed string, valid 24 hours and bound to the sort and filters (changing a filter answers 400 invalid_cursor). Listings use keyset cursors that stay stable while the dataset changes; search results (q) are ranked and capped at 1,000. Lists carry no total count; /meta has the dataset counts.
One shape for every non-2xx answer: { "error": { "code", "message", "details" } }. message is for developers; details is structured (an object, an array of { param, issue } for validation, or null).
Errors
Field
Type
Description
Example
400 validation_error
Type: details[]
Every invalid parameter, with param and issue.
400 invalid_cursor
Type: —
Cursor expired, malformed, or from other filters.
400 since_too_old
Type: window_days
since is outside the plan’s feed window: reload with /drugs.
401 unauthorized
Type: —
Missing, malformed, unknown, revoked or expired key.
403 forbidden
Type: plan, required_scope, reason
Plan, scope, suspended account or expired subscription.
403 ip_not_allowed
Type: —
Enterprise IP allowlist.
404 not_found
Type: —
Unknown id or path.
301 moved
Type: new_id + Location
The id was retired; follow Location.
429 rate_limited
Type: retry_after
Per-minute limit; Retry-After header.
429 quota_exceeded
Type: resets_at
Monthly quota; Retry-After until the month resets.
500 internal_error
Type: request_id
Our fault; quote request_id to support.
503 unavailable
Type: —
Maintenance or database unreachable; Retry-After.
Rate limits
Per key: a per-minute limit and a monthly quota (calendar month, UTC), both from your plan. The sample is limited per source network (30/min, 300/day). Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (Unix seconds) and, for keys, X-Quota-Limit, X-Quota-Remaining, X-Quota-Reset. Over the limit: 429 with Retry-After. A 304 or 4xx counts as a request; a 5xx does not. Send If-None-Match with the ETag you received to get a free 304 when nothing changed.
Delta-sync guide
The feed GET /price-changes?since= returns append-only events ordered by id: price_changed, added, status_changed, removed, renamed. Each event carries the current record (drug), so one call updates your copy.
First sync: load the catalogue with GET /drugs (all pages) and store meta.generated_at.
Every N minutes: GET /price-changes?since=<stored>; page with cursor (it carries the original since); apply each event; store latest_detected_at.
since is inclusive and delivery is at-least-once: skip event ids you already applied.
since older than your plan's window answers 400 since_too_old: do a full reload.
Sync loop (JavaScript)
let since = stored.latest_detected_at; // from the previous run
let cursor = null;
do {
const url = cursor ? `https://api.dwaprice.com/v1/price-changes?cursor=${encodeURIComponent(cursor)}`
: `https://api.dwaprice.com/v1/price-changes?since=${since}&limit=100`;
const page = await (await fetch(url, { headers: { Authorization: "Bearer " + KEY } })).json();
for (const e of page.data) {
if (e.id <= stored.last_event_id) continue; // at-least-once: skip what you applied
applyEvent(e); // e.type, e.data, e.drug (current record)
stored.last_event_id = e.id;
}
if (page.latest_detected_at) stored.latest_detected_at = page.latest_detected_at;
cursor = page.page.next_cursor;
} while (cursor);
Drugs
Products, lookups, alternatives and price history.
GET/drugs
List or search drugs
Without q: the catalogue, filtered and sorted (keyset cursor, stable across updates). With q: typo-tolerant search over Arabic and English trade names, approved aliases and spelling variants, active ingredients and barcodes; ranked, at most 1,000 results.
Parameters
GET /drugs parameters
Field
Type
Description
Example
q (query)
Type: string
Search text, 2–80 characters (Arabic or English; typo-tolerant).
كونجستل
company (query)
Type: string
Company id (see `/companies`).
sigma
ingredient (query)
Type: string
Ingredient id (see `/ingredients`); products containing it.
paracetamol
atc (query)
Type: string
ATC code prefix, matched against product-level and ingredient-level codes (`J01` matches `J01CR02`).
N02BE
dosage_form (query)
Type: DosageFormClass
route (query)
Type: RouteClass
prescription_status (query)
Type: PrescriptionStatus
status (query)
Type: ListingStatus
Default is all statuses.
has_barcode (query)
Type: boolean
updated_since (query)
Type: string (date-time)
Records whose `updated_at` is after this timestamp (any field change). For price changes alone use `/price-changes`.
sort (query)
Type: name | price | -price | updated_at | -updated_at
Ignored when `q` is given (results are ranked by relevance).
default "name"
limit (query)
Type: integer
default 20
cursor (query)
Type: string
Opaque cursor from `page.next_cursor` of the previous response.
Responses
200 A page of drugs. → DrugList
400 Invalid parameter, cursor or `since`. → Error
401 Missing, malformed, unknown, revoked or expired key. → Error
403 Key valid, but the plan, scope or IP does not allow this request. → Error
429 Per-minute limit or monthly quota exceeded; `Retry-After` in seconds. → Error
500 Our fault; quote `request_id` to support. → Error
Returns a list: in the source price lists 73 barcodes are shared by more than one pack (e.g. two pack sizes of one product). Ordered listed first, then newest price date. An unknown barcode gives an empty list.
Products with the same active ingredients and the same strength, cheapest first (no price last). strength=any appends products with the same ingredients at another strength.
Parameters
GET /drugs/{id}/alternatives parameters
Field
Type
Description
Example
id (path)*
Type: string
Stable public id (the product's URL slug).
congestal-20-tabs
strength (query)
Type: same | any
default "same"
limit (query)
Type: integer
default 50
cursor (query)
Type: string
Opaque cursor from `page.next_cursor` of the previous response.
Responses
200 Alternatives. → AlternativeList
401 Missing, malformed, unknown, revoked or expired key. → Error
403 Key valid, but the plan, scope or IP does not allow this request. → Error
404 Unknown id or path. → Error
429 Per-minute limit or monthly quota exceeded; `Retry-After` in seconds. → Error
500 Our fault; quote `request_id` to support. → Error
Append-only events ordered by id: price_changed, added, status_changed, removed, renamed. Each event carries the current record, so one call updates a local copy. Sync loop: store latest_detected_at, call again with it as since, page with cursor, skip event ids already applied (delivery is at-least-once). since older than the plan's feed window → 400 since_too_old (reload with /drugs). Requires scope changes:read.
Parameters
GET /price-changes parameters
Field
Type
Description
Example
since (query)
Type: string (date-time)
Events with `detected_at` at or after this timestamp (inclusive; skip event ids already applied). Required unless `cursor` is given — a cursor carries the original `since`.
2026-10-21T00:00:00Z
type (query)
Type: string
Comma-separated event types; default all.
price_changed,status_changed
limit (query)
Type: integer
default 20
cursor (query)
Type: string
Opaque cursor from `page.next_cursor` of the previous response.
Responses
200 A page of events. → PriceChangeList
400 Invalid parameter, cursor or `since`. → Error
401 Missing, malformed, unknown, revoked or expired key. → Error
403 Key valid, but the plan, scope or IP does not allow this request. → Error
429 Per-minute limit or monthly quota exceeded; `Retry-After` in seconds. → Error
500 Our fault; quote `request_id` to support. → Error
Batches of events are POSTed within 10 minutes of each dataset capture, signed with X-Dwaprice-Signature: sha256=<HMAC-SHA256 of the raw body with the secret> and X-Dwaprice-Delivery: <id>. Non-2xx answers are retried with exponential back-off for 24 hours; then the webhook is paused and the admin alerted. Requires scope webhooks.
Responses
201 Created; `secret` is shown only in this response. → object
400 Invalid parameter, cursor or `since`. → Error
401 Missing, malformed, unknown, revoked or expired key. → Error
403 Key valid, but the plan, scope or IP does not allow this request. → Error
Additive changes (new fields, parameters, endpoints, enum values) ship in /v1 and are listed here; clients must ignore unknown fields and values. Breaking changes ship in /v2 with /v1 kept for 12 months and announced with Deprecation and Sunset headers.