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

every error
{
  "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

StatuserrorWhat 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.
401unauthorizedNo key, an unparseable key, a revoked key, or a legacy ms_ key.
402ai_allowance_exhaustedPOST /v1/chat only: this month’s AI answers and any packs are spent. resets and upgrade_url say what next.
403forbiddenThe key is valid but lacks the scope this endpoint requires. missing_scope names it.
404not_foundThe identifier resolved to nothing. Not an error in your request shape.
405method_not_allowedWrong verb for that path. The allow header lists what the path accepts.
422validationYour request was understood and refused. message says why; details carries specifics.
429quota_exceededThe account used its monthly quota or daily cap. resets, retry-after and upgrade_url say when and how.
429rate_limitedIP rate limit on a keyless public endpoint. retry-after gives seconds.
502upstream_error / bad_gatewayA backend or an external public API failed. Transient — retry with backoff.
503unavailableA 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:

429
{
  "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:

403
{
  "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:

502
{
  "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 (honour retry-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 422 is 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.