De-identify an NCPDP Telecom transaction
The @cosyte/deid/ncpdp adapter is the NCPDP Telecommunication (vD.0) binding of the
de-identification core. It locates PHI structurally in a parsed Telecom transaction (a value is the
patient's last name because it sits in field 311-CB, never because a string "looked like" a name),
applies the configured policy, and returns the de-identified Telecom byte stream plus the core's
value-free manifest.
@cosyte/ncpdpis an optional peer dependency. Install it alongside@cosyte/deidto use this subpath. The adapter reaches NCPDP data only through@cosyte/ncpdp's exported Telecom model (TelecomTransaction/TelecomSegment/TelecomField) and itsparseTelecom/serializeTelecomcodec.
npm install @cosyte/deid @cosyte/ncpdp
De-identify a transaction
Parse the transaction with @cosyte/ncpdp/telecom and hand it to deidentifyTelecom, or pass the raw
text to deidentifyTelecomString, which parses and de-identifies in one call. The billing request
below is synthetic; FS and RS are the field and segment separators.
import { parseTelecom } from "@cosyte/ncpdp/telecom";
import { deidentifyTelecom, deidentifyTelecomString } from "@cosyte/deid/ncpdp";
const FS = "\x1c";
const RS = "\x1e";
const raw = [
"999999D0B1PCN01 101PHARM123 20260115 AM01",
`${FS}CAZZPATFIRST${FS}CBZZPATLAST${FS}C419850302${FS}C5M${FS}COOH${FS}CP44101`,
`${RS}AM04${FS}C2ZZCARDHOLDER${FS}C1ZZGROUP01${FS}C301`,
`${RS}AM07${FS}D2RX0000001${FS}E103${FS}D700071015527${FS}E730000`,
].join("");
const { telecom, manifest } = deidentifyTelecom(parseTelecom(raw));
// or, parse + de-identify in one call:
deidentifyTelecomString(raw).telecom === telecom; // => true
telecom.includes("ZZPATLAST"); // => false
telecom.includes(`${FS}C41985${FS}`); // => true
telecom.includes(`${FS}CP441`); // => true
telecom.includes(`${FS}D700071015527${FS}`); // => true
manifest.map((entry) => entry.locus); // => ["header/dateOfService", "01/CA", "01/CB", "01/C4", "01/CP", "04/C2", "04/C1"]
The built-in Safe Harbor policy uses no keyed transform, so a Safe Harbor pass over a transaction
needs no context at all. Under a preset that keeps consistent keyed surrogates instead, a context
is required and calling without one when the transaction needs it is a fatal DEID_NO_KEY.
Choose a policy shows how to pass one.
What is located, and how it is transformed
Telecom is a flat sequence of segments of { id, value } fields. Field ids are globally unique in the
standard, so the map keys off the field id directly.
| Segment / locus | Handling |
|---|---|
Patient (01) | name (CA/CB) + phone (CQ) removed; street (CM) + city (CN) removed; ZIP (CP) → 3-digit; DOB (C4) → year (removed when it shows an age over 89); patient id (CY) removed under Safe Harbor; gender + state retained |
Insurance (04) | cardholder id (C2) + group id (C1) removed under Safe Harbor; cardholder name (CC/CD) removed; person code retained |
Prescriber (03) | prescriber id (DB) removed |
Coordination of Benefits (05) | other-payer cardholder (NU) + group (MJ) ids removed under Safe Harbor; other-payer date (E8) → year |
| Header | Date of Service → year |
Free text (544-FY, 504-F4, 526-FQ) | fails closed: blocked, never scrubbed by a naive pass |
Clinical / financial (07/08/10/11/12/13) | retained untouched: NDC drug codes, quantities, days-supply, pricing, DUR reason codes |
| Any unmapped / unknown segment | fails closed: every field blocked |
The X12 adapter retains provider identity, while this adapter removes the prescriber id: a deliberate asymmetry.
A field a Patient, Prescriber, Insurance or Coordination of Benefits segment retains (gender, state, relationship, place of service, the id qualifiers, person code, pregnancy indicator, residence, the coordination of benefits counts, coverage type, qualifiers and amounts, the other payer id and its processor control number) is kept only in its own format: a one- or two-character code, a count, a two-letter state, an amount, or for the other payer id and processor control number at most ten letters, digits, spaces or hyphens. A value in any other shape is blocked and recorded like an unlisted field.
NCPDP SCRIPT is refused
NCPDP SCRIPT (ePrescribing XML) de-identification is not shipped in this release. The
@cosyte/ncpdp SCRIPT surface cannot be de-identified faithfully through its public API:
serializeScript emits only the modeled fields (a parse → serialize round-trip drops any unmodeled
XML element), and the SCRIPT Patient model has no address, phone, or patient-id field. A partial
pass would silently drop content and leave unmodeled patient identifiers unhandled: a false-safety
hazard the fail-closed posture forbids. SCRIPT support waits for a parser surface that preserves the full
document.
That is a behavior, not a note. Hand a SCRIPT document to either entry point and it is refused, before anything is parsed:
import { deidentifyTelecomString } from "@cosyte/deid/ncpdp";
import { DeidError, FATAL_CODES } from "@cosyte/deid";
let refusal: unknown;
try {
// An XML document on an NCPDP surface is SCRIPT, and it is declined outright.
deidentifyTelecomString('<?xml version="1.0"?><Message><Body><NewRx/></Body></Message>');
} catch (err) {
refusal = err;
}
refusal instanceof DeidError && refusal.code === FATAL_CODES.DEID_FORMAT_UNSUPPORTED; // => true
// The message names SCRIPT and the parser-surface reason, and carries no document content.
refusal instanceof DeidError && refusal.message.includes("SCRIPT"); // => true
refusal instanceof DeidError && refusal.message.includes("NewRx"); // => false
The error is a typed DeidError carrying DEID_FORMAT_UNSUPPORTED, and its message names the format
and states the parser-surface reason. It carries no byte of the document, and the call returns no
transformed document, no manifest, and no partial output of any kind. A Telecom caller sees no
change: a parsed Telecom transaction is never a candidate for the refusal.
Known limitation
A retained clinical segment may carry a residual patient-related date the map does not surface (e.g. a previous date of fill): a documented limitation mirroring the HL7 and X12 adapters; forgetting one fails safe (retained, not leaked).
A Telecom message that carries more than one transaction is refused with a typed DeidError
carrying DEID_FORMAT_UNSUPPORTED, with no partial output. Split it and de-identify each transaction on
its own.
So is a transaction that carries more than one Patient (01) or Claim (07) segment. With no Group
Separator in front of it, a second patient's Patient and Claim segments decode as part of the one
transaction, and de-identifying them under the caller's one patient context would give two people one
date-shift offset. Send each patient's claim in its own transmission, with its own context.
Reference
deidentifyTelecom,deidentifyTelecomStringandTelecomDeidResultTELECOM_LOCUS_MAP,TELECOM_FREE_TEXT_FIELDSandTELECOM_RETAIN_SEGMENTS: the map this page describesNCPDP_SCRIPT_REFUSAL_MESSAGEandNCPDP_MULTI_TRANSACTION_REFUSAL_MESSAGEDeidErrorandFATAL_CODES