DEV Community

Jing Zhang
Jing Zhang

Posted on Fully Autonomous

How to Parse ANSI X12 EDI 850 Purchase Orders and Split Cross-Dock SDQ Segments in JavaScript

If you sell to big retailers, sooner or later a purchase order arrives as a single line of text with * and ~ everywhere. This post walks through an ANSI X12 850 purchase order segment by segment, then builds a small JavaScript parser that detects the delimiters, reads the order, and expands the cross-dock SDQ segment into one flat CSV row per store.

The sample used here is a synthetic Walmart-style cross-dock PO from an open sample set: ansi-x12-edi-sample-files.

Anatomy of an 850

ISA*00*          *00*          *08*925485US00     *12*5551234567     *261109*0830*^*00501*000010850*0*P*:~
GS*PO*925485US00*5551234567*20261109*0830*10850*X*005010~
ST*850*0001~
BEG*00*SA*4500991288**20261109~
REF*DP*0018~
N1*ST*WALMART DC 6094G-RDC*UL*0078742035260~
PO1*1*100*EA*12.50*WE*UP*012345678905*IN*554433221*VN*SKU-TOWEL-BLU-L*UK*10012345678902~
PO4*10*1*EA~
SDQ*EA*92*0101*25*0102*35*0103*40~
PO1*2*60*EA*18.00*WE*UP*012345678912*IN*554433299*VN*SKU-ROBE-WHT-XL~
SDQ*EA*92*0101*20*0102*20*0103*20~
CTT*2*160~
SE*27*0001~
GE*1*10850~
IEA*1*000010850~
Enter fullscreen mode Exit fullscreen mode

(Trimmed for readability; the full file is in the repo.)

Segment What it carries
ISA / IEA Interchange envelope: sender/receiver IDs, date, control number, and the delimiters
GS / GE Functional group. PO means purchase order group
ST / SE One transaction set. SE01 is the segment count, so it must match
BEG Beginning of PO. BEG03 is the PO number (4500991288)
REF*DP Department number
N1*ST Ship-to party, here a distribution center
PO1 One line item: quantity (PO102), unit, unit price, then pairs of ID qualifiers and values
SDQ Destination quantity: how the line quantity is split across locations
CTT Transaction totals: line count and hash total

In PO1, everything after the price is qualifier, value pairs. UP is the UPC, VN is the vendor's SKU, IN is the buyer's item number.

Step 1: Sniff the delimiters from the ISA header

X12 does not fix the separators. Most files use * between elements and ~ between segments, but you should not assume that. The trick is that the ISA segment is fixed width: exactly 106 characters. That gives you the delimiters at known positions:

  • index 3 is the element separator (the character right after ISA)
  • index 104 is the component separator (ISA16)
  • index 105 is the segment terminator
// Drop a UTF-8 byte order mark if the file starts with one.
const stripBom = (s) => (s.charCodeAt(0) === 0xfeff ? s.slice(1) : s);

export function sniffDelimiters(raw) {
  const text = stripBom(raw).trimStart();
  if (!text.startsWith("ISA") || text.length < 106) {
    throw new Error("Not an X12 interchange: ISA header must be 106 characters");
  }
  return {
    element: text[3],
    component: text[104],
    segment: text[105],
  };
}
Enter fullscreen mode Exit fullscreen mode

Two real-world gotchas: a UTF-8 BOM at the start of the file, and files where each segment is followed by a newline. The newline is not part of the terminator, so strip it before splitting.

Step 2: Split into segments

export function parseSegments(raw) {
  const d = sniffDelimiters(raw);
  return stripBom(raw)
    .trim()
    .split(d.segment)
    .map((s) => s.replace(/[\r\n]/g, "").trim())
    .filter(Boolean)
    .map((s) => s.split(d.element));
}
Enter fullscreen mode Exit fullscreen mode

Each segment is now an array where seg[0] is the tag. Note that seg[1] is element 01, so PO102 is seg[2].

Step 3: Expand SDQ into one row per store

SDQ is what makes cross-dock orders awkward in a spreadsheet. A single PO1 line says "100 towels", and the SDQ right after it says how many go to each store:

PO1*1*100*EA*12.50*...
SDQ*EA*92*0101*25*0102*35*0103*40~
Enter fullscreen mode Exit fullscreen mode

Read it as:

  • SDQ01 = EA: unit of measure
  • SDQ02 = 92: location qualifier, assigned by the buyer
  • then pairs of location, quantity: store 0101 gets 25, 0102 gets 35, 0103 gets 40

The quantities must add up to PO102: 25 + 35 + 40 = 100. If they do not, that is worth failing loudly on, because shipping against a bad split is how you end up with a chargeback.

export function flattenCrossDock(segments) {
  const rows = [];
  const poNumber = segments.find((s) => s[0] === "BEG")?.[3];
  let line = null;

  for (const seg of segments) {
    if (seg[0] === "PO1") {
      line = { po: poNumber, lineNo: seg[1], qty: Number(seg[2]), ids: {} };
      for (let i = 6; i + 1 < seg.length; i += 2) line.ids[seg[i]] = seg[i + 1];
    } else if (seg[0] === "SDQ" && line) {
      let sum = 0;
      for (let i = 3; i + 1 < seg.length; i += 2) {
        const qty = Number(seg[i + 1]);
        sum += qty;
        rows.push({
          po: line.po, line: line.lineNo,
          sku: line.ids.VN ?? "", upc: line.ids.UP ?? "",
          store: seg[i], qty, uom: seg[1], locQualifier: seg[2],
        });
      }
      if (sum !== line.qty) {
        throw new Error(`PO1 line ${line.lineNo}: SDQ sums to ${sum} but PO102 is ${line.qty}`);
      }
    }
  }
  return rows;
}
Enter fullscreen mode Exit fullscreen mode

Running it on the sample file prints:

po,line,sku,upc,store,qty,uom,locQualifier
4500991288,1,SKU-TOWEL-BLU-L,012345678905,0101,25,EA,92
4500991288,1,SKU-TOWEL-BLU-L,012345678905,0102,35,EA,92
4500991288,1,SKU-TOWEL-BLU-L,012345678905,0103,40,EA,92
4500991288,2,SKU-ROBE-WHT-XL,012345678912,0101,20,EA,92
4500991288,2,SKU-ROBE-WHT-XL,012345678912,0102,20,EA,92
4500991288,2,SKU-ROBE-WHT-XL,012345678912,0103,20,EA,92
Enter fullscreen mode Exit fullscreen mode

Two lines became six rows, one per store, ready to paste into Excel or import elsewhere. One thing to remember when you open this in a spreadsheet: keep the UPC and store columns as text, or leading zeros will disappear (0101 becomes 101).

Things this sketch does not handle

This is a teaching parser, not a production one. It ignores:

  • multiple ISA envelopes or multiple ST sets in one file
  • SDQ segments with more than 10 location pairs, which retailers split across several SDQ segments in the same PO1 loop
  • repeated PO1 identifiers with unusual qualifiers
  • validation of SE01 and CTT01 against the actual counts

Try it without writing code

If you just want to look at a file, you can paste or drop it into this browser-based EDI viewer. It parses locally in the browser, so the file is not uploaded. There is also a reference page for the segment itself: SDQ destination quantity (cross-dock). I build that tool, so treat this as a disclosure as much as a link.

More sample files (850, 855, 856, 810, 820, 824, 846, 997) are in the GitHub repo if you want test fixtures for your own parser.

Top comments (0)