Errors
Errors are JSON, always, with a machine-readable error string as the first field. Branch on that string and the status code — never on the human-readable message, which we reword.
The shape
{
"error": "forbidden",
"message": "Scope 'interactions:write' required. Upgrade tier at https://drug-database.com/pricing.",
"tier": "starter",
"missing_scope": "interactions:write"
}error is stable. message is prose for a human reading a log. Anything else is context specific to that error and may be absent.
Status codes
| Status | error | What it means |
|---|---|---|
| 200 | — | Success. Derived payloads carry a _meta.sources block. |
| 201 | — | Created. Returned by POST /v1/webhooks, with the one-time secret. |
| 204 | — | Deleted. No body. |
| 304 | — | Not modified — you sent if-none-match and the etag still matched. No body, and it still costs a request against quota. |
| 401 | unauthorized | No key, an unparseable key, a revoked key, or a legacy ms_ key. |
| 402 | ai_allowance_exhausted | POST /v1/chat only: this month’s AI answers and any packs are spent. resets and upgrade_url say what next. |
| 403 | forbidden | The key is valid but lacks the scope this endpoint requires. missing_scope names it. |
| 404 | not_found | The identifier resolved to nothing. Not an error in your request shape. |
| 405 | method_not_allowed | Wrong verb for that path. The allow header lists what the path accepts. |
| 422 | validation | Your request was understood and refused. message says why; details carries specifics. |
| 429 | quota_exceeded | The account used its monthly quota or daily cap. resets, retry-after and upgrade_url say when and how. |
| 429 | rate_limited | IP rate limit on a keyless public endpoint. retry-after gives seconds. |
| 502 | upstream_error / bad_gateway | A backend or an external public API failed. Transient — retry with backoff. |
| 503 | unavailable | A dependency is deliberately unavailable. retry-after gives seconds. |
The ones that cause support tickets
429 quota_exceeded
The account has used its monthly quota or its daily cap. Quotas are per account, so every key you hold and the console's lookups draw on the same counter. The body says which window you hit and when it resets:
{
"error": "quota_exceeded",
"tier": "developer",
"limit": 20,
"window": "month",
"resets": "2026-10-01T00:00:00.000Z",
"upgrade_url": "https://drug-database.com/pricing"
}See it coming
Read x-ratelimit-remaining on your successful calls and alert before it reaches zero, or check Usage in the console. The Free plan's 10 requests a month are for trying the API; Pro starts at 1,500.
402 ai_allowance_exhausted
Only POST /v1/chat returns this: the month's AI answers and any prepaid pack are spent, so the turn was refused before the model ran. Nothing is billed as overage. It carries answers_included, resets and upgrade_url; on a paid plan, POST /v1/chat/packs tops up.
403 with missing_scope
Your tier does not carry that scope. The body tells you which one and which tier you are on, so you can decide between upgrading and dropping the call:
{
"error": "forbidden",
"message": "Scope 'ch:claims' required. Upgrade tier at https://drug-database.com/pricing.",
"tier": "starter",
"missing_scope": "ch:claims"
}Authentication maps every scope to the endpoints it opens. Note also that a key can be narrowed below its tier — if the tier should carry the scope, check the key itself in the console.
502 upstream_error
This is us, not you. It covers a failed database query and an unreachable external public API alike, and the message is deliberately generic:
{
"error": "upstream_error",
"message": "The query could not be completed. Retry shortly; if it persists, contact support."
}The underlying detail is logged on our side and never returned — an earlier version passed the upstream body through and ended up serving customers several kilobytes of a firewall interstitial. Retry with exponential backoff; if it persists past a few minutes, check status before writing in.
One 422 is really a 502 in disguise, and vice versa
An offset past the end of a result set comes back as a 422 with the message offset is beyond the end of the result set, because that genuinely is your request. Everything else that fails downstream is a 502 — we do not report infrastructure failures as if your input were wrong.
Retrying
- Retry:
502,503,429(honourretry-after), and network-level failures. Use exponential backoff with jitter. - Do not retry:
400,403,404,405,422. Nothing about the response will change. - Retry once, then stop:
401. If it was a transient backend failure during auth a retry clears it; if it is quota or revocation, retrying in a loop just burns your remaining calls. - Every request counts against quota, including the ones that 4xx. A retry loop on a
422is an expensive way to stay broken.
Partial data is not an error
Composite endpoints — /v1/drugs/{id}/profile above all — assemble a dozen best-effort sections, and any of them can come back null or empty while the response is a clean 200. That is deliberate: a missing monograph should not cost you the price and interaction data that did resolve. Check for the section you need rather than assuming a 200 means every field is populated.