Electronic Data Interchange (EDI) has been the backbone of B2B commerce for decades. Yet most ecommerce teams treat it as a black box — something their 3PL or ERP "just handles." That mindset breaks the moment you onboard a new retail partner, scale past 500 daily orders, or try to automate your supply chain.
This guide walks you through the complete integration process: from understanding what EDI documents actually contain, to mapping them into Shopify, to avoiding the production-breaking mistakes that trip up 80% of first-time integrations. We'll use X12 850 (North America) and EDIFACT ORDERS (international) as our primary examples throughout.
EDI is a standardized electronic format for exchanging business documents — purchase orders, invoices, shipment notices, and more — between companies. It predates the internet and still processes trillions of dollars in commerce annually. The key standards you'll encounter:
| Standard | Region | PO Document | PO Acknowledgment | ASN / Ship Notice | Invoice |
|---|---|---|---|---|---|
| ANSI X12 | North America | 850 | 855 | 856 | 810 |
| EDIFACT | International / EU | ORDERS | ORDRSP | DESADV | INVOIC |
| TRADACOMS | UK Retail | ORDER | ORDHDR | DLCDET | INVFIL |
For Shopify integrations, you'll almost always be on the receiving end of an 850/ORDERS (a retailer sends you a PO) and the sending end of an 856/DESADV (you send them your shipment notice) and 810/INVOIC (you send them an invoice). Understanding this directionality is critical before you write a single line of code.
An 850 is a flat, delimiter-separated file. It looks nothing like JSON. Each line is a segment, and each segment contains fields separated by an asterisk (by default). Here's a simplified real-world example:
ISA*00* *00* *ZZ*RETAILER *ZZ*YOURCOMPANY *260501*0900*^*00501*000000001*0*P*: GS*PO*RETAILER*YOURCOMPANY*20260501*0900*1*X*005010 ST*850*0001 BEG*00*SA*PO-98765**20260501 ~ Purchase order number + date REF*DP*DEPT-42 ~ Department number DTM*002*20260515 ~ Required delivery date N1*ST*Ship-To Warehouse*92*WH-001 ~ Ship-to party N3*123 Commerce Blvd N4*Austin*TX*78701*US PO1*1*24*EA*29.99*TE*VN*SKU-ABC123 ~ Line 1: qty 24, unit price $29.99 PID*F****Blue Widget 500ml PO1*2*12*EA*49.99*TE*VN*SKU-DEF456 ~ Line 2: qty 12, unit price $49.99 PID*F****Red Widget Pro CTT*2 ~ 2 line items total SE*12*0001 GE*1*1 IEA*1*000000001
PO1 segments are your order line
items. Each contains quantity, unit of measure, unit price, and a
vendor-specific product identifier (your SKU). This maps directly to Shopify's
line_items array in the Orders API.
EDIFACT uses a different delimiter convention (plus signs and apostrophes by default) but carries the same semantic payload:
UNB+UNOA:1+RETAILER+YOURCOMPANY+260501:0900+00001' UNH+1+ORDERS:D:96A:UN:EAN008' BGM+220+PO-98765+9' ← document type + PO number + original DTM+137:20260501:102' ← document date DTM+2:20260515:102' ← delivery date NAD+ST+WH-001::92+Ship-To Warehouse+123 Commerce Blvd+Austin+TX+78701+US' LIN+1++SKU-ABC123:SA' ← line 1 with seller's article number IMD+F++:::Blue Widget 500ml' QTY+21:24' ← ordered quantity: 24 PRI+AAA:29.99' ← unit price: 29.99 LIN+2++SKU-DEF456:SA' IMD+F++:::Red Widget Pro' QTY+21:12' PRI+AAA:49.99' UNS+S' CNT+2:2' UNT+18+1' UNZ+1+00001'
The following process is proven across hundreds of B2B ecommerce deployments. Follow it in order — skipping steps is the single biggest cause of failed go-lives.
Every retailer sends you an EDI Implementation Guide (IG) — a PDF specifying exactly which segments, qualifiers, and codes they require. Never skip this. Even if you know X12 850 cold, Target's IG is different from Walmart's, which is different from Costco's.
Key items to extract from the IG before touching any code:
There are three viable approaches for connecting EDI to Shopify. Each has a real tradeoff:
| Approach | Best For | Typical Cost | Setup Time |
|---|---|---|---|
| Managed VAN (SPS Commerce, TrueCommerce, DiCentral) |
Teams with no EDI expertise; retailer mandates specific VAN | $500–$3k/month | 2–6 weeks |
| iPaaS / Integration
Platform (Celigo, Boomi, MuleSoft, Workato) |
Engineering teams wanting control + pre-built Shopify connectors | $1k–$5k/month | 3–8 weeks |
| Custom EDI Parser + Shopify
API (node-x12, pyx12, python-edifact) |
High-volume teams needing full control and cost efficiency | Dev time only | 6–16 weeks |
EDI files need a secure transport mechanism. AS2 (Applicability Statement 2) is the industry standard for direct connections and is required by most large retailers. SFTP is common for smaller partners or internal tools.
{
"as2Id": "YOURCOMPANY",
"partnerId": "RETAILER",
"url": "https://as2.retailer.com:4080/as2",
"encryption": "AES-256-CBC",
"signing": "SHA-256",
"mdn": {
"type": "sync", // request synchronous MDN
"signed": true
},
"certificates": {
"your_private": "./certs/yourcompany.p12",
"partner_public": "./certs/retailer.cer"
}
}
Certificate exchange happens out-of-band — you'll email your public certificate to your partner's EDI team and receive theirs. Budget a full week for this process with large retailers due to their security review cycles.
This is where the real engineering work happens. You need to extract every relevant field from the 850/ORDERS and map it to the Shopify Orders API. Here's a complete field mapping:
| X12 850 Segment/Element | EDIFACT ORDERS | Shopify Field | Notes |
|---|---|---|---|
| BEG03 (PO Number) | BGM02 | order.name / tags | Store as tag for searchability |
| DTM02 (Delivery Date) | DTM+2 | order.note_attributes | No native Shopify field; use metafield |
| N1*ST (Ship-To) | NAD+ST | order.shipping_address | Map N3/N4 to address lines |
| N1*BT (Bill-To) | NAD+BY | order.billing_address | Often same as ship-to in B2B |
| PO1*02 (Quantity) | QTY+21 | line_items[].quantity | Watch UOM — convert CS→EA if needed |
| PO1*04 (Unit Price) | PRI+AAA | line_items[].price | May differ from your Shopify price list |
| PO1*07 (SKU, qualifier VN) | LIN+SA | line_items[].sku | Match to Shopify variant.sku exactly |
| REF*DP (Dept. Number) | RFF+DP | order.note_attributes | Required for some retailer invoicing |
import { X12Parser } from 'node-x12'; async function edi850ToShopifyOrder(rawEdiString) { const parser = new X12Parser(true); const interchange = parser.parse(rawEdiString); const group = interchange.functionalGroups[0]; const transaction = group.transactions[0]; // 850 // Extract PO number from BEG segment, element 03 const poNumber = transaction.get('BEG03'); // Extract ship-to address from N1/N3/N4 loop const n1Loop = transaction.getLoop('N1', 'ST'); const shippingAddress = { address1: n1Loop.get('N301'), address2: n1Loop.get('N302') || '', city: n1Loop.get('N401'), province: n1Loop.get('N402'), zip: n1Loop.get('N403'), country: n1Loop.get('N404'), }; // Extract line items from PO1 loops const lineItems = transaction.getLoops('PO1').map(po1 => ({ sku: po1.get('PO107'), // VN qualifier = vendor SKU quantity: parseInt(po1.get('PO102')), price: parseFloat(po1.get('PO104')), title: po1.get('PID05') || '', // from PID segment in loop })); // Resolve Shopify variant IDs from SKUs const resolvedItems = await Promise.all( lineItems.map(async item => { const variantId = await lookupVariantBySku(item.sku); if (!variantId) throw new Error(`SKU not found: ${item.sku}`); return { variant_id: variantId, quantity: item.quantity, price: item.price }; }) ); return { order: { line_items: resolvedItems, shipping_address: shippingAddress, tags: [`edi-po:${poNumber}`, 'edi-order'], note_attributes: [ { name: 'edi_po_number', value: poNumber }, { name: 'edi_source', value: 'X12-850' }, ], financial_status: 'pending', } }; }
With your mapped payload ready, post it to Shopify's Admin REST or
GraphQL API. For B2B orders, use the
send_receipt: false flag to suppress the
customer-facing confirmation email (your trading partner expects EDI
acknowledgments, not email).
const response = await fetch( `https://${SHOP}.myshopify.com/admin/api/2024-04/orders.json`, { method: 'POST', headers: { 'X-Shopify-Access-Token': ACCESS_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ ...shopifyOrderPayload, send_receipt: false, // suppress email to partner send_fulfillment_receipt: false, inventory_behaviour: 'decrement_obeying_policy', }), } ); if (!response.ok) { const error = await response.json(); // Log full error — Shopify returns field-level validation errors throw new Error(`Shopify order creation failed: ${JSON.stringify(error)}`); } const { order } = await response.json(); // Store order.id → poNumber mapping in your database for 855/856 generation await savePoMapping({ shopifyOrderId: order.id, poNumber, partnerId });
Most trading partners require an 855 Purchase Order Acknowledgment within 24–48 hours. This document tells the retailer you received their PO and whether you accept it in full, partially, or reject it. Missing the SLA on this is a chargeback risk.
ISA*00* *00* *ZZ*YOURCOMPANY *ZZ*RETAILER *260501*1000*^*00501*000000002*0*P*: GS*PR*YOURCOMPANY*RETAILER*20260501*1000*2*X*005010 ST*855*0001 BAK*00*AC*PO-98765*20260501 ~ AC = Accepted, references original PO# PO1*1*24*EA*29.99*VN*SKU-ABC123 ACK*IA*24*EA*202*20260515 ~ IA = Item Accepted PO1*2*12*EA*49.99*VN*SKU-DEF456 ACK*IA*12*EA*202*20260515 CTT*2 SE*9*0001 GE*1*2 IEA*1*000000002
The BAK02 element is your acceptance code.
AC = fully accepted. AD = accepted with
changes. RJ = rejected. If you change quantities or
dates, you must use AD and document exactly what changed on each PO1
line.
When Shopify marks an order as fulfilled (via webhook
orders/fulfilled), your integration must immediately
generate and transmit an 856 Advance Ship Notice. The ASN must
include tracking numbers, carrier codes, and exact carton-level
detail for many retailers. This document is time-sensitive — many
partners require ASN receipt before the physical shipment arrives.
// Register webhook in Shopify POST /admin/api/2024-04/webhooks.json { "topic": "orders/fulfilled", "address": "https://your-integration.com/webhooks/fulfillment", "format": "json" } // In your webhook handler: async function handleFulfillment(shopifyOrder, fulfillment) { const poMapping = await getPoMapping(shopifyOrder.id); const asnData = { shipmentId: `ASN-${Date.now()}`, poNumber: poMapping.poNumber, shipDate: fulfillment.created_at, carrier: mapCarrierCode(fulfillment.tracking_company), trackingNum: fulfillment.tracking_number, lines: fulfillment.line_items.map(li => ({ sku: li.sku, quantity: li.quantity, })), }; const edi856 = generate856(asnData, poMapping.partnerId); await transmitViaAS2(edi856, poMapping.partnerId); }
EDI integrations fail silently in ways that cost real money. A malformed 850 that never creates a Shopify order is invisible unless you're actively monitoring. At minimum you need:
These are the errors that appear on every post-mortem after a failed EDI go-live. Most are invisible in testing and catastrophic at scale.
Shopify variant IDs change when products are deleted and recreated. Trading partners only know your SKU. Build your lookup table on sku, never on ID.
Control numbers must be unique and sequential per trading partner. Reusing them causes partner systems to reject your transmissions as duplicates — silently.
A PO for 5 CA (cases) of 12 units is 60 units in Shopify. Getting this wrong overfulfills or underfulfills every order. It also breaks your ASN quantities.
Retail partners charge back $50–$500 per late or missing PO acknowledgment. Many require the 855 within 24 hours — not business hours, calendar hours.
N1 loops use qualifier codes that differ per partner (92 vs
9 vs UL for location identifiers). Hardcoding
one causes misrouting when you add partner #2.
Sending a test 855 with a real PO number triggers your partner's fulfillment workflows. Two teams learned this via unexpected shipments to test warehouses.
AS2 retries, network timeouts, and webhook replays can cause the same 850 to be processed twice — creating duplicate Shopify orders for the same PO.
Trading partners send files that violate their own IGs. Optional segments are sometimes present, required segments are sometimes missing. Your parser must be defensive.
Use this before every new trading partner activation. Copy it to your project management tool and check off each item with a linked artifact (screenshot, config file, or test result).
send_receipt: falseorders/fulfilled webhookEDI integration is not a feature you ship once. Trading partners update their IGs. Retailers change their chargeback policies. New document types get added (functional acknowledgments, 997s, PO changes via 860s). Shopify's API version ticks forward. Your item master diverges from your partner's records.
Build your integration as a maintainable service, not a script. Keep your field mappings in configuration rather than code. Version your IG interpretations. Log everything. The teams that treat EDI as infrastructure — rather than a one-time project — are the ones that onboard their fifth retail partner as smoothly as their first.
Ready to go live?
We've done this exact stack — 850 to Shopify, AS2, 855/856 automation — for ecommerce brands scaling into retail. One call to scope it, no legacy consultants.
Book a free scoping call →