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 Java
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.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.