Authentication
One mechanism, everywhere: a tenant API key sent as a bearer token. There are no cookies, no OAuth dance and no session — which is also why the API is safe to call from a browser.
The header
Authorization: Bearer dd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
No other authentication is accepted — not a query parameter, not a custom header. A request without a parseable Authorization: Bearer … header is a 401 before any handler runs.
Key format
| Prefix | What it is |
|---|---|
| dd_live_ | Production key. This is the normal case. |
| dd_test_ | Behaves identically and reads the same data — the prefix exists so you can tell environments apart in a log. |
| ms_live_ / ms_test_ | Legacy, pre-rebrand. Rejected by default. Mint a dd_ replacement. |
After the prefix is 24 bytes of cryptographic randomness in base64url — roughly 192 bits, which is far past anything brute-forceable. We store a SHA-256 hash and the last four characters; the plaintext exists only in the response that created it.
Shown once
There is no endpoint, no support process and no database row that can return a key's plaintext after creation. If it is lost, revoke it in the console and mint another.
Scopes
Every endpoint declares one scope. A key's effective scopes are the intersection of its tier's catalogue and the scopes set on the key itself — so narrowing a key is always possible and widening one never is. Ask for a scope your tier does not carry and you get a 403 naming the missing scope in missing_scope.
What each tier carries
developer
pro
starter
growth
enterprise
Higher tiers strictly extend lower ones — nothing is ever taken away by upgrading.
What each scope unlocks
| Scope | Endpoints |
|---|---|
| drugs:read | /v1/drugs · /v1/drugs/{id} · /v1/drugs/{id}/profile · /v1/drugs/{id}/monograph · /v1/drugs/lookup · /v1/packs · /v1/substances · /v1/rxnorm · /v1/markets/overview · /v1/assessment · /v1/signatures · /v1/targets · /v1/guidelines · /v1/hta · and every enrichment resolver |
| atc:read | /v1/atc/{code} and its children/drugs sub-paths · /v1/therapies/{code} · /v1/classes and its sub-paths |
| changes:read | /v1/changes |
| webhooks:write | POST and DELETE /v1/webhooks |
| graphql:query | POST /v1/graphql |
| interactions:write | POST /v1/interactions/check · POST /v1/prescriptions/validate |
| ch:claims | POST /v1/ch/claims/validate · POST /v1/ch/price-comparison |
| byol:write | /v1/byol/{provider} and /v1/byol/{provider}/query |
| chat:write | POST /v1/chat · POST /v1/chat/packs |
| sso | Single sign-on for the console. Grants no API surface. |
chat:write is on every tier
What limits the assistant is not the scope but the monthly AI answer allowance each plan includes: Free 5¢ of credit (half an answer at the per-answer ceiling), Pro 50, Starter 170, Growth 850. When it and any prepaid pack are spent, POST /v1/chat answers 402 ai_allowance_exhausted before the model runs. Paid plans can top up with POST /v1/chat/packs (100 answers for $15, 500 for $60).
Quotas
Quotas are counted per account: every key you hold, and the console's drug-profile, market and interaction lookups, share one counter. Two ceilings apply at once and the tighter one wins.
| Tier | Monthly | Daily cap | AI answers / month | Keys |
|---|---|---|---|---|
| Free | 10 | 10 | 5¢ credit | 2 |
| Pro ($29) | 1,500 | 500 | 50 | 5 |
| Starter ($99) | 20,000 | none | 170 | 10 |
| Growth ($490) | 150,000 | none | 850 | 25 |
| Enterprise | effectively unlimited | none | by contract | 100 |
An exhausted quota answers 429
When the account runs out of monthly quota or daily cap, calls return 429 with "error": "quota_exceeded", the limit and window you hit, a resets timestamp, a retry-after header and an upgrade_url. Nothing is billed by usage. Watch x-ratelimit-remaining on successful calls to see it coming.
Response headers
| Header | On | Meaning |
|---|---|---|
| x-ratelimit-remaining | authenticated 2xx | Calls left before the tighter of the two ceilings is reached. |
| x-drug-database-tier | authenticated 2xx | The tier the key resolved to. |
| x-drug-database-cache | enrichment endpoints | Where the payload came from: db (cached mirror), live (fetched from upstream now) or stale (cached copy returned because upstream was unreachable). |
| etag | cacheable GETs | Send it back as if-none-match to get a 304. |
| retry-after | 429 / 503 | Seconds to wait. |
Revocation timing
Resolved keys are cached in memory for 30 seconds. A revoked key can therefore keep working for up to half a minute on an already-warm instance. Treat revocation as eventually consistent within 30s; if you need it to be instant, rotate the upstream secret your integration reads as well.
Calling from a browser
The API sends access-control-allow-origin: * and deliberately does not send access-control-allow-credentials. There is no ambient cookie for a hostile page to ride, so a wildcard origin grants nobody anything they could not do with curl.
- Allowed request headers:
authorization,content-type,if-none-match. - Exposed response headers:
etag,x-ratelimit-remaining,x-drug-database-tier,retry-after. - Preflights are answered at the edge and cached for 24 hours.
That does not make a key safe in front-end code
CORS being open is about the browser, not about secrecy. A key shipped in client-side JavaScript is a public key — anyone can read it and spend your quota. Proxy through your own backend, or mint a narrowly-scoped key you are willing to lose.