Base URL https://api.hlaverify.com (also https://hlaverify.com/v1/…). Pinned to IPD-IMGT/HLA 3.66.0 — 47,424 named alleles. Every response carries the release and the attribution line. No LLM anywhere; nothing you send is stored.
What to send. HLA-Verify checks allele nomenclature, typing-report consistency and match arithmetic against a pinned IPD-IMGT/HLA release. Send allele names, typing strings, GL strings and report text about HLA typing. Do not send patient identifiers: no names, medical record numbers, dates of birth, accession or case identifiers, or other patient details. The service neither needs nor wants them, request bodies are processed in memory and never stored, and de-identifying before you send is the caller's responsibility. This is a nomenclature and reference-release checker: not a diagnostic aid, not clinical decision support, and it does not recommend a donor.
Free public beta. Verdicts are production-quality and pinned to IPD-IMGT/HLA 3.66.0 — the beta is about pricing and limits, not about correctness. Anonymous access stays open with no key, at 100 calls a day per IP and 60 requests/minute. Paid keys with higher quotas — Starter, Lab and Scale — are self-serve: pick a tier on the pricing page and the key is issued automatically when Stripe confirms the subscription. Academic and nonprofit labs get a free, durable key at Lab's limits just by emailing hello@hlaverify.com from an institutional address — no Stripe involved — or can instead apply with POST /v1/research-access (below), approved by hand, for a checkout-issued key. Enterprise: hello@hlaverify.com.
Without a key the API is open for evaluation at 100 calls a day per IP, 60 requests per minute. Labs, LIMS vendors and agent platforms get a key (header X-API-Key: … or Authorization: Bearer …) with a higher or uncapped daily quota and larger batches. Per-key usage reporting is built: see Usage below. Keyed customers are told by email before the deployment-wide release pin moves, with a diff of changed verdicts — a manual step in the release runbook, not an automated or per-key feature: see Release pinning below. Keys: hello@hlaverify.com.
| Tier | Calls/day | Typings per /v1/normalize call | Burst | Auth |
|---|---|---|---|---|
free | 100 calls/day | 250 | 60 requests/minute per IP | none (anonymous) |
starter | 500 calls/day | 250 | 60 requests/minute | API key |
lab | 10,000 calls/day | 5,000 | 600 requests/minute | API key |
scale | 1,000,000 calls/day | 5,000 | 6,000 requests/minute | API key |
enterprise | uncapped calls/day | 5,000 | uncapped | API key |
academic | 10,000 calls/day | 5,000 | 600 requests/minute | API key |
pro is the former name of lab. Keys issued as pro keep working and get Lab's limits; they still report x-hla-verify-tier: pro.
Calls are counted per UTC day — the quota resets at 00:00 UTC, no rolling window, no proration. A key is counted per key (an OAuth token on /mcp counts against the key behind it); an anonymous caller is counted per IP, and the IP is never stored — the counter is addressed by a SHA-256 digest of the day and the address, which changes at midnight. Every billable response carries the count:
| Header | Meaning |
|---|---|
x-hla-verify-tier | The tier this call was served at. |
x-hla-verify-daily-limit | Calls per UTC day, or unlimited. |
x-hla-verify-daily-remaining | Calls left today. unknown on the rare occasion the counter is unreachable — the call is served anyway. |
x-hla-verify-daily-reset | ISO-8601 timestamp of the next UTC midnight. |
x-hla-verify-max-typings | Your tier's cap on typings in one /v1/normalize call. |
Over quota is 429 with the usual {"detail": "…"}, naming the tier, the limit, the reset time and where to upgrade, plus Retry-After in seconds. Over your tier's batch cap is 422, naming the cap and the tier that lifts it — split the batch or upgrade. What does not consume quota: /healthz, /docs, /openapi.json, /pricing, /checkout/success, /v1/checkout, /v1/beta-signup, /v1/research-access, the OAuth and .well-known routes, the Stripe webhook, and on /mcp everything that is not a tool call plus the about, beta_signup and research_access tools. text on /v1/verify stays capped at 200,000 characters for every tier, including free.
Prices come from Stripe at request time, so the pricing page is always what you will actually be charged. POST /v1/checkout with {"tier": "lab"} returns a Stripe Checkout Session url; the buttons on the pricing page do exactly that. The endpoint is free and does not consume daily quota. The price you sign at is the price you keep: a subscription stays on the price it was created with until it is deliberately migrated, so a later price change applies to new subscribers, not to you. Beta keys at a paid tier's limits are still issued by hand on request — email hello@hlaverify.com.
Starter, Lab and Scale keys are issued automatically through Stripe: subscribe on the pricing page, and Stripe's webhook creates an active key in the same store the API reads at request time — usually ready within a few seconds of payment, no manual provisioning. The key is shown once on the checkout success page and is also written to your Stripe customer record (visible in your receipts and the Stripe customer portal). Cancelling or letting a subscription lapse in the Stripe customer portal revokes the key the same way. Need an enterprise key? Email hello@hlaverify.com.
/v1/verify — check every allele-shaped token in free textSend HLA typing report text or a model's answer about HLA, with patient identifiers removed first. Every token that looks like an allele is classified: valid, group (G/P), deleted (with successor), fabricated_group, hallucinated, mac_code (an NMDP multiple allele code such as A*02:AB — reporting shorthand this service does not expand, surfaced so it is never silently skipped), or out_of_scope (a KIR allele name such as KIR3DL1*001 — IPD-KIR is a separate reference outside IPD-IMGT/HLA, so the name is surfaced, not checked, and never counted as fabricated). Two-field A*02:01g and the XX code A*02:XX are checked by the name they abbreviate; legacy spellings — colon-less pre-2010 names (A*0201, DRB1*1501) and the old Cw locus label (Cw*0702, Cw*07:02) — by the current name they stand for, reported with current_2field and the flag deprecated_name, so a report written in the old format is valid, not fabricated, while a made-up Cw*9999 is still hallucinated. clean is true only when nothing is fabricated, deleted, or a made-up group; check counts.mac_code and counts.out_of_scope separately.
curl -s https://api.hlaverify.com/v1/verify -H 'content-type: application/json' \
-d '{"text": "Reported typing: A*0101, B*15:504:01, DRB1*14:06. Assistant suggested DQB1*05:03:26:99 (DQB1*05:03:01G)."}'
{"release":"3.66.0","clean":false,
"counts":{"valid":3,"deleted":0,"group":1,"fabricated_group":0,"hallucinated":1,"mac_code":0,"out_of_scope":0},
"tokens":[{"token":"A*0101","status":"valid","current_2field":"A*01:01","g_group":"AMBIGUOUS",
"flags":["deprecated_name"],"note":"assigned in this release (or a valid lower-resolution prefix)"},
{"token":"DQB1*05:03:26:99","status":"hallucinated","note":"no such name in any release back to 1.05.0 — fabricated"}, …],
"attribution":"Computed from IPD-IMGT/HLA (Barker DJ et al., Nucleic Acids Res 2025), …"}
/v1/normalize — bring reported typings to the current releaseAny era and any reporting shorthand: colon-less 1990s strings (A*0101, Cw*0702), the old Cw locus label on a colon-style name (Cw*07:02), deleted names, lower-resolution prefixes, G and P group names (A*02:01:01G, DPB1*04:01P), the XX code (A*02:XX), two-field A*02:01g, and an optional HLA- prefix. Returns the current name, the comparable 2-field name (keeping an expression suffix only when every full-resolution allele shares it), the G group (NONE / AMBIGUOUS), and flags such as deprecated_name, null_allele, nonexistent_allele, g_group_name, p_group_name, xx_code, lg_notation. NMDP multiple allele codes (A*02:AB) are not expanded: they return UNRESOLVABLE with the flag mac_code.
curl -s https://api.hlaverify.com/v1/normalize -H 'content-type: application/json' \
-d '{"typings": ["A*0101", "A*01:34N", "DRB1*1406", "A*24:09N", "B*9999"]}'
/v1/allele/{name} — the facts for one nameassigned (G/P group, first release, confirmed status, WMDA serology, null flag). serology values are the serological equivalents as WMDA publishes them in rel_dna_ser.txt, passed through as published: an antigen number, ? for an undetermined serological equivalent (TAP1*01:01:01:01), or ∅ for no serological equivalent (null). The lab routes (/v1/typing/check) report the same two markers as uncertain and null, valid_prefix (member count and sample), group (a G or P group name: type, member count and sample), or deleted (successor). The same reported shorthands /v1/normalize accepts resolve to the name they stand for and answer with its facts plus resolves_to and a flag, so nothing is converted silently: legacy spellings (A*0101, Cw*0702, Cw*07:02 → deprecated_name), the XX code (A*02:XX → xx_code), two-field A*02:01g (lg_notation) and an optional HLA- prefix. An NMDP multiple allele code (A*02:AB) returns mac_code: recognised, not expanded. 404 for anything not in the release. Class I (A/B/C) names carry a trailing ligands object: expression, the -21 leader residue (leader_21: M/T, Petersdorf 2020), Bw4/Bw6 (bw), the C1/C2 epitope (c_group), and the aggregate kir_ligand class, each aggregated over the name's members with an ambiguities map when they disagree.
curl -s 'https://api.hlaverify.com/v1/allele/A*24:09N'
/v1/allele/{name}/history — every name an allele has had, release by releaseFor a name as it was written in any IPD-IMGT/HLA release since 1.05.0 (A*0101, A*010101, A*01:01:01:01), the allele behind it (its HLA ID), its current name, the first and last release that used this name, the full timeline of names ({name, from, to}, oldest first), and the deletion note if the name was retired. ?as_of=3.40.0 adds what the allele was called in that release. A name used for more than one allele lists each. Unknown names are 404. From IPD-IMGT/HLA's own Allelelist_history.txt and Deleted_alleles.txt. Billable like /v1/allele.
curl -s 'https://api.hlaverify.com/v1/allele/A*0101/history?as_of=2.10.0'
/v1/translate — a list of names from one release to another{"names": [...], "from": "3.39.0", "to": "3.66.0"}. from is the release the names were written in (omit it to accept a name used in any release); to defaults to the current release. Each row has a status: unchanged, renamed (with to_name), deleted (with the reason, the successor and the successor's name in to), not_yet_named (the allele was named after to), not_in_from_release (with the releases that did use the name), ambiguous (one name, several alleles; each listed) or unknown. Up to your tier's batch size (250 free, 5,000 Lab and up); one call, whatever the batch. For lab-written shorthand like B*1501 or A*02:XX, use /v1/normalize first.
curl -s https://api.hlaverify.com/v1/translate -H 'content-type: application/json' \
-d '{"names": ["A*24:02:03", "C*12:139", "A*0101"], "to": "3.66.0"}'
/v1/releases/diff?from=&to= — what changed between two releasesEvery allele name added, removed (with the deletion reason and successor) and renamed between any two releases, optionally for one locus. counts are always complete; each list is cut at limit (default 500, up to 5,000) and truncated says so. Either direction works. Billable, one call.
curl -s 'https://api.hlaverify.com/v1/releases/diff?from=3.58.0&to=3.66.0&locus=A&limit=50'
/v1/releases — the releases the history coversEvery IPD-IMGT/HLA release from 1.05.0 to the current one, oldest first, and the loci in the history. Free and never counted.
/v1/match — donor–recipient match verdictTwo reported alleles per locus, any nomenclature era, G/P group names included (a typing reported at G-group resolution matches by the 2-field name it is written under). Frameworks 6/6, 8/8, 10/10, 12/12, antigen. Counts per chromosome, not per locus; GvH and HvG mismatches reported separately; a locus whose typing is too coarse to call is potential and excluded from the denominator with resolution_insufficient — a confident count over unresolvable typing is itself the error. Null alleles hiding inside serologic matches (the A*24:09N trap) raise null_allele_mismatch. Rules R1–R6 are published in rules.py for lab audit.
curl -s https://api.hlaverify.com/v1/match -H 'content-type: application/json' -d '{
"framework": "8/8",
"recipient": {"A": ["A*02:01", "A*24:02"], "B": ["B*07:02", "B*44:02"], "C": ["C*07:02", "C*05:01"], "DRB1": ["DRB1*15:01", "DRB1*04:01"]},
"donor": {"A": ["A*02:01", "A*24:09N"], "B": ["B*07:02", "B*44:02"], "C": ["C*07:02", "C*05:01"], "DRB1": ["DRB1*15:01", "DRB1*04:01"]}}'
{"release":"3.66.0","framework":"8/8","count":"7/8",
"verdicts":{"A":"mismatch","B":"match","C":"match","DRB1":"match"},
"hvg_mismatches":1,"gvh_mismatches":1,"flags":["null_allele"], "attribution":"…"}
/v1/typing/check — QC one typing (all loci)Lab Toolkit. {"typing": {"A": [...], "B": [...], "DRB1": [...], ...}}, any nomenclature era, up to 4 reported alleles per locus. Resolves every reported string, flags unresolvable / deprecated_name / locus_mismatch / null_allele per allele and too_many_alleles / single_allele / homozygous per locus, and returns a profile (B-leader -21 M/T genotype, C KIR-ligand genotype, KIR ligands present, and whether the A/B/C typing is complete enough for a confident KIR-ligand call) plus a drb345 expected-vs-reported block when a DRB1 key is present. Units metered = total reported strings.
curl -s https://api.hlaverify.com/v1/typing/check -H 'content-type: application/json' -d '{
"typing": {"A": ["A*02:01"], "B": ["B*07:02", "B*46:01"], "C": ["C*07:02", "C*01:02"], "DRB1": ["DRB1*15:01", "DRB1*04:01"]}}'
/v1/compat — donor/recipient immunogenetic compatibilityLab Toolkit. {"recipient": typing, "donor": typing} (each shaped as in /v1/typing/check). Two published models, each run over the full typing QC on both sides: HLA-B leader match (-21 M/T; Petersdorf 2020, Blood) for a single HLA-B mismatch, and KIR ligand (C1/C2/Bw4) class comparison, which requires complete A, B and C typing. Decision support only; not a medical device. Units metered = total reported strings on both sides.
curl -s https://api.hlaverify.com/v1/compat -H 'content-type: application/json' -d '{
"recipient": {"B": ["B*07:02", "B*44:02"]}, "donor": {"B": ["B*07:02", "B*44:03"]}}'
/v1/glstring — GL String validation and normalizationLab Toolkit. {"gl": "A*01:01/A*02:01+A*03:01~B*07:02"} — the ^ locus-block / | genotype-list / + genotype / ~ haplotype / / allele-list grammar, up to 5,000 allele tokens. Resolves and renames every allele token and flags structural problems (mixed_locus_allele_list, haplotype_repeats_locus, more_than_two_haplotypes, genotype_loci_differ, genotype_list_loci_differ, locus_repeated_across_blocks, empty_element, whitespace_in_name), returning a normalized string with outdated names rewritten to current. Units metered = allele token count (including empty slots from a doubled separator).
curl -s https://api.hlaverify.com/v1/glstring -H 'content-type: application/json' -d '{"gl": "A*0101+A*02:01"}'
/v1/pgx/table — the HLA pharmacogenomics table (open data)Pharmacogenomics. The whole hla-pgx-open table — published CPIC guideline recommendations and PharmGKB clinical annotations that key on an HLA allele (abacavir and B*57:01, carbamazepine and B*15:02 / A*31:01, allopurinol and B*58:01, …), one row per source record × allele × drug, with every allele name checked against the pinned IPD-IMGT/HLA release. JSON by default, ?format=csv for the file. Free, no key, not billable, cached for a day; CC BY-SA 4.0 — the response carries the attribution block, and every pgx response names the pgx_snapshot (UTC date and git commit of the table) it was answered from, the way every verdict names the release.
curl -s https://api.hlaverify.com/v1/pgx/table?format=csv -o hla_pgx.csv
/v1/pgx/screen — which drugs a typing carries a published HLA association forPharmacogenomics. {"typing": {"A": [...], "B": [...], ...}} (shaped as in /v1/typing/check) or {"alleles": ["B*5701", "A*31:01", ...]} — exactly one of the two, any nomenclature era or shorthand. Every name goes through /v1/normalize first (that is the point: guideline lookups need clean two-field names, and labs report every era), so each row carries current_name, allele_2field and the usual flags — nothing is converted silently — and then joins the table on the two-field form. Per allele: associations[] (source, drug, evidence level, CPIC testing recommendation, allele status, implication, recommendation, guideline or annotation URL, PMIDs). summary.actionable is the subset a prescriber acts on: CPIC level A, allele-positive. Billable like /v1/normalize; units metered = names sent. Decision support only; not a medical device.
curl -s https://api.hlaverify.com/v1/pgx/screen -H 'content-type: application/json' -d '{"alleles": ["B*5701", "A*3101"]}'
{"release":"3.66.0","pgx_snapshot":{"date":"2026-10-12","commit":"35bcc18…"},"scope":"Guideline lookup over published CPIC and PharmGKB associations…",
"alleles":[{"reported":"B*5701","current_name":"B*57:01:01:01","allele_2field":"B*57:01","flags":["deprecated_name"],
"associations":[{"source":"cpic","drug":"abacavir","evidence_level":"CPIC A","pgx_testing":"Testing Required","allele_status":"positive",
"recommendation":"Abacavir is not recommended", "guideline_url":"https://www.clinpgx.org/guideline/PA166251444", "…":"…"}]}, "…"],
"summary":{"names":2,"unresolvable":0,"with_associations":2,"actionable":["…"],"drugs_actionable":["abacavir","carbamazepine"]},
"license":"CC-BY-SA-4.0","attribution":"…"}
/v1/pgx/pair — which genome carries each actionable association, donor and recipientPharmacogenomics. {"recipient": {"typing": {...}} | {"alleles": [...]}, "donor": {...}}. Screens both sides exactly as /v1/pgx/screen does, then lists every actionable association with in_recipient, in_donor, discordant (present in exactly one) and whose_hla (both / recipient_only / donor_only). After an allogeneic transplant the recipient's blood carries the donor's genome, and which genome should drive an HLA-mediated drug alert is an open question (NCT06856226): this endpoint reports both and decides nothing. Billable like /v1/match; units metered = names on both sides. Decision support only; not a medical device.
curl -s https://api.hlaverify.com/v1/pgx/pair -H 'content-type: application/json' -d '{
"recipient": {"alleles": ["B*57:01", "A*02:01"]}, "donor": {"alleles": ["B*44:02", "A*02:01"]}}'
/v1/checkout — start a Stripe Checkout Session for a paid tier{"tier": "starter"|"lab"|"scale"} (or a specific active price id, as a cross-check on tier). Returns a hosted Stripe Checkout url to send the buyer to; the buttons on /pricing call this endpoint. On a completed checkout, Stripe's webhook creates the key — see Self-serve keys above for whether that is switched on for real purchases right now. Free and not billable against the daily quota, but rate limited like every other /v1/ route. 503 when self-serve checkout is not configured on this deployment or the tier has no live price.
curl -s https://api.hlaverify.com/v1/checkout -H 'content-type: application/json' -d '{"tier": "starter"}'
/v1/beta-signup — join the beta list for paid keys{"email": "you@lab.example", "org": "…", "use_case": "…", "source": "…"} — only email is required; org (≤120 chars), use_case (≤500) and source (≤120, a free-text hint such as site, mcp or docs) are optional. Returns {"ok":true,"status":"recorded"|"already_recorded","message":"…","release":"3.66.0"}; signing the same address twice is not an error and does not create a second record. Rejections are the usual {"detail": "…"}: 422 for an address that doesn't look like one or a field over its cap. We store the address, the optional fields, a timestamp and the CF-IPCountry of the request — no IP address, nothing else, and the record is not an API key. Need a higher limit today? Email hello@hlaverify.com for a beta key.
curl -s https://api.hlaverify.com/v1/beta-signup -H 'content-type: application/json' \
-d '{"email": "you@lab.example", "org": "Example HLA Lab", "use_case": "LIMS ingest QC", "source": "docs"}'
/v1/research-access — apply for free academic and nonprofit access{"email": "…", "institution": "…", "use_case": "…", "expected_volume": "…", "source": "…"}. email, institution (≤200 chars) and use_case (≤1,000) are required; expected_volume (≤120) and source (≤120) are optional. Returns {"ok":true,"status":"recorded"|"already_recorded","message":"…","release":"3.66.0"}; applying twice from the same address is not an error and does not create a second record or overwrite a decision already made. Rejections are the usual {"detail": "…"}: 422 for an address that doesn't look like one, a missing or empty required field, or a field over its cap. We store what you send plus a timestamp, the CF-IPCountry of the request and a status. No IP address is stored, and the record is not an API key.
How approval works, plainly: a person reads every application. There is no automatic decision, no SLA, and no guarantee. If it is approved you are emailed a single-use promotion code; you then pick a tier at /pricing and enter the code at checkout, which takes 100% off for 12 months. Because the total is then $0, Stripe asks for no card, and your API key is issued by the same webhook that issues a paid one. The code is good for one redemption and expires; commercial labs should buy a tier instead.
curl -s https://api.hlaverify.com/v1/research-access -H 'content-type: application/json' \
-d '{"email": "you@lab.example", "institution": "Example University", "use_case": "Retyping QC for a 4,000-donor registry cohort study", "expected_volume": "about 20,000 typings a month", "source": "docs"}'
/v1/report — report an answer you believe is wrong{"kind": "wrong_result", "endpoint": "/v1/verify", "input": "…", "received": "…", "expected": "…", "details": "…", "email": "…", "request_id": "…", "source": "…"}. input (≤2,000 chars: the names or request you sent) and expected (≤1,000: what you expected instead) are required. kind is one of wrong_result (default), docs_mismatch, outdated_release, question or other; endpoint (≤60), received (≤4,000: what the API answered), details (≤2,000), email (to hear back), request_id and source are optional. Returns {"ok":true,"id":"r…","status":"received","status_url":"https://hlaverify.com/support?id=r…","message":"…","release":"…"}. Not billable. The same form is at hlaverify.com/support.
What happens next: every report is rerun against the live API and the IPD-IMGT/HLA reference data, and a person confirms any error before it is published; a confirmed error is fixed and listed with the release that fixed it. Do not include patient identifiers of any kind, names included: send only the HLA names or the request. Obvious identifiers (record-number and date-of-birth labels, Social Security numbers, US-format dates) are refused with 422 and nothing is stored; a name cannot be detected, so leave names out. At most 20 reports a day from one connection (429 after that).
curl -s https://api.hlaverify.com/v1/report -H 'content-type: application/json' \
-d '{"kind": "wrong_result", "endpoint": "/v1/verify", "input": "B*07:02", "expected": "a valid allele, not flagged", "email": "you@lab.example"}'
/v1/report/{id} — follow a reportReturns {"id","kind","status","created","updated","note","fixed_in","release_at_report"}, where status is received, investigating, confirmed, fixed, expected_behavior, duplicate or needs_info, note is the public explanation and fixed_in the release that fixed it. Never the input or the email address. Unknown ids are 404. Not billable.
curl -s https://api.hlaverify.com/v1/report/r0123456789abcdef
/v1/usage - your own call volumeAny keyed caller, any tier, can read back its own usage: calls and units today, over the last 7 days and over the last 30 days, each broken down by endpoint and by response status, plus the tier's published daily quota (keys.js TIER_LIMITS) so a dashboard can show "calls used vs allowed" without separately parsing the x-hla-verify-daily-* headers on every call. Scoped strictly to the presented key's own label; there is no cross-key view here (that is the operator dashboard below, a different secret). Anonymous callers get 401: anonymous traffic is counted per IP-day for quota purposes (quota.js), not per caller, so there is no label to report on. Answers are cached for 60 seconds per key, so a dashboard refresh cannot turn into Analytics Engine SQL API traffic. Not billable against the daily quota. 503 if this deployment has no Analytics Engine read access configured.
curl -s https://api.hlaverify.com/v1/usage -H 'X-API-Key: YOUR_KEY'
{"release":"3.66.0","tier":"lab","daily_quota":10000,"max_typings":5000,
"usage":{"today":{"calls":412,"units":890,"by_endpoint":{"verify":300,"normalize":112},"by_status":{"200":405,"429":7}},
"7d":{"calls":2801,"units":6110,"by_endpoint":{"…":"…"},"by_status":{"…":"…"}},
"30d":{"calls":11402,"units":24980,"by_endpoint":{"…":"…"},"by_status":{"…":"…"}}}}
/admin/usage - operator dashboardDeployment-wide usage, protected by a separate ADMIN_TOKEN secret (Authorization: Bearer …); not a customer API key, and never accepted as one. Totals for 24h/7d/30d (calls, unique key labels, anonymous calls), a 30-day daily series, breakdowns by endpoint, by tier and by status class (2xx/4xx/5xx), the top 20 key labels by calls, p50/p95 latency and the overall error rate. Also an unmet requests report: for each of a fixed list of request shapes the service cannot fully serve (an NMDP MAC code, a serologic antigen in an allele slot, a slash-list ambiguity string, a name that would resolve after trivial cleanup, a locus outside IPD-IMGT/HLA, a match framework or MCP tool that does not exist, a batch over the cap, a spent quota, …) how many requests and how many distinct callers hit it over 30 days, which endpoints it arrived through, and a score that ranks the gaps by how many callers — and how many paying, repeat callers — are asking. Every call writes one small integer per shape next to the usual counts; the strings that produced them are never written, and a categorical value (a gene symbol, a framework spec, a tool name) is recorded only from a closed or gene-symbol-shaped set. JSON by default; add Accept: text/html or ?format=html for a small self-contained dashboard page with inline SVG bar charts; no external assets, matching this page's styling. 401 without a valid admin token, 503 if this deployment has no admin token or no Analytics Engine read access configured.
/healthz{"ok":true,"release":"3.66.0","alleles":47424,"exported_at":"2026-10-16T17:29:25Z","uptime_s":…} — exported_at identifies the build (the tables are regenerated on every deploy).
A remote MCP server lives at POST /mcp (Streamable HTTP transport, JSON-RPC 2.0, stateless — one JSON response per call, no session to manage). It exposes the same deterministic lookups as the REST API as tools: verify_text, normalize_allele, allele_info, match_score, check_typing, donor_compat, validate_gl_string, pgx_lookup, pgx_pair, the release history tools translate_names, release_diff and allele_history, about, plus the two tools that write: beta_signup (it joins the beta list) and research_access (it files a research access application), each doing exactly what its endpoint above does. Results are byte-identical to the matching /v1/… response because both run the same code underneath. donor_compat, pgx_lookup and pgx_pair are decision support only; not a medical device. The same input rule applies as on REST: send allele names, typing strings, GL strings and HLA report text, never patient identifiers. Anonymous access shares the free tier's 100 calls a day and 60 req/min; an API key on /mcp gets the same tier, daily quota and batch cap as on REST. A tool call over quota comes back on HTTP 200 as an ordinary tool result with isError:true and a message naming the reset time, with x-hla-verify-daily-remaining: 0 and Retry-After on the response: MCP clients treat a non-2xx POST as a transport failure and never read the frame, so the refusal is sent the one way an agent actually sees it. REST keeps its 429.
Protocol versions: 2026-07-28 (stateless — call server/discover for versions, capabilities and instructions; send the MCP-Protocol-Version, Mcp-Method and, for tools/call, Mcp-Name headers) and, through the initialize handshake, 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05. Clients that support both, such as the Cloudflare Agents SDK, pick the newest automatically.
Discovery before connecting: an MCP Server Card (SEP-2127) at GET /mcp/server-card (also /.well-known/mcp/server-card.json) gives name, version, endpoint, headers and protocol versions without a handshake. For domain-level crawlers, /.well-known/ard.json lists it per the Agentic Resource Discovery spec (identifier, capabilities and representative queries), and /.well-known/ai-catalog.json does the same for its predecessor. Tools are not in the card — call tools/list. The repository's server.json describes the same server (com.hlaverify/hla-verify) for the official MCP Registry.
{"mcpServers": {"hla-verify": {"url": "https://api.hlaverify.com/mcp"}}}
{"mcpServers": {"hla-verify": {"url": "https://api.hlaverify.com/mcp",
"headers": {"Authorization": "Bearer YOUR_KEY"}}}}
.cursor/mcp.json){"mcpServers": {"hla-verify": {"url": "https://api.hlaverify.com/mcp"}}}
Prefer a local process instead? python -m sci_envs.mcp_server serves the same tools over stdio except allele_info — see llms.txt.
| Where | Call | Gate on |
|---|---|---|
| Typing report ingest (LIMS, HistoTrac/TIMS exports, PDF-to-text) | /v1/normalize per reported allele | nonexistent_allele → reject; deprecated_name → rewrite to current_name and log |
| Any LLM or agent output that mentions HLA | /v1/verify on the text | clean == false → block or annotate before display |
| Search / match reports | /v1/match per pair | Compare with the lab's count; any potential or null_allele* flag routes to human review |
| Registry / data-warehouse QC | /v1/normalize in batches up to your tier's cap (x-hla-verify-max-typings) | Diff reported vs current_name per release |
Python (no HTTP): pip install "verifiable-science-envs @ git+https://github.com/jasonbrelsford/verifiable-science-envs", then from sci_envs.families.nomenclature.normalize import normalize and from sci_envs.families.matching.rules import score — the same engine that computed these tables. Agents: MCP server python -m sci_envs.mcp_server with tools verify_text, normalize_allele, match_score, check_typing, donor_compat, validate_gl_string, about; see llms.txt.
The same verification service as a container, no API key or network dependency on hlaverify.com: docker run -p 8000:8000 ghcr.io/jasonbrelsford/hla-verify:latest (once a tagged release publishes one), or build it yourself from the repository (docker build -t hla-verify -f sci_envs/service/Dockerfile .), or run the Python service directly (pip install -e ".[service]" && uvicorn sci_envs.service.app:app). Details and the current image tags: README.
This deployment is pinned to 3.66.0 for every caller — one release for the whole deployment, not a value that varies by key. The tables were exported 2026-10-16T17:29:25Z from the release's own files (Allelelist, Deleted_alleles, Allelelist_history, hla_nom_g/p, rel_dna_ser). IPD-IMGT/HLA publishes quarterly; when this deployment's pin moves to a new release, that move is a deliberate, recorded step (see the release runbook), not a silent change. Keyed customers are told by email before the deployment-wide pin moves, with a diff of changed verdicts — a manual step in that runbook, not an automated feature. Per-key pinning — holding an individual key on an older release — is on the roadmap and not built today.
Errors are JSON {"detail": "…"}: 400 malformed JSON, 401 missing/invalid/revoked key, 404 unknown name or route, 415 wrong content type, 422 invalid input, 429 rate limited or daily quota exhausted (tier-specific, with Retry-After), 500 (nothing stored). MCP tools/call validation failures are not HTTP or JSON-RPC errors — they come back as a normal tool result with isError:true and an explanatory text block, per the MCP spec.
HLA-Verify validates HLA nomenclature and match arithmetic against a pinned reference release. It is a research-and-evaluation tool and not a medical device; it is not clinical decision support, and output supports and does not replace clinical judgement. Send allele names and HLA report text only: the service is not designed to receive protected health information, does not need patient identifiers, and de-identifying input before sending is the caller's responsibility. Service code: PolyForm Noncommercial 1.0.0 — commercial use requires a licence from Brelsford Software LLC (hello@hlaverify.com). Reference data: IPD-IMGT/HLA (Barker DJ et al., Nucleic Acids Research 2025), CC-BY-ND, fetched from the official source and never redistributed in bulk. Requests are processed in memory and discarded; metering records counts per key, never content.