Skip to main content
Version: v0.1.14

Read and build an 820 premium payment

A plan sponsor pays its group premium and sends an 820 to say what the money covers: one payment header, the trace that ties it to the bank deposit, the two parties, and a remittance per group or member with the policy each amount was paid against. You need to post each amount to the right policy, and to know whether the parts add up to the deposit. Or you are the sponsor, and need to send one.

get820Payments(delimiters, tx) reads the 005010X218 guide and returns an X12PremiumPayments, or undefined for a transaction set that is not an 820. build820(spec) writes one.

Read a premium payment​

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

const raw820 = [
"ISA*00* *00* *ZZ*EMPLOYERCO *ZZ*MEDPAY *260601*1200*^*00501*000000001*0*P*:~",
"GS*RA*EMPLOYERCO*MEDPAY*20260601*1200*1*X*005010X218~",
"ST*820*0001*005010X218~",
"BPR*I*375.00*C*ACH*CCP***********20260601~",
"TRN*1*PREM-202606*1512345678~",
"N1*PE*MEDPAY INSURANCE*FI*111223333~",
"N1*RM*EMPLOYER CO*FI*444556666~",
"ENT*1*2J*EI*GRP-0001~",
"NM1*IL*1*DOE*JANE****ZZ*MBR0001~",
"RMR*AZ*POL-0001**250.00*250.00~",
"DTM*582*20260601~",
"NM1*IL*1*ROE*JOHN****ZZ*MBR0002~",
"RMR*AZ*POL-0002**125.00*150.00~",
"ADX*-25.00*52~",
"SE*13*0001~",
"GE*1*1~",
"IEA*1*000000001~",
].join("");

const ix820 = parseX12(raw820);
const tx820 = ix820.groups[0]?.transactions.find((t) => t.st.elements[1] === "820");
if (tx820 === undefined) throw new Error("no 820 in interchange");

const premium = get820Payments(ix820.delimiters, tx820);
premium?.warnings.length; // => 0

// The payment header: BPR-02 is what the bank moved.
premium?.payment.totalPremiumAmount?.toString(); // => "375.00"
premium?.payment.method; // => "ACH"
premium?.payment.paymentDate; // => "20260601"

// The trace a bank statement carries for the same deposit.
premium?.traces[0]?.referenceId; // => "PREM-202606"

// The parties, keyed on the N1 qualifier: PE receives, PR or RM remits.
premium?.receiver?.name; // => "MEDPAY INSURANCE"
premium?.remitter?.name; // => "EMPLOYER CO"

// One remittance per ENT or NM1 loop. An NM1 right after an ENT names the
// member of that same loop; any other NM1 opens a loop of its own.
premium?.remittances.length; // => 2
const first = premium?.remittances[0];
first?.entity?.idCode; // => "GRP-0001"
first?.individual?.lastName; // => "DOE"
first?.openItems[0]?.referenceId; // => "POL-0001"
first?.openItems[0]?.amountPaid?.toString(); // => "250.00"
first?.dates[0]?.value; // => "20260601"

const second = premium?.remittances[1];
second?.entity; // => undefined
second?.individual?.idCode; // => "MBR0002"
second?.openItems[0]?.amountPaid?.toString(); // => "125.00"
second?.openItems[0]?.amountDue?.toString(); // => "150.00"
second?.adjustments[0]?.amount.toString(); // => "-25.00"
second?.adjustments[0]?.reasonCode; // => "52"

Every amount is an X12Decimal, never a number. An amount element that is absent, empty or not a decimal reads undefined, never zero; one that is not a decimal also raises X12_UNPARSEABLE_DECIMAL. An RMR with neither a qualifier nor a reference id is not on the model, and if it stated an amount the reader raises X12_STATED_AMOUNT_DISCARDED at it. Decimal-exact money has the rest.

Reconcile the total yourself​

The 820 guide sets no balance equation between BPR-02 and the open items, so get820Payments does not compare them and build820 writes the amounts you give it as they are. That is a deliberate contrast with the 835, whose reader checks its balance. If your posting depends on the parts adding up to the deposit, check it before you post, with exact decimals:

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

const raw820 = [
"ISA*00* *00* *ZZ*EMPLOYERCO *ZZ*MEDPAY *260601*1200*^*00501*000000001*0*P*:~",
"GS*RA*EMPLOYERCO*MEDPAY*20260601*1200*1*X*005010X218~",
"ST*820*0001*005010X218~",
"BPR*I*375.00*C*ACH*CCP***********20260601~",
"TRN*1*PREM-202606*1512345678~",
"NM1*IL*1*DOE*JANE****ZZ*MBR0001~",
"RMR*AZ*POL-0001**250.00~",
"NM1*IL*1*ROE*JOHN****ZZ*MBR0002~",
"RMR*AZ*POL-0002**125.00~",
"SE*8*0001~",
"GE*1*1~",
"IEA*1*000000001~",
].join("");

const ix = parseX12(raw820);
const tx = ix.groups[0]?.transactions[0];
const premium = tx ? get820Payments(ix.delimiters, tx) : undefined;
if (premium === undefined) throw new Error("not an 820");

/** The sum of every open item's amount paid, or undefined when one did not decode. */
function sumPaid(p: NonNullable<typeof premium>): X12Decimal | undefined {
let total = X12Decimal.ZERO;
for (const remittance of p.remittances) {
for (const item of remittance.openItems) {
if (item.amountPaid === undefined) return undefined;
total = total.add(item.amountPaid);
}
}
return total;
}

const paid = sumPaid(premium);
const deposit = premium.payment.totalPremiumAmount;
paid?.toString(); // => "375.00"
paid !== undefined && deposit !== undefined && paid.equals(deposit); // => true

An amount that did not decode makes the sum unknown rather than smaller, so route that payment to a person instead of posting a partial total.

Build a premium payment and read it back​

build820(spec) fixes GS-01 to RA and GS-08 and ST-03 to 005010X218, counts the segments, and returns an X12Interchange. It never sends anything, opens a socket or touches the filesystem. Write it out with serializeX12.

import { build820, get820Payments, serializeX12, X12Decimal, type Build820Spec } from "@cosyte/x12";

const spec: Build820Spec = {
envelope: {
senderId: "EMPLOYERCO",
receiverId: "MEDPAY",
interchangeDate: "260601",
interchangeTime: "1200",
interchangeControlNumber: "000000001",
groupControlNumber: "1",
transactionSetControlNumber: "0001",
},
payment: {
transactionHandlingCode: "I",
totalPremiumAmount: X12Decimal.fromString("375.00")!,
creditDebitFlag: "C",
method: "ACH",
paymentFormatCode: "CCP",
paymentDate: "20260601",
},
traces: [{ traceTypeCode: "1", referenceId: "PREM-202606", originatingCompanyId: "1512345678" }],
receiver: {
entityIdentifierCode: "PE",
name: "MEDPAY INSURANCE",
idQualifier: "FI",
idCode: "111223333",
},
remitter: {
entityIdentifierCode: "RM",
name: "EMPLOYER CO",
idQualifier: "FI",
idCode: "444556666",
},
remittances: [
{
entity: {
assignedNumber: "1",
entityIdentifierCode: "2J",
idQualifier: "EI",
idCode: "GRP-0001",
},
individual: {
entityIdentifierCode: "IL",
lastName: "DOE",
firstName: "JANE",
idQualifier: "ZZ",
idCode: "MBR0001",
},
openItems: [
{
qualifier: "AZ",
referenceId: "POL-0001",
amountPaid: X12Decimal.fromString("250.00")!,
amountDue: X12Decimal.fromString("250.00")!,
},
],
},
{
individual: {
entityIdentifierCode: "IL",
lastName: "ROE",
firstName: "JOHN",
idQualifier: "ZZ",
idCode: "MBR0002",
},
openItems: [
{
qualifier: "AZ",
referenceId: "POL-0002",
amountPaid: X12Decimal.fromString("125.00")!,
amountDue: X12Decimal.fromString("150.00")!,
},
],
adjustments: [{ amount: X12Decimal.fromString("-25.00")!, reasonCode: "52" }],
},
],
};

const built = build820(spec);
built.groups[0]?.gs.elements[1]; // => "RA"
built.groups[0]?.transactions[0]?.st.elements[3]; // => "005010X218"

const builtTx = built.groups[0]?.transactions[0];
const readBack = builtTx ? get820Payments(built.delimiters, builtTx) : undefined;
readBack?.warnings.length; // => 0
readBack?.payment.totalPremiumAmount?.toString(); // => "375.00"
readBack?.remittances[1]?.openItems[0]?.amountDue?.toString(); // => "150.00"
readBack?.remittances[1]?.adjustments[0]?.amount.toString(); // => "-25.00"

serializeX12(built).includes("RMR*AZ*POL-0001**250.00*250.00~"); // => true

What it refuses, each a Premium820BuildError with the code X12_820_BUILD_INVALID_SPEC: no trace, no remittance, a remittance with neither an entity nor an individual to open its loop, a remittance with no open item, and an open item whose qualifier and reference id are both empty, since the reader would drop it. The message names the position in the spec, never a member's name or id.

import { build820, Premium820BuildError, X12Decimal } from "@cosyte/x12";

let code: string | undefined;
try {
build820({
envelope: {
senderId: "EMPLOYERCO",
receiverId: "MEDPAY",
interchangeDate: "260601",
interchangeTime: "1200",
interchangeControlNumber: "000000001",
groupControlNumber: "1",
transactionSetControlNumber: "0001",
},
payment: {
transactionHandlingCode: "I",
totalPremiumAmount: X12Decimal.fromString("250.00")!,
creditDebitFlag: "C",
method: "ACH",
paymentDate: "20260601",
},
traces: [{ traceTypeCode: "1", referenceId: "PREM-202606" }],
// A remittance with no open item: nothing to post, so nothing is written.
remittances: [
{
individual: { entityIdentifierCode: "IL", idQualifier: "ZZ", idCode: "MBR0001" },
openItems: [],
},
],
});
} catch (err) {
if (err instanceof Premium820BuildError) code = err.code;
}
code; // => "X12_820_BUILD_INVALID_SPEC"