npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cosyte/x12

v0.1.0

Published

Developer-focused ASC X12 healthcare EDI parser, serializer, and builder for Node.js and TypeScript: lenient on parse, spec-clean on emit.

Readme

@cosyte/x12

Read the money out of an 835, an 837 or a 271 without a TR3 open on your desk.

npm version CI License: MIT Node

Developer-focused ASC X12 healthcare EDI parser, serializer, and builder for Node.js and TypeScript: lenient on parse, spec-clean on emit.

Why this exists

Reading an X12 healthcare transaction correctly normally means buying the TR3 implementation guide for it and then hand-mapping element positions that no compiler will ever check. This library removes that step for the engineer who has one 835 to post, one 271 to answer or one 837 to send, and who needs the amounts to be exactly right rather than approximately right. The nearest alternatives are a general-purpose EDI translator, which frames segments correctly but knows nothing about what CLP-04 means, and a hand-rolled split on the segment terminator, which works until the first trading partner sends a repetition separator you did not plan for. This is neither: a zero-dependency TypeScript library that decodes the healthcare transaction sets into typed models, keeps every amount in exact decimal arithmetic, and reports each vendor deviation it tolerated with a stable code instead of failing or guessing.

Status

Version 0.1.0, published on npm from a public repository.

From 0.1.0 the public API is settled and safe to depend on: the exported readers, builders, types and warning codes are the surface we keep stable, and the warning registry only ever gains codes. Below 1.0 a breaking change bumps the minor version and is called out in the changelog with its migration. On a 0.0.x version, pin an exact version rather than a range. Typed read and typed emit both ship for every transaction set this package covers: 270, 271, 276, 277 and 277CA, 278 request and response, 820, 834, 835, 837P, 837I and 837D, 999 and TA1, each with a reader and a matching domain builder, and the two claims attachments guides 45 CFR 162.2002 adopts for the period on and after 26 May 2028: the 277 request for additional information (006020X313) and the 275 that answers it (006020X314). For those two the base X12 006020 structure is typed and no implementation-guide usage is checked, so a qualifier or segment the guide requires is carried as sent and never validated; KNOWN-LIMITATIONS.md lists what else they leave open. X12_TR3_CONFORMANCE is the machine-readable answer to which implementation guide each one follows, and is a better source than this page.

Not covered, and deliberately so: a byte-exact round trip is not guaranteed in general (see Compatibility), and non-healthcare transaction sets, EDIFACT, transport such as AS2 and SFTP, pre-005010 revisions, 006020 guides other than the two attachments guides, and decoding what an attachment carries are all out of scope.

Install

pnpm add @cosyte/x12

npm install @cosyte/x12 and yarn add @cosyte/x12 work the same way.

Node >=22.0.0 is the engine floor, and there are zero runtime dependencies. The package ships dual ESM and CJS builds with type declarations for both conditions, so import and require each resolve to the artifact meant for them.

Usage

Parse an 835 remittance advice and read the money: the payment total, the claim's charge/paid/patient-responsibility split, and the adjustment reason behind the difference. The interchange below is synthetic, with a fabricated payer, provider, patient and amounts.

import { parseX12, get835 } from "@cosyte/x12";

const raw = `ISA*00*          *00*          *ZZ*MEDICARE       *ZZ*SUBMITTER      *260601*1200*^*00501*000000001*0*P*:~
GS*HP*MEDICARE*SUBMITTER*20260601*1200*1*X*005010X221A1~
ST*835*0001~
BPR*I*450.00*C*ACH*CCP*01*123456789*DA*987654321*1512345678**01*111111111*DA*222222222*20260601~
TRN*1*0012345*1512345678~
DTM*405*20260601~
N1*PR*MEDICARE PART A~
N3*123 PAYER WAY~
N4*BALTIMORE*MD*21244~
PER*BL*JANE COORDINATOR*TE*5551234567~
N1*PE*SAMPLE CLINIC INC~
N3*456 PROVIDER LN~
N4*CLEVELAND*OH*44113~
REF*TJ*123456789~
LX*1~
CLP*PT-ACCT-001*1*500.00*450.00*50.00*MC*PAYER-CLAIM-001*11*1~
NM1*QC*1*PATIENT*TEST*A***MI*MEMBER001~
NM1*82*2*RENDERING PROVIDER INC*****XX*1234567890~
DTM*232*20260501~
DTM*233*20260501~
SVC*HC:99213*500.00*450.00**1~
DTM*472*20260501~
CAS*PR*1*50.00~
REF*6R*LINE-CTRL-001~
SE*23*0001~
GE*1*1~
IEA*1*000000001~`;

const ix = parseX12(raw);
const tx = ix.groups[0]?.transactions.find((t) => t.st.elements[1] === "835");
if (tx === undefined) throw new Error("no 835 in this interchange");
const remit = get835(ix.delimiters, tx);
if (remit === undefined) throw new Error("not an 835");

// The payment, the claim's split, and who owes the difference and why.
const claim = remit.claims[0];
const adjustment = claim?.serviceLines[0]?.adjustments[0];
console.log("paid", remit.payment.totalActualPayment?.toString(), "by", remit.payment.method);
console.log("claim", claim?.patientControlNumber, "charged", claim?.totalChargeAmount?.toString());
console.log("patient owes", claim?.patientResponsibilityAmount?.toString());
console.log("reason", adjustment?.groupCode, adjustment?.reasonCode, adjustment?.reasonDescription);
console.log("warnings", ix.warnings.length);

// The values printed above, which the test suite asserts on every run:
remit.payment.totalActualPayment?.toString(); // => "450.00"
claim?.totalChargeAmount?.toString(); // => "500.00"
claim?.patientResponsibilityAmount?.toString(); // => "50.00"
adjustment?.groupCode; // => "PR"
adjustment?.reasonCode; // => "1"
adjustment?.reasonDescription; // => "Deductible Amount"
ix.warnings; // => []

It prints:

paid 450.00 by ACH
claim PT-ACCT-001 charged 500.00
patient owes 50.00
reason PR 1 Deductible Amount
warnings 0

The groupCode is what tells you who owes the money (PR patient responsibility, CO contractual obligation, and so on). Read it; never infer it.

PHI and safety

X12 healthcare transactions carry protected health information, so treat every interchange you hand this library as PHI.

What it does with your document. It decodes it in memory and nothing else. No socket is opened, nothing is written to disk, and nothing is fetched at run time: the bundled code lists are versioned data snapshots, so updating one is a release rather than a network call. The acknowledgment builders are pure functions and never auto-send.

What it keeps out of diagnostics. A warning message is a lookup into a frozen registry rather than something built from your document, and no warning factory in this library takes a value parameter, so no element of yours can reach a diagnostic. The code and the position say what and where; the bytes stay on the model. ALL_WARNING_MESSAGES is exported so you can assert that.

The one deliberate exception. X12ParseError.snippet on the four structural fatals is a bounded copy of the start of the input, so on real traffic it can carry PHI, and it is not redacted. Log err.code and err.position instead, or redact at your call site.

An attachment refuses to print. The data of each BDS a 275 carries is read into X12AttachmentData, whose string, JSON and util.inspect forms state its octet count and nothing else; readOctets() is the one route to the octets.

Builder refusals are a weaker surface, deliberately. A build* refusal names the control number, count or code you passed in, so that you can see what was refused. The rendered fragment is bounded by an exported constant, but it is bounded rather than redacted. The two attachments builders are the exception and name no value at all. Log err.code, not err.message, from a builder.

What you still own. Transport, storage, retention, access control, audit logging, and every log line your own code writes. This library makes no HIPAA compliance claim on your behalf.

API

Everything ships from one entry point; there are no subpath imports.

  • Read. parseX12 decodes an interchange into a model, and a per-transaction reader turns a transaction set into a typed one (get835, get837Claims, get270Inquiry, get271Eligibility, get276StatusInquiry, parse999, parseTA1, and the rest).
  • Emit. serializeX12 and buildInterchange are the general path, and every transaction set with a reader also has a domain builder (build835, build837P, build271, and the rest) that layers that guide's own invariants on top.
  • Errors and warnings. Parsing is lenient, and only four structural failures are ever fatal (X12_NO_ISA_HEADER, X12_ISA_TOO_SHORT, X12_INVALID_DELIMITERS, X12_EMPTY_INPUT). Everything else arrives on ix.warnings as a stable code with positional context, so a tolerated deviation is visible rather than silent.
  • Money. Every monetary, percent and quantity field decodes as X12Decimal, which is string-backed with BigInt arithmetic, and this library never parseFloats an EDI amount. A slot the sender left empty reads undefined rather than zero, so "stated zero" and "stated nothing" stay different readings.
  • Conformance. X12_TR3_CONFORMANCE states which implementation guide each transaction set follows and how that identifier stands against the federal incorporation by reference.
  • CORE Code Combinations. checkCoreCodeCombination answers whether an 835 adjustment's group, reason and remark codes are in a CORE Code Combinations table for a CORE-defined business scenario you name. You supply the table and name its version, no CORE table ships with this package, and an unevaluated answer is never permission to post.

Task-oriented recipes are in the cookbook, and KNOWN-LIMITATIONS.md is the honest do-not-over-trust list.

Dates and times

Every typed reader surfaces its dates verbatim, as the qualifier the sender used plus the bytes it carried. toObject, toISO and toDate are the three ways to read one, and every @cosyte/* parser exports the same three names with the same shapes and the same timezone rule.

import { toObject, toISO, toDate } from "@cosyte/x12";

// A DTP or DTM date off any typed reader: claim.dates[0], eligibility.dates[0], and so on.
const day = { formatQualifier: "D8", value: "20260601" };

toObject(day); // { year: 2026, month: 6, day: 1 }
toISO(day); // "2026-06-01"

// A calendar day is not an instant. Without a stated offset there is no answer.
toDate(day); // undefined
toDate(day, { assumeOffsetMinutes: 0 })?.toISOString(); // "2026-06-01T00:00:00.000Z"
toDate(day, { assumeOffsetMinutes: -300 })?.toISOString(); // "2026-06-01T05:00:00.000Z"

// A range converts to nothing, because an interval is not a point in time.
const span = { formatQualifier: "RD8", value: "20260601-20260605" };
toISO(span); // undefined

The decoded format qualifier set is D8 and RD8, and nothing else. Those are the two DTP-02 values this library already parses and builds: D8 is a single CCYYMMDD day, and RD8 is a CCYYMMDD-CCYYMMDD range. A qualifier outside that pair, an absent one, a value whose digits do not match the shape its qualifier declares, and a day that is not on the calendar all convert to undefined. None of the three ever throws, so a lenient parse stays lenient all the way to the value you read.

RD8 converts to undefined from all three, deliberately. An interval is not a point in time, so there is no single day to hand back for a service period or an eligibility span. Handing back the first endpoint would be a quiet wrong answer with a right-looking shape. Split the range yourself and convert whichever end you meant.

toDate returns an instant only when you state the zone. No X12 date element this library decodes carries a UTC offset, so assumeOffsetMinutes (signed minutes east of UTC) is the only way one reaches the result. Without it the answer is undefined: the host machine's timezone is never read and UTC is never assumed, because the same midnight is two different instants either side of a border and a date of birth read in the wrong zone is a day out.

toObject returns only the components the value stated, with month spec-native 1 to 12 rather than the JavaScript Date 0 to 11, so Object.keys() of the result tells you the precision. Delete offsetMinutes and what is left is accepted by Temporal.PlainDateTime.from and by luxon's DateTime.fromObject with no renaming. Neither library is a dependency here; this package still has zero of those.

parseDocumentDate, which reads the document date for the date-aware code-list queries, keeps its own contract and still THROWS on input it refuses. That divergence is deliberate: it answers a caller who asked a validity question and could not state the day, while these three read a document that was already parsed leniently.

Using two @cosyte parsers in one file

The three names are identical in every @cosyte/* parser, so importing two of them means aliasing:

import { toISO as x12ToISO } from "@cosyte/x12";
import { toISO as hl7ToISO } from "@cosyte/hl7";

x12ToISO({ formatQualifier: "D8", value: "20260601" });
hl7ToISO(someParsedHl7Timestamp);

A namespace import works too: import * as x12 from "@cosyte/x12", then x12.toISO(day).

Compatibility

  • 005010 HIPAA transaction sets, with hooks for the errata revisions the industry actually exchanges. Non-healthcare sets such as 850, 856, 810 and 204, plus EDIFACT, AS2 and SFTP transport, and pre-005010 revisions, are out of scope.
  • Vendor deviations become warnings, not exceptions. Built-in profiles record whose companion-guide deviation they accommodate; selecting one never changes an otherwise correct parse, and profiles are authored through the same public defineProfile() API you have.
  • Round trips are not byte-exact in general. serializeX12 reproduces the segments on the model verbatim, including element padding, composites and ?-release escapes, but serialize(parse(s)) === s is not guaranteed in general. Line breaks between segments are the common case: any run of CR or LF between segments is absorbed at parse, so a pretty-printed file emits compact. Most of the constructs that break a round trip are silent, so "my file has no line breaks" is not sufficient either. KNOWN-LIMITATIONS.md is the canonical list.

Contributing

Questions, bug reports and trading-partner quirks all belong in GitHub issues.

Pull requests are welcome. A contribution must clear pnpm typecheck, pnpm lint and pnpm test, hold the per-directory coverage floors, pass the PHI commit gate (pnpm phi-scan) and the prose gates (pnpm check:no-emdash, pnpm check:no-internal-refs), and carry a changeset. Every fixture must be synthetic: never open a pull request carrying a real patient's data.

Trademarks

Availity and Blue Cross Blue Shield are trademarks of their respective owners. cosyte is not affiliated with, endorsed by, or sponsored by any of them. The names identify the trading partners whose companion-guide deviations the built-in profiles accommodate. See TRADEMARKS.md.

License

MIT, copyright Cosyte. See LICENSE.

Built by Cosyte.