Quickstart

Three steps to a 200: create a key, call an endpoint, read the envelope. The Free plan needs no card and covers 10 requests (search, the ATC tree, the change feed) and 5¢ of AI credit a month.

  1. 1

    Create a key

    Keys are minted in the console at Advanced → API keys. The plaintext is shown once, at creation — we store only a SHA-256 hash and the last four characters, so a lost key is replaced, never recovered.

    A live key looks like dd_live_… and a test key like dd_test_…. Both are real credentials against the same data; the prefix exists so you can tell in a log which environment a call came from.

    dd_, not ms_

    Keys issued before the mid-2026 rebrand start ms_live_ / ms_test_. Those are no longer accepted by default — if you have one in an old config, mint a replacement rather than filing a bug.
  2. 2

    Make the call

    Every endpoint hangs off https://drug-database.com/v1 and takes the key as a bearer token. This is the whole of the hello-world:

    curl
    curl -s 'https://drug-database.com/v1/drugs?q=ibuprofen&country=ALL&limit=5' \
      -H 'Authorization: Bearer dd_live_YOUR_KEY'
    javascript
    const res = await fetch(
      'https://drug-database.com/v1/drugs?' +
        new URLSearchParams({ q: 'ibuprofen', country: 'ALL', limit: '5' }),
      { headers: { Authorization: `Bearer ${process.env.DRUG_DATABASE_API_KEY}` } },
    )
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`)
    const { results, _meta } = await res.json()
    python
    import os, requests
    
    res = requests.get(
        "https://drug-database.com/v1/drugs",
        params={"q": "ibuprofen", "country": "ALL", "limit": 5},
        headers={"Authorization": f"Bearer {os.environ['DRUG_DATABASE_API_KEY']}"},
        timeout=30,
    )
    res.raise_for_status()
    payload = res.json()

    The /v1 and /api/v1 prefixes are the same routes

    https://drug-database.com/v1/drugs is the canonical, documented form and is what the OpenAPI document advertises. /api/v1/drugs reaches the identical handler — it is the internal path the rewrite points at. Use /v1.

  3. 3

    Read what comes back

    Successful responses are JSON. Anything derived from an ingested register carries a _meta.sources block naming every source that contributed and when it last synced cleanly, so you can decide for yourself whether a figure is fresh enough to act on.

    200 — trimmed
    {
      "results": [
        {
          "id": "…-uuid",
          "product_name": "Algifor Dolo forte Filmtabl 400 mg",
          "country_code": "CH",
          "atc_code": "M01AE01",
          "manufacturer": "VERFORA SA"
        }
      ],
      "_meta": {
        "sources": {
          "bag_sl":     { "last_synced_at": "2026-09-11T03:12:00Z", "schema_state": "ok" },
          "swissmedic": { "last_synced_at": "2026-09-11T03:40:00Z", "schema_state": "ok" }
        }
      }
    }

    Two response headers are worth logging on every call:

    HeaderMeaning
    x-ratelimit-remainingCalls left before the tighter of your monthly quota and daily cap is exhausted.
    x-drug-database-tierThe tier the key resolved to — useful when a 403 says a scope is missing.

    Both are exposed cross-origin, so browser clients can read them too. See Errors for what each non-200 status means.

What the free tier covers

A Free key resolves to four scopes and one small monthly ceiling, shared by all your keys and the console. When it is hit, calls return 429 quota_exceeded until the month rolls or you upgrade. It is sized for trying the API, not for running on it.

Free
Scopesdrugs:readatc:readchanges:readchat:write
Monthly quota10 requests
AI credit5¢ a month
Card requiredNo

Authentication has the full scope and quota table for every tier, and explains which endpoints each scope unlocks.

Where to go next