← All articles
TechnicalAugust 12, 2026

CVX Code Lookup API: Vaccine Codes to FHIR Immunizations

CVX is the vocabulary every immunization record speaks. Here's how to look up and search vaccine codes, crosswalk them to NDC, and build a FHIR Immunization — with working TypeScript.

CVX vaccine code lookup API tutorial

If your application records that someone got a shot, it speaks CVX. Every flu vaccine, every childhood MMR, every COVID-19 booster that moves between an EHR, a state immunization registry, and a payer carries a CVX code to say what was administered.

CVX is small — under a thousand codes, maintained by the CDC, public domain. That makes it look like the easy one. It isn't, and the reason is worth understanding before you write the lookup.

Why CVX Is Trickier Than Its Size Suggests

Most terminology work is about finding the right code. CVX work is mostly about codes that are no longer right but still appear in your data.

Immunization records are historical by nature. A patient's chart holds shots given in 2009, 2021, and last week, and the codes for those shots have been retired, replaced, and reactivated in the meantime. A lookup that only resolves currently-active codes will fail on a large share of real records.

Here's what that looks like in practice:

curl -H "x-api-key: $FHIRFLY_API_KEY" \
  "https://api.fhirfly.io/v1/cvx/207"
{
  "code": "207",
  "display": "COVID-19, mRNA, LNP-S, PF, 100 mcg/0.5mL dose or 50 mcg/0.25mL dose",
  "status": "inactive",
  "is_covid_vaccine": true,
  "vaccine_type": "mRNA",
  "last_updated_by_cdc": "2023-10-24",
  "notes": "Original monovalent Moderna adult vaccine. FDA rescinded all doses for US use 4/18/2023. Continue to used to record historic US Moderna vaccines and those administered in non-US locations (includes tradename Spikevax)"
}

Code 207 is inactive. It is also one of the most common codes in any US immunization history from 2021–2022. The CDC's own notes field says exactly what to do with it: keep using it for historical records.

So status is not a filter you apply blindly — it's a signal about context. Use active to constrain order-entry and new-administration pick-lists. Do not use it to constrain lookups of existing records, or you'll drop real clinical history on the floor.

Setup

npm install @fhirfly-io/terminology
import { Fhirfly } from "@fhirfly-io/terminology";

const fhirfly = new Fhirfly({ apiKey: process.env.FHIRFLY_API_KEY! });

Lookups require the cvx.read scope. The free tier includes 10,000 requests/month.

Look Up a Single Code

const result = await fhirfly.cvx.lookup("141");

console.log(result.data.display);
// "Influenza, split virus, trivalent, preservative"

console.log(result.data.full_vaccine_name);
// "Influenza, split virus, trivalent, injectable, contains preservative"

console.log(result.data.status);
// "active"

console.log(result.data.notes);
// "Trivalent code reactivated for 2024/2025 season"

That last field is the CVX lifecycle in one line. Code 141 was retired, then brought back when trivalent formulations returned for the 2024/2025 season. Codes move in both directions, which is why you want the notes and last_updated_by_cdc fields available rather than caching a display string and forgetting about it.

One formatting detail that catches people: some CVX codes are zero-padded. MMR is 03, not 3. Hepatitis B pediatric is 08. Pass them through as strings exactly as they appear in your source data.

Response Shapes

ShapeFieldsBest for
compactCode, display, statusAutocomplete, pick-lists
standard+ full name, notes, vaccine type, COVID flag, FHIR codingRecord processing, FHIR mapping
full+ short description, ingest metadataAI agents, provenance, auditing

standard is the default. Drop to compact when you're rendering a list and don't need the prose:

const result = await fhirfly.cvx.lookup("03", { shape: "compact" });
// { code: "03", display: "MMR", status: "active" }

FHIR-Ready Coding

Every standard response includes a pre-built coding block:

console.log(result.data.fhir_coding);
// {
//   system: "http://hl7.org/fhir/sid/cvx",
//   code: "141",
//   display: "Influenza, split virus, trivalent, preservative"
// }

That goes straight into Immunization.vaccineCode.coding — more on that below.

Search When You Don't Have a Code

Most immunization work starts from a description or a category, not a code. Search accepts free text plus structured filters.

const results = await fhirfly.cvx.search({ q: "hepatitis", status: "active" }, { limit: 5 });

console.log(results.total);
// 10

for (const vaccine of results.items) {
  console.log(`${vaccine.code} — ${vaccine.display}`);
}
// 08 — Hep B, adolescent or pediatric
// 43 — Hep B, adult
// 104 — Hep A-Hep B
// 30 — HBIG
// 52 — Hep A, adult

Responses include facets, which are useful for building filter UIs without a second round trip:

const flu = await fhirfly.cvx.search({ q: "influenza" });

console.log(flu.facets);
// {
//   status: { active: 30 },
//   is_covid_vaccine: { false: 30 },
//   vaccine_type: { inactivated: 3, live: 3 }
// }

You can also filter on the structured fields directly. is_covid_vaccine is a first-class boolean rather than something you have to infer from display text:

const covid = await fhirfly.cvx.search({ is_covid_vaccine: true, status: "active" });
console.log(covid.total); // 6

Batch Lookups

Immunization histories arrive as sets. Resolve up to 100 codes per request:

const batch = await fhirfly.cvx.lookupMany(["207", "208", "141", "03"]);

console.log(batch.count); // 4

for (const entry of batch.results) {
  if (entry.status === "ok") {
    console.log(`${entry.input} → ${entry.data.display}`);
  } else {
    console.warn(`${entry.input} did not resolve`);
  }
}

Each result carries the input you sent alongside the resolved code, so you can join back to your source rows without tracking array positions.

Crosswalk CVX to NDC

A CVX code says what kind of vaccine. It does not say which product. For claims, inventory reconciliation, or lot-level tracking, you need the NDC.

const result = await fhirfly.cvx.lookup("207", { include: ["crosswalks"] });

for (const ndc of result.data.crosswalks.ndc) {
  console.log(`${ndc.ndc} — ${ndc.trade_name} (${ndc.manufacturer})`);
}
// 80777-0273-15 — Moderna COVID-19 Vaccine (Moderna US, Inc.)
// 80777-0273-10 — Moderna COVID-19 Vaccine (Moderna US, Inc.)
// 80777-0100-11 — Spikevax (Moderna US, Inc.)

Each crosswalk entry includes ndc, ndc11, product_ndc, trade_name, manufacturer, source, and a begin_date/end_date pair. Note that those dates are YYYYMMDD strings, not ISO dates — parse accordingly.

The date range matters more than it looks. One CVX code maps to several NDCs over time, and the correct one depends on when the dose was given. If you're reconciling a 2021 administration, filter the crosswalk by the administration date rather than taking the first entry.

For the manufacturer side, MVX codes resolve separately:

const mvx = await fhirfly.mvx.lookup("MOD");
console.log(mvx.data.manufacturer_name); // "Moderna US, Inc."

CVX and MVX together are how immunization registries uniquely identify a vaccine product.

Build a FHIR Immunization

The payoff. @fhirfly-io/fhir-builder has a cvxCode() convenience method that sets the code system for you:

import { FHIRBuilder } from "@fhirfly-io/fhir-builder";

const fb = new FHIRBuilder();
const lookup = await fhirfly.cvx.lookup("207");

const immunization = fb.immunization()
  .status("completed")
  .cvxCode(lookup.data.code, lookup.data.display)
  .patient("Patient/example")
  .occurrenceDateTime("2026-08-12")
  .primarySource(true)
  .lotNumber("039K20A")
  .doseQuantity(0.5, "mL")
  .build();
{
  "resourceType": "Immunization",
  "status": "completed",
  "vaccineCode": {
    "coding": [
      {
        "system": "http://hl7.org/fhir/sid/cvx",
        "code": "207",
        "display": "COVID-19, mRNA, LNP-S, PF, 100 mcg/0.5mL dose or 50 mcg/0.25mL dose"
      }
    ]
  },
  "patient": { "reference": "Patient/example" },
  "occurrenceDateTime": "2026-08-12",
  "primarySource": true,
  "lotNumber": "039K20A",
  "doseQuantity": { "value": 0.5, "unit": "mL" }
}

primarySource is worth setting deliberately. It marks whether the record came from the entity that administered the dose (true) or was reported second-hand — a patient recalling a childhood shot, or a record imported from another registry (false). Registries treat those very differently.

Error Handling

Malformed codes fail validation before they reach the data:

curl -H "x-api-key: $FHIRFLY_API_KEY" "https://api.fhirfly.io/v1/cvx/9999"
{
  "error": "invalid_cvx_code",
  "code": "9999",
  "message": "Invalid CVX code format (must be 1-3 digits, 1-999)"
}

That's a 400, not a 404 — the code isn't merely absent, it can't exist. Worth distinguishing in your own error surface, because a 400 usually means a mapping bug upstream (an NDC or a free-text vaccine name that leaked into a CVX field), while a 404 means a legitimately unrecognized code.

try {
  const result = await fhirfly.cvx.lookup(code);
  return result.data;
} catch (err) {
  if (err.status === 400) {
    // Upstream mapping problem — log the source record
    logger.warn({ code }, "malformed CVX code in source data");
    return null;
  }
  throw err;
}

Key Takeaways

  • status: "inactive" does not mean "ignore". Retired codes are the backbone of historical immunization records. Filter on active for order entry, never for record lookup.
  • CVX codes are strings, and some are zero-padded. 03 is MMR; 3 is not.
  • Codes get reactivated. Code 141 came back for the 2024/2025 season. Read notes and last_updated_by_cdc instead of caching display text indefinitely.
  • CVX alone doesn't identify a product. Add the NDC crosswalk for claims and inventory, and MVX for the manufacturer — and filter crosswalks by administration date.
  • A 400 is a different bug than a 404. Malformed codes point at a mapping problem upstream, not missing data.

Further Reading

CVX data comes from the CDC's Immunization Information Systems and is public domain. Every response includes source and license metadata under meta.legal.

Tagscvxtutorialimmunizationfhirapi
Written by The FHIRfly Team — healthcare data, AI, and interoperability folks building better clinical coding APIs.

Build it on real terminology

Try any endpoint live — no sign-up required.

© 2026 FHIRfly.io LLC. All rights reserved. · Terminology data sourced from official registries, updated daily.