Skip to main content
Version: v0.1.0

Cookbook

Task-oriented recipes for the common reads. Each one is a few lines over a parsed message, using the real subpath exports: @cosyte/ncpdp/script for ePrescribing XML, @cosyte/ncpdp/telecom for the PBM claim wire, @cosyte/ncpdp/common for the shared value wrappers.

The rules underneath every recipe are the same: the parser is lenient (vendor quirks become stable-coded warnings, not failures), the model is immutable, and safety-critical dispositions are fail-safe: a failure is never quietly read as a success, and money is never a float.

All XML and wire samples below are synthetic. Fixtures must never carry real PHI: no real patient names, member IDs, prescriber NPIs, or NDCs.

Parse a SCRIPT NewRx​

Read a NewRx ePrescribing message: header, patient, prescribed medication, and the coded product. parseScript never throws on a vendor quirk. It collects warnings and reads best-effort. newRx projects the NewRx body, or returns undefined for any other transaction.

import { parseScript, newRx } from "@cosyte/ncpdp/script";

const xml = `<Message version="2017071">
<Header><MessageID>SYNTH-MSG-0001</MessageID></Header>
<Body><NewRx>
<Patient><HumanPatient>
<Name><LastName>DOE</LastName><FirstName>JANE</FirstName></Name>
</HumanPatient></Patient>
<MedicationPrescribed>
<DrugDescription>Amoxicillin 500 MG Oral Capsule</DrugDescription>
<DrugCoded><ProductCode Qualifier="ND">00000000001</ProductCode></DrugCoded>
<Quantity><Value>30</Value></Quantity>
<Sig><SigText>Take 1 capsule by mouth three times daily for 10 days.</SigText></Sig>
</MedicationPrescribed>
</NewRx></Body>
</Message>`;

const msg = parseScript(xml);

msg.header.messageId; // => "SYNTH-MSG-0001"

const rx = newRx(msg); // the NewRx body, or undefined for another transaction
rx?.patient?.name?.lastName; // => "DOE"
rx?.medication?.description; // => "Amoxicillin 500 MG Oral Capsule"
rx?.medication?.coded?.productCode?.value; // => "00000000001"
rx?.medication?.coded?.productCode?.system; // => "NDC"
rx?.medication?.quantity?.value?.source; // => "30"
  • Lenient by default. Vendor quirks (own-text dates, alternate element shapes, an absent version) become warnings with an XPath position, not failures. Only unrecoverable structural corruption throws a typed NcpdpScriptParseError: empty input, non-XML, a non-<Message> root, a <!DOCTYPE>/<!ENTITY> payload (the XXE boundary), or a pre-XML legacy version.
  • Coded product carries its own system. coded.productCode.system is recognized from the wire qualifier ("NDC", "RXNORM", "SNOMED", …); the raw value is always kept verbatim.
  • A warning is a code plus an XPath position, by construction. The message text comes from a frozen registry keyed by the code, and the factory that builds a warning takes no value argument at all, so no part of a document can reach it. The path is assembled only from element names this parser recognizes. Log .warnings whole; the parsed model is the part that holds patient data.

Read a SCRIPT response (Status / Error / Verify)​

Every SCRIPT transaction is answered. The response spine reads the three acknowledgment transactions and exposes the disposition without ever reading an Error as a success.

import { parseScript, status, error, verify } from "@cosyte/ncpdp/script";

const responseXml = `<Message version="2017071">
<Header><RelatesToMessageID>SYNTH-MSG-0002</RelatesToMessageID></Header>
<Body><Error><Code>900</Code><Description>Prescriber identifier could not be validated.</Description></Error></Body>
</Message>`;

const msg = parseScript(responseXml);

msg.disposition; // "success" (Status) | "error" (Error) | "verify" (Verify) | undefined
msg.correlatesTo; // "SYNTH-MSG-0002": the answered request's MessageID (<RelatesToMessageID>)

error(msg)?.code; // "900": the Error code, verbatim; never reformatted or looked up
status(msg)?.description; // the positive-ack description, verbatim (undefined on an Error)
verify(msg)?.code;
  • An Error never reads as success. disposition is derived only from the response body kind, so a failure cannot be coerced to "success": status(msg) is undefined on an Error. If a malformed message carries more than one response body, the most conservative disposition (Error first) wins and NCPDP_SCRIPT_RESPONSE_AMBIGUOUS_DISPOSITION is raised.
  • correlatesTo ties the answer to its request. It reads <RelatesToMessageID> verbatim so you can match the acknowledgment back to the NewRx you sent.
  • Codes and descriptions are surfaced verbatim. <Code>, <DescriptionCode>, and <Description> are read as-is; the library bundles no NCPDP code→meaning table.

Read a Telecom PBM response (paid / rejected)​

The PBM answers a claim with a response transmission. parseTelecom detects the response shape automatically (it leads with the Version/Release, not the routing BIN); adjudication lifts the outcome. The same reader serves B1 billing, B2 reversal, B3 rebill, and E1 eligibility responses.

import { parseTelecom, adjudication } from "@cosyte/ncpdp/telecom";

const t = parseTelecom(rawResponse); // kind: "response" (detected, not configured)
const a = adjudication(t); // undefined for a request transmission

a?.status?.disposition; // "paid" | "rejected" | "captured" | "approved" | "duplicate" | "unknown"
a?.status?.rejectCodes; // every Reject Code (511-FB), verbatim, in wire order (none dropped)
a?.status?.statusConflict; // true when the status field claimed paid but a reject was present
a?.pricing?.patientPayAmount?.amount; // "10.00": implied 2-place decimal, string-wise (never a float)
a?.pricing?.totalAmountPaid?.amount; // "45.00"
a?.dur; // every returned DUR/PPS alert: one per occurrence, never collapsed
  • A reject always wins. disposition is a total function over the Transaction Response Status (112-AN) and the reject codes together. If any reject is present the disposition is "rejected" even when the status field claims paid. A consumer is never told a rejected claim was paid. The self-contradiction surfaces via NCPDP_TELECOM_STATUS_CONFLICT and status.statusConflict. An unrecognized status reads "unknown", never paid.
  • Reject codes are verbatim. Each is kept in wire order; an unrecognized code is preserved with known: false (NCPDP_TELECOM_UNKNOWN_REJECT_CODE) rather than dropped. No 511-FB label table ships, so today that is every reject code: you get the code, not a sentence about it. Which fields do have a label table, and on what source, is in KNOWN-LIMITATIONS.md.
  • Money is never a float. Every dollar amount carries an implied 2-place decimal (and an optional zoned-decimal overpunch sign); both are interpreted string-wise with the verbatim source kept, so binary floating point can never corrupt a paid amount. Anything unexpected is preserved with isValid: false and no interpreted amount. Money is never guessed.

Decode the structured SIG (lossy, labeled)​

A medication's directions can arrive as free text and as a structured <Sig>. The structured decode is best-effort and explicitly lossy: the free-text sigText stays the source of truth and is always preserved verbatim; the structured view is additive and every field is provenance-tagged.

import { parseScript, newRx } from "@cosyte/ncpdp/script";

const sig = newRx(parseScript(xml))?.medication?.sig;

sig?.sigText; // the free-text directions, verbatim (ALWAYS authoritative)
sig?.hasStructuredData; // false when the <Sig> carried only free text

sig?.route.provenance; // "coded" | "derived" | "absent"
sig?.route.code?.system; // "SNOMED" | "NCI" | … when coded
sig?.route.text; // verbatim text when present
sig?.dose.text; // the dose quantity, string-preserved (never a float, never guessed)
  • The free text is authoritative and never overwritten. When the structured dose and the free text disagree, both are surfaced as-is. The library never collapses the disagreement into one answer.
  • Per-field provenance. Every component (doseDeliveryMethod, dose, doseUnitOfMeasure, route, siteOfAdministration, administrationTiming, duration, vehicle, indication, maximumDoseRestriction) is tagged coded / derived / absent. An absent field is never inferred from the free text.
  • A component decodes only where its element name is grounded. doseUnitOfMeasure, duration, vehicle, indication and maximumDoseRestriction have no element name this release can trace to a published field label, so they always read absent whatever the message carried. See Structured SIG spec notes for the per-component table and the evidence behind each recognized name.
  • Ambiguous doses are never guessed. If a dose structure is present but no unambiguous quantity can be read, dose is surfaced as absent and NCPDP_SCRIPT_SIG_AMBIGUOUS_DOSE is raised. Whenever any structured component decodes, NCPDP_SCRIPT_SIG_STRUCTURED_LOSSY flags the additive, lossy view.
  • Decode-only. v1 does not generate a SIG from structure, and does not parse arbitrary natural-language directions. See Structured SIG spec notes.

Read a Telecom B1 claim​

The Telecommunication standard is the pharmacy-to-PBM claim protocol: a fixed positional Transaction Header followed by FS/GS/RS control-character-framed, field-id-keyed segments. parseTelecom decodes the header and segments; claim lifts the safety-relevant B1/B2/B3 request fields.

import { parseTelecom, claim } from "@cosyte/ncpdp/telecom";

const t = parseTelecom(raw); // raw: string | Buffer (latin1 by default)

t.header.transactionCode; // "B1"
t.warnings; // stable, byte-offset-positioned tolerance warnings, never throws on quirks

const c = claim(t); // the B1/B2/B3 request view, or undefined when no segments decoded

c?.product?.id; // Product/Service ID (e.g. the NDC), verbatim
c?.product?.qualifier; // Product/Service ID Qualifier (436-E1), verbatim: no label ships
c?.quantityDispensed?.source; // Quantity Dispensed, verbatim
c?.quantityDispensed?.impliedDecimal; // "30.000": implied 3-place decimal, applied string-wise
c?.daysSupply?.source; // decimal-safe, never a float
c?.prescriptionReferenceNumber; // the Rx reference, verbatim
c?.cardholderId; // PHI: synthetic only in fixtures
  • Quantity is never a float. Quantity Dispensed carries an implied 3-place decimal; it is scaled string-wise so binary floating point can never corrupt the value, and the verbatim source is kept.
  • Versions are not guessed. Which Telecom version this package decodes, which stamp it recognizes without decoding, and the dates those adoptions end are stated once in the Conformance statement and are deliberately not restated here. In a request, a stamp that is recognized but not decoded is surfaced via NCPDP_TELECOM_VF6_NOT_DECODED; a stamp the reader does not recognize where it looked is refused with NCPDP_TELECOM_UNSUPPORTED_VERSION, and that includes the same stamp arriving on a response. A non-empty body with no framing bytes is NCPDP_TELECOM_INVALID_FRAMING. A separator is never guessed.
  • Nothing is dropped. Unknown segments/fields, a missing AM, and malformed tokens are preserved verbatim and warned. Every group-separated transaction is decoded: read them at t.transactions[n] or pass the index to a view (claim(t, 1)). See Telecom spec notes.

Convert a date without inventing a timezone​

Parsed date fields are verbatim strings and stay that way. dateValue decodes one into a value; toObject, toISO and toDate read that value. The three names are the same in every @cosyte/* parser, so a consumer importing two of them aliases (toISO as ncpdpToISO) or namespaces them.

import { dateValue, toObject, toISO, toDate } from "@cosyte/ncpdp/common";

const dob = dateValue("19850722"); // Date of Birth (304-C4), CCYYMMDD

toObject(dob); // => { year: 1985, month: 7, day: 22 }
toISO(dob); // => "1985-07-22"

toDate(dob); // => undefined
toDate(dob, { assumeOffsetMinutes: 0 })?.toISOString(); // => "1985-07-22T00:00:00.000Z"
toDate(dob, { assumeOffsetMinutes: -300 })?.toISOString(); // => "1985-07-22T05:00:00.000Z"

dateValue("19880732"); // => undefined
dateValue("1985-07-22"); // => undefined

// A day the calendar does not have is refused on the value route too, not just the wire route.
const feb30 = { source: "20240230", year: 2024, month: 2, day: 30 };

toISO(feb30); // => undefined
toDate(feb30, { assumeOffsetMinutes: 0 }); // => undefined
  • toDate returns undefined unless you supply the zone. No form decoded here carries a UTC offset, so the zone is never determinate on its own: the host machine's timezone is never read and UTC is never assumed. Pass assumeOffsetMinutes (an explicit 0 means "treat this naive value as UTC") or get nothing back. A date of birth resolved to a guessed zone lands on the previous day in every negative-offset zone, silently.
  • One wire form is decoded, CCYYMMDD, the only date form this package declares: Date of Service (401-D1) and Date of Birth (304-C4). Other date-bearing fields (SCRIPT <SentTime>, <DateOfBirth>, <WrittenDate>; Telecom 443-E8 and 530-FU) are carried verbatim with no form stated, so dateValue answers undefined for them rather than guessing one. The fields themselves are unchanged and still readable.
  • A day the calendar does not have is refused, never rolled over. DateValue is an exported interface, so a value you build or spread yourself reaches the conversions without passing dateValue; it is bound by the same 4/100/400 leap rule, so both routes answer alike for the same eight digits. toISO never renders 2024-02-30, which every ISO-8601 reader reads back as 1 March, and toDate never returns a day in the following month.
  • Nothing is rewritten and nothing is zero-filled. toObject reports only the components the value stated, toISO truncates to that precision and appends no Z, and the parsed model still carries the wire string. See the Telecom spec notes for the header layout that positions Date of Service.

Next​

  • Compound, COB and DUR depth: every compound ingredient, every other-payer money row, the submitted DUR interactions, and prior-authorization presence.
  • Serializers and builders: turning a model back into spec-clean wire form, and the inputs emit refuses outright.
  • Trading-partner profiles: attaching a partner's conventions so you alert only on the warnings you did not expect.
  • Troubleshooting and known limitations: every diagnostic code these recipes can raise, and what v1 does not do.
  • Read the API reference for every export, generated from source.
  • The README covers the lifecycle transactions (renewal / change / cancel) as well.