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~ (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], }; } 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)); } 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~ Read it as:
-
SDQ01 = EA: unit of measure -
SDQ02 = 92: location qualifier, assigned by the buyer - then pairs of
location, quantity: store0101gets 25,0102gets 35,0103gets 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; } 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 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
ISAenvelopes or multipleSTsets in one file -
SDQsegments with more than 10 location pairs, which retailers split across severalSDQsegments in the samePO1loop - repeated
PO1identifiers with unusual qualifiers - validation of
SE01andCTT01against 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.

