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
warningswith an XPath position, not failures. Only unrecoverable structural corruption throws a typedNcpdpScriptParseError: 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.systemis recognized from the wire qualifier ("NDC","RXNORM","SNOMED", …); the rawvalueis always kept verbatim. - A warning is a
codeplus an XPathposition, 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.warningswhole; 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
Errornever reads as success.dispositionis derived only from the response body kind, so a failure cannot be coerced to"success":status(msg)isundefinedon an Error. If a malformed message carries more than one response body, the most conservative disposition (Error first) wins andNCPDP_SCRIPT_RESPONSE_AMBIGUOUS_DISPOSITIONis raised. correlatesToties 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.
dispositionis 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 viaNCPDP_TELECOM_STATUS_CONFLICTandstatus.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 inKNOWN-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
sourcekept, so binary floating point can never corrupt a paid amount. Anything unexpected is preserved withisValid: falseand 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 taggedcoded/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,indicationandmaximumDoseRestrictionhave no element name this release can trace to a published field label, so they always readabsentwhatever 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,
doseis surfaced asabsentandNCPDP_SCRIPT_SIG_AMBIGUOUS_DOSEis raised. Whenever any structured component decodes,NCPDP_SCRIPT_SIG_STRUCTURED_LOSSYflags 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
sourceis 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 withNCPDP_TELECOM_UNSUPPORTED_VERSION, and that includes the same stamp arriving on a response. A non-empty body with no framing bytes isNCPDP_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 att.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
toDatereturnsundefinedunless 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. PassassumeOffsetMinutes(an explicit0means "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, sodateValueanswersundefinedfor 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.
DateValueis an exported interface, so a value you build or spread yourself reaches the conversions without passingdateValue; it is bound by the same 4/100/400 leap rule, so both routes answer alike for the same eight digits.toISOnever renders2024-02-30, which every ISO-8601 reader reads back as 1 March, andtoDatenever returns a day in the following month. - Nothing is rewritten and nothing is zero-filled.
toObjectreports only the components the value stated,toISOtruncates to that precision and appends noZ, 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.