The blog · 2026-07-31
Ecommerce has orders and shipments — and no invoice, anywhere
Written for ED-3 — the in-house supply-chain platform team — the platform engineer wiring the DTC channel of a co-manufactured brand.
One thesis: an ecommerce platform can tell the record what was ordered and what shipped, and it structurally cannot tell the record what was billed — there is no invoice object in any major platform's core API — so an ecommerce-sourced record claims po and desadv context honestly and goes to the ERP for inv, instead of laundering a checkout total into a billing document.
The compile map, Shopify edition
Shopify's Admin API is GraphQL-canonical (REST has been legacy since late 2024; new public apps have been GraphQL-only since April 2025), webhooks are HMAC-signed (X-Shopify-Hmac-Sha256) and deliverable over HTTPS, Google Pub/Sub, or Amazon EventBridge, and dev stores are free — the lowest-friction po/desadv source in the entire connector matrix. The objects compile as:
- Order → the po-analog reference (the demand side of the cycle, in the platform's own grain)
- FulfillmentOrder + Fulfillment (with trackingInfo and fulfillment events) → desadv
- Return → ReverseFulfillmentOrder → ReverseDelivery → rma
Each lands as a CBV business-transaction reference on the events it contextualizes — core fields, not extensions — and the compiled output validates against the pinned official EPCIS 2.0 JSON schema: superset where the platform's objects carry more than the standard models, and conformant always.
And then the map stops, one row short.
The invoice absence is a category law
Shopify has no invoice object. The closest thing the API surface offers is DraftOrder.invoiceUrl, and it is worth being precise about what that is: a checkout link — a URL a buyer pays at — not a billing document with lines, parties, and terms.
This is not a Shopify quirk you route around by picking a different cart. In our verified matrix the finding holds across the category: BigCommerce carries invoices only in its separately-sold B2B Edition, not core; WooCommerce has no invoice in core. The pattern is structural. Ecommerce platforms are demand-capture systems; billing — the inv reference, the document an auditor reconciles payments against — lives in the ERP or the accounting system, full stop. Which is exactly where the record should source it: the full-cycle systems carry real invoice objects, and the compile takes the inv reference from there.
Say it as a design law: the connector never mints a reference type the source system does not actually possess. An ecommerce-sourced record carries po, desadv, and rma context and is silent on inv — and a record that is honestly silent is worth more than one that is confidently wrong, because the silence is machine-legible: a downstream consumer that needs billing context knows to look at the ERP join, not to trust a checkout total wearing an invoice costume.
The barcode field, quoted
The second honesty line item is identity. Shopify's product identity field is ProductVariant.barcode, and its own documentation describes it as the "barcode, UPC, or ISBN" — free text, unvalidated. Whatever the merchant typed is what the API returns: a real GTIN-13, a UPC-A missing its check digit, an internal SKU, or an empty string, all indistinguishable at the schema level. Party identity is thinner still: MailingAddress is free-text lines everywhere, and no GLN field exists anywhere in the surface.
So the compile treats ecommerce identity as raw material, not as identity: every barcode value is checksum-validated before it is allowed to become the GTIN component of an EPC class URI, failures fall back to tenant-scoped URIs (CBV explicitly permits non-GS1 identifiers, so the record stays valid), and each join carries an identity grade — GTIN-grade where the checksum held, SKU-grade where it didn't — so downstream consumers know the join strength instead of assuming it. The full census behind that posture is the GTIN-gap post; the resolution machinery is barcoding.dev's layer.
The proof artifact: one fulfillment, compiled
One Shopify fulfillment with tracking, compiled to the shipping-side skeleton:
{
"type": "ObjectEvent",
"action": "OBSERVE",
"bizStep": "shipping",
"disposition": "in_transit",
"quantityList": [
{
"epcClass": "urn:epc:class:lgtin:0614141.107346.SHOPIFY",
"quantity": 2,
"uom": "EA"
}
],
"bizTransactionList": [
{ "type": "desadv", "bizTransaction": "urn:epc:id:gdti:0614141.00001.F10442" },
{ "type": "po", "bizTransaction": "urn:epc:id:gdti:0614141.00001.O98213" }
]
}
Two references, no inv, identity graded — the record claims exactly what the platform knows. What closes the loop is not ecommerce at all: the physical receiving side comes from the 3PL's receiving object, the delivery corroboration from the parcel rails as evidence, and the reconcile between claim and observation is a typed verdict.
What free-text addresses cost at reconcile time
One consequence deserves its own paragraph, because it surfaces months after go-live. Reconciliation joins on party and place as well as product, and an ecommerce record's parties are strings. "ACME Distribution, 100 Main St" and "Acme Dist. — 100 Main Street" are the same counterparty to a human and two counterparties to a set comparison. The compile's answer is the same as for products: address normalization against a customer-supplied party master, GLN out where resolution succeeds, tenant-scoped party URIs where it doesn't, grade attached. The cost never disappears — it moves to where it is visible and priced.
The DTC channel is a third of the paper trail. The record wants all three. Tell us what your stack looks like — the access flow's branching questions exist so the channel you actually run is the branch that gets wired.
875 words. The access flow is at /get-access/; the family's event door is epcis.dev.