@cosyte/x12

中文文档
0.1.10发布于 2026/11/10MITTypeScriptESM + CJS零依赖构建来源已验证近 30 天下载 9282

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

原始使用文档
文档目录14 节
  1. Why this exists
  2. Status
  3. Install
  4. Usage
  5. API
  6. Dates and times
  7. Compatibility
  8. Documentation
  9. Support
  10. Security
  11. PHI and safety
  12. Contributing
  13. Trademarks
  14. License
The Cosyte logo on its own white ground: the icon beside the word Cosyte.

@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

The 0.1 line, published on npm from a public repository.

On the 0.1 line 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 an older 0.0 release, 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 May 26, 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.js 22 or later, tested on 22, 24, and 26: the engine floor is >=22.0.0, 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. The optional @cosyte/x12/arbitraries entry point, fast-check generators for property-testing your own code, needs fast-check installed beside it; the main entry point never loads it.

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.

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).
  • 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 how-to guides are in docs-content/, 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.

The date-aware code-list queries, checkCarcValidity and checkRarcValidity, keep their own contract and still THROW an X12CodeListError on a document date they refuse. That divergence is deliberate: they answer 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.

Documentation

The full documentation lives at docs.cosyte.com/x12, built from this repository's docs-content/. Start with these pages:

  • Quickstart: read an 835 and pull out the payment, each claim's money, and the adjustment reasons.
  • The 80/20 transaction sets: every transaction set this package covers, with its reader and its builder.
  • Decimal-exact money: why every amount is an X12Decimal and never a JavaScript number.
  • Build and serialize X12: the emit side, from serializeX12 to the domain builders.
  • Post an 835 remittance: the first of the how-to guides, one task each, for the transactions you get handed.
  • Troubleshooting: the error model, the common symptoms, and the boundaries this package states on purpose.

Support

Questions and ideas go to Discussions, which covers every Cosyte repository. Bug reports and trading-partner quirks go to the issue tracker, with a synthetic reproduction and never a real interchange. The organization's support guide has the rest.

Need an X12 integration built, or hosted and run for you? See our integration services.

Security

Report a vulnerability privately, as the security policy describes, never in a public issue.

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.

Contributing

Bug reports and trading-partner quirks belong in GitHub issues, as Support says above.

Pull requests are welcome. The organization's contributing guide and code of conduct apply here. 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 (c) 2026 Cosyte LLC. See LICENSE.

Built by Cosyte.

版本与发布记录

@cosyte/x12 共发布 29 个版本,首次发布于 2026/07/17;过去 12 个月发布了 29 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.1.10latest2026/11/10今天
0.1.92026/11/092 天前
0.1.82026/11/092 天前
0.1.72026/11/082 天前
0.1.62026/11/082 天前
0.1.52026/11/082 天前

关于 @cosyte/x12 的常见问题

@cosyte/x12 是什么?

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

如何安装 @cosyte/x12?

在项目目录运行 npm install @cosyte/x12,或使用 pnpm add / yarn add / bun add。

@cosyte/x12 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cosyte/x12 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@cosyte/x12 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@cosyte/x12 可以商用吗?

@cosyte/x12 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@cosyte/x12 的下载量有多少?

根据 npm 官方下载统计,@cosyte/x12 在 2026/10/11 – 2026/11/09 这 30 天内被下载 9,282 次,日均约 309 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cosyte/x12 还在维护吗?

最新版本 0.1.10 发布于 2026/11/10(今天);至今共发布 29 个版本,其中过去 12 个月发布了 29 个;首次发布于 2026/07/17。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。