The Short Answer
If your PharmGKB calls started failing in late July 2026, the hostname api.pharmgkb.org was retired on 20 July and no longer resolves in DNS. The same API answers at api.clinpgx.org, with the same paths and the same JSON.
TL;DR:
- Replace
https://api.pharmgkb.org/v1withhttps://api.clinpgx.org/v1.- Keep to 2 requests per second.
- Treat a connection error as an outage, not as "no data".
- Self-test against a variant you know exists (rs4244285).
CPIC API users:
pharmgkbidis nowclinpgxid(and means something different in theguidelinetable), and the guideline functions are nowclinpgx_*.
You can confirm the diagnosis in ten seconds:
dig +short api.pharmgkb.org # prints nothing: the name is gone
curl -sI https://www.pharmgkb.org/ | head -1 # HTTP/2 301: the website still redirects
curl -s "https://api.clinpgx.org/v1/data/variant?symbol=rs4244285" | head -c 120
# prints the start of a JSON record: the new host works
The first command returning nothing while the second returns a redirect is the whole story, and it's also why this failure is easy to miss. The third shows the fix already works.
What Broke: api.pharmgkb.org No Longer Resolves
In July 2025 PharmGKB became part of ClinPGx, which brings together PharmGKB, CPIC and PharmCAT under one site. The website moved first and kept redirecting. On 6 July 2026 ClinPGx announced the retirement of the old API hostname it would go on 20 July, and "all endpoints, paths, and response formats are unchanged — only the hostname is different". ClinPGx gave two weeks' notice, and the fix is one line.
What makes it awkward is the asymmetry. Open www.pharmgkb.org in a browser and you land on ClinPGx, so the project looks alive. Call api.pharmgkb.org from code and the name doesn't exist. In Python with requests, on macOS, it surfaces like this:
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='api.pharmgkb.org', port=443):
Max retries exceeded with url: /v1/data/variant?symbol=rs4244285
(Caused by NameResolutionError("HTTPSConnection(host='api.pharmgkb.org', port=443):
Failed to resolve 'api.pharmgkb.org' ([Errno 8] nodename nor servname provided, or not known)"))
On Linux the tail reads [Errno -2] Name or service not known. With curl, or R's httr, you get Could not resolve host: api.pharmgkb.org.
That traceback is loud. The problem is the code around it. A lot of annotation code wraps every external call in try/except Exception, logs a warning and returns None, because a pipeline shouldn't die when one database is slow. With a dead hostname, that pattern turns every lookup into a clean, plausible "no annotation found". Nothing crashes. The output just gets thinner — and in pharmacogenomics, thinner is worse than a crash, because an empty annotation reads as "no interaction" to whoever acts on it.
We had the old hostname in our own engine until we caught it on 20 September. Nothing in our test suite would have told us, because those tests never touch the network. (No user data was affected: our file-parsing pipeline isn't deployed, and no genetic data is processed today.) We aren't alone: GitHub's code search on 3 October 2026 reports about 1,136 files that still mention api.pharmgkb.org, spread over more than 250 public repositories, including agent skills and tool servers that build their base URL from it.
Moving to the ClinPGx API (api.clinpgx.org)
The fix is the base URL. While you're in that file, add the two things the API page asks for: no more than 2 requests per second, and a heads-up to the team if your use is more than trivial. Too many requests get a 429.
import time
import requests
BASE = "https://api.clinpgx.org/v1" # was https://api.pharmgkb.org/v1
MIN_INTERVAL = 0.5 # ClinPGx asks for at most 2 requests per second
session = requests.Session()
session.headers["User-Agent"] = "my-pgx-pipeline/1.0 ([email protected])" # your own contact
_last_call = 0.0
def clinpgx_get(path, params=None, retries=4):
"""GET with a 2 req/s throttle and backoff on 429."""
global _last_call
for attempt in range(retries + 1):
wait = MIN_INTERVAL - (time.monotonic() - _last_call)
if wait > 0:
time.sleep(wait)
_last_call = time.monotonic()
resp = session.get(f"{BASE}{path}", params=params, timeout=20)
if resp.status_code != 429 or attempt == retries:
return resp # a 429 here means every retry was used; the caller decides
retry_after = resp.headers.get("Retry-After", "")
time.sleep(int(retry_after) if retry_after.isdigit() else 2 ** attempt)
We haven't seen whether ClinPGx sends a Retry-After header with its 429s, and we didn't hammer the API to find out. The code respects the header if it arrives and falls back to exponential waits if it doesn't. The throttle covers one process and isn't thread-safe. Treat ClinPGx's limit as one budget for your whole client: parallel workers have to split it: two workers at 1 request per second each, for example.
The API page also warns that while the endpoints are "pretty stable", parameters and responses "may change at any time" until a final release is announced on the ClinPGx blog.
Fail Loudly: Outage vs. No Data
Getting the calls working again is the easy half. The other half is making sure the next change like this shows up as an error, not as missing data. There are three ways this goes wrong, and two of them look identical from the status code.
1. The host is unreachable. Catch requests.exceptions.ConnectionError (and Timeout) specifically and raise your own error. A generic except Exception is how a DNS failure becomes "no results".
2. The variant has no record. ClinPGx answers 404. That one really means no data.
3. The path is wrong. ClinPGx also answers 404. Same status code, different meaning:
# 404, no record for this rsID
{"status": "fail", "data": {"errors": [{"message": "No results matching criteria."}]}}
# 404, wrong path
{"status": "fail", "data": {"errors": [{"message": "HTTP 404 Not Found"}]}}
You could parse those two messages apart, but message strings are exactly the kind of thing that changes without notice. A sturdier test is a variant that must exist. If it doesn't resolve, your host or path is wrong, and you want to know before you annotate ten thousand rows:
class ClinPGxError(RuntimeError):
"""The service could not answer. Not the same thing as "no data"."""
def get_variant(rsid):
try:
resp = clinpgx_get("/data/variant", {"symbol": rsid})
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
# DNS failure, refused connection or timeout: unreachable, not empty
raise ClinPGxError(f"cannot reach {BASE}: {e}") from e
if resp.status_code == 404:
return None # "no results" -- or a wrong path, see self_test()
if resp.status_code != 200:
raise ClinPGxError(f"{resp.status_code} from {resp.url}")
return resp.json()["data"]
def self_test():
"""A variant we know must resolve. If it doesn't, the host or path is wrong."""
data = get_variant("rs4244285")
if not data or data[0]["id"] != "PA166154053":
raise ClinPGxError("self-test failed: rs4244285 did not resolve")
Run self_test() when the process starts, or as the first step of a batch. Against the live API on 3 October 2026:
get_variant("rs4244285")returns the ClinPGx variant recordPA166154053, withclinicalSignificanceset to"drug-response".get_variant("rs999999999999")returnsNone.- The same call with the old base URL raises
ClinPGxError: cannot reach https://api.pharmgkb.org/v1.
The wrong-path case isn't hypothetical. Some agent skill files on GitHub document routes such as /v1/gene/CYP2D6, /v1/geneDrugPair or /v1/clinicalAnnotation. All three return 404. The data routes live under /v1/data/, and both of these return 200:
https://api.clinpgx.org/v1/data/gene?symbol=CYP2D6
https://api.clinpgx.org/v1/data/clinicalAnnotation?location.fingerprint=rs4244285
The Swagger documentation is the reference; a self-test is what tells you that you read it right. (If CYP2D6 is the gene you care about, read its page before trusting any call from a SNP array.)
If you run a pipeline that has to degrade gracefully when a source is down, that's fine. Return nothing for the row if you must, but count the ClinPGxErrors and alert on them. "Degraded" should be a state someone sees, not a state that looks exactly like "healthy with less data".
The CPIC API Renamed Things Too
If you query CPIC data directly through api.cpicpgx.org, the hostname didn't change, but the schema did. Release v1.55.0 made these changes, as announced on the ClinPGx blog:
drugandgenetables:pharmgkbidwas renamedclinpgxid. Same content.pairtable:pgkbcalevelwas renamedclinpgxlevel. Same content.guidelinetable:pharmgkbidwas replaced. The old column held an array of PharmGKB guideline-annotation IDs; the newclinpgxidholds a single ID for an overall ClinPGx Guideline. A find-and-replace here leaves you treating a single ID as an array.
The functions followed in v1.60.1 (cpic-data issue #51 has the history). The two guideline functions are now clinpgx_guideline_alleles and clinpgx_guideline_recommendation, and both take clinpgxdrugid; the second also needs lookupkeyarg, as before. The old pharmgkb_recommendation has no clinpgx_ twin: its error points you to clinpgx_guideline_recommendation.
Unlike the hostname, these changes fail loudly, as long as you check the status code:
import requests
CPIC = "https://api.cpicpgx.org/v1"
# pair table: pgkbcalevel -> clinpgxlevel
r = requests.get(f"{CPIC}/pair", timeout=20, params={
"genesymbol": "eq.CYP2C19",
"drugid": "eq.RxNorm:32968", # clopidogrel
"select": "genesymbol,drugid,cpiclevel,clinpgxlevel",
})
print(r.status_code, r.json())
# 200 [{'genesymbol': 'CYP2C19', 'drugid': 'RxNorm:32968', 'cpiclevel': 'A', 'clinpgxlevel': '1A'}]
old = requests.get(f"{CPIC}/pair", timeout=20, params={
"genesymbol": "eq.CYP2C19", "select": "pgkbcalevel"})
print(old.status_code, old.json()["message"])
# 400 column pair.pgkbcalevel does not exist
# Functions: pharmgkb_* -> clinpgx_*, and the argument is now clinpgxdrugid
r = requests.get(f"{CPIC}/rpc/clinpgx_guideline_alleles", timeout=20,
params={"clinpgxdrugid": "PA449053"}) # clopidogrel
print(r.status_code, [(g["genesymbol"], len(g["alleles"])) for g in r.json()])
# 200 [('CYP2C19', 36)]
The old function name returns 404 with PostgREST's PGRST202 code and a hint that names the new one. If your client ignores status codes and iterates over whatever JSON comes back, an error object can still slip through as an odd-looking row, so check r.ok before you read the body.
A related change, if you work with CYP2C19 star alleles: since August 2026, *2 is defined by c.681G>A (rs4244285) alone. Our CYP2C19 and SLCO1B1 walkthrough in Python already calls *2 from that single marker.
What the Licenses Say
This is the part to read slowly if you build anything commercial on top of these APIs. We're describing the documents as they read on 3 October 2026, not giving legal advice.
- ClinPGx / PharmGKB data. ClinPGx's current Data Usage Policy grants use under the Creative Commons Attribution-ShareAlike 4.0 license, and its terms of use include two conditions that matter here: the data is "for research purposes", and you agree not to offer "all or any part of the data for sale as a commercial item". The API page, which points to that policy, links to a different license: Attribution-NonCommercial-ShareAlike 4.0. (An older PharmGKB notice, from August 2018, also names Attribution-ShareAlike 4.0.)
- CPIC. CPIC's curated content is released under CC0, with a request for attribution. But CPIC's own license file says the CPIC database and API "are also bound by" the ClinPGx Data Usage Policy, and asks you to cite the URL, the date accessed and the version number.
If you charge for anything, read the research-purposes and no-sale terms as applying to you, and ask ClinPGx at [email protected] before you build on the difference between the two licenses. Whatever you build, keep attribution in your output: the ClinPGx licenses require it and CPIC's CC0 requests it.
A Cited Drug–Gene Layer for Agents
If you're migrating because an agent depends on these lookups, there's one more option for a narrower job. DeepDNA, which is not affiliated with ClinPGx, PharmGKB or CPIC, runs free knowledge endpoints with no key: 51 drug–gene records across 11 of the 12 genes in its knowledge base. Forty of them cite a CPIC or DPWG guideline, one cites only a drug label, and ten have guideline: null, meaning the record names no guideline as its basis. Every record lists its sources, and the evidence level field is null for every pair today. The records are summaries we wrote from our own reviewed gene pages, citing CPIC, PharmGKB and PharmVar pages and the primary papers. It's a small curated set, not a mirror of ClinPGx.
The same records are exposed as a remote MCP server, so an MCP-capable agent can call them as tools instead of recalling guidelines from memory:
curl -s https://deepdna.ai/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"lookup_drug_gene","arguments":{"gene":"CYP2C19","drug":"clopidogrel"}}}'
The request body carries only a gene and a drug name, never a genotype or anything about a person. In Claude Code, adding the server is claude mcp add --transport http deepdna https://deepdna.ai/mcp. The REST version is:
GET https://deepdna.ai/api/v1/pgx/CYP2C19/clopidogrel
The API docs list every endpoint and the four MCP tools.
Frequently Asked Questions
Why is the PharmGKB API not working?
The old hostname was switched off after 20 July 2026, so api.pharmgkb.org no longer exists in DNS. Point your client at api.clinpgx.org: the paths and the JSON didn't change.
Do I need an API key for api.clinpgx.org?
The public variant, gene and clinical annotation endpoints we tested on 3 October 2026 answered without a key. ClinPGx asks you to keep to 2 requests per second and to write to [email protected] if your use is more than trivial.
What is the ClinPGx API rate limit?
ClinPGx asks for at most 2 requests per second; treat it as one budget across all your workers. Faster clients get HTTP 429, so throttle on your side and back off when one arrives.
Is the CPIC API affected too?
Not by the hostname: api.cpicpgx.org still answers. Its schema changed in 2026, though. In the drug, gene and pair tables the PharmGKB columns were renamed (clinpgxid, clinpgxlevel); in the guideline table the old array column was replaced by a single Guideline ID. The guideline functions are now clinpgx_guideline_alleles and clinpgx_guideline_recommendation, with a clinpgxdrugid argument.
Can I use ClinPGx data in a commercial product?
Read the terms before you do. ClinPGx's Data Usage Policy names CC BY-SA 4.0 and adds research-purposes and no-sale conditions, while its API page links to CC BY-NC-SA 4.0. CPIC's curated content is CC0, but the CPIC API is also bound by the ClinPGx policy. Ask ClinPGx at [email protected] and get legal advice.
This article is for educational purposes only and does not constitute medical advice. The code examples return database records about variants and guidelines; they are not a basis for prescribing decisions, which belong to a qualified clinician using a validated test.
This article was created with AI assistance and reviewed by the DeepDNA editorial team.
DNA and bloodwork, as an API
DeepDNA is now an API for AI agents and health apps. The knowledge endpoints are live and free, no key. Parsing DNA files and lab reports, and crossing the two, is in private beta. Building something with genetic or lab data? Tell us what.
Read the API docs