← All case studies

cXML PunchOut Integration for Enterprise B2B Procurement

Full-Stack Developer · 2023 – 2024

  • Node.js
  • React
  • AWS Serverless
  • GraphQL
  • cXML · PunchOut
  • XML / DTD Validation
  • UNSPSC / UOM Mapping

The buyer who never visits the store

A B2B supplier ran a public storefront, but its biggest prospect was a Fortune 500 buyer who never shops on public websites. That buyer purchases through a corporate e-procurement platform, where employees raise requisitions, route them through budget and approval, and emit purchase orders against negotiated contracts. Selling into that account meant the storefront could not simply take an order. It had to appear inside the buyer’s procurement system as a PunchOut catalog, a supplier the buyer’s own software already knows how to talk to.

That conversation runs on cXML, the commerce XML dialect that governs PunchOut. cXML is strict and DTD-versioned, so every message had to validate against the exact version the buyer’s platform spoke. One malformed element, a missing required extrinsic, or a unit of measure the platform did not recognize, and the cart came back rejected with a terse diagnostic. Pricing shown during a session had to be the buyer’s contract pricing rather than public retail. Sessions were credential-authenticated and short-lived, and the round-trip had to finish inside the shopper’s browser without them ever feeling they had left the procurement application.

A conversation in cXML

PunchOut runs e-commerce backwards. The buyer’s system opens the session, hands the shopper off to the supplier storefront to build a cart, then takes that cart back as a requisition. No order is ever placed on the storefront itself. The whole exchange is a run of cXML documents passed between the two systems, with the shopper’s browser carrying them.

Fig. 1 · cXML PunchOut round-trip
Buyer e-Procurement Platform Supplier PunchOut Storefront 1 — PunchOutSetupRequest (cXML) Shared-Secret credential · BuyerCookie · BrowserFormPost URL 2 — PunchOutSetupResponse one-time StartPage URL carrying the session token user's browser opens StartPage → 3 — Shopping session on storefront buyer-specific contract pricing · cart built 4 — PunchOutOrderMessage cart lines · SupplierPartID · UOM · UNSPSC · price (BrowserFormPost) 5 — OrderRequest — approved Purchase Order sent after buyer-side requisition + approval workflow 6 — OrderResponse — 200 acknowledgment

Walking the sequence:

  1. PunchOutSetupRequest. The shopper clicks the supplier catalog inside the procurement UI, and the buyer’s platform POSTs a cXML PunchOutSetupRequest to the storefront’s endpoint. It carries a Credential block (a shared-secret identity), a BuyerCookie that ties the round-trip together, and a BrowserFormPost URL, the address the finished cart must return to.
  2. PunchOutSetupResponse. The storefront authenticates the credential, provisions a session, and answers with a PunchOutSetupResponse holding one StartPage URL. That URL is single-use and carries an opaque session token.
  3. Shopping session. The buyer’s platform redirects the browser to the StartPage. The shopper browses in punch-in mode, where catalog and pricing are scoped to the buyer’s contract and the cart binds to the session token rather than a normal storefront login.
  4. PunchOutOrderMessage. On transfer cart, the storefront serializes the cart to a cXML PunchOutOrderMessage and returns it through an auto-submitting HTML form POST to the BrowserFormPost URL from step 1. Each ItemIn line carries a SupplierPartID, a UnitPrice with currency, a description, a unit of measure, and a UNSPSC classification code. The cart lands in the buyer’s system as a requisition line rather than an order.
  5. OrderRequest. Once the requisition clears the buyer’s internal approval workflow, their platform issues a formal purchase order as a cXML OrderRequest to the supplier’s order endpoint.
  6. OrderResponse. The storefront acknowledges with a 200 cXML OrderResponse, and the loop closes.

Steps 1 through 4 are Level 1 PunchOut. The integration also ran Level 2, where individual catalog items are indexed inside the procurement system’s own search, so a shopper can deep-link straight to a product page in punch-in mode instead of starting from the storefront home.

The endpoint that speaks another protocol

The storefront was a React front end over a Node.js API on AWS serverless (Lambda), with GraphQL driving the shopping experience. PunchOut fits none of that. It speaks cXML over plain HTTPS form POSTs, so it lived in its own set of serverless cXML endpoints alongside the GraphQL API.

The inbound edge did the security work. Every PunchOutSetupRequest was parsed, DTD-validated, and checked against the buyer’s registered shared secret before a session existed. Anything that failed schema or auth came back as a spec-compliant cXML fault rather than an HTTP error page the procurement platform would not know how to read.

punchout-setup.ts TS
// A PunchOutSetupRequest lands as raw cXML on an HTTPS form POST. Check the
// shared secret in constant time before trusting the rest of the document,
// then keep the two anchors the round-trip turns on: the BuyerCookie that
// pairs this setup with its eventual cart, and the URL to post that cart back.
export function readSetupRequest(xml: string) {
const doc = parseCxml(xml);                   // already rejected if not DTD-valid
const sender = doc.first('Header/Sender/Credential');
const claimed = Buffer.from(sender.text('SharedSecret'), 'utf8');
const onFile = Buffer.from(secretFor(sender.attr('identity')), 'utf8');

// Length guard first: timingSafeEqual throws on buffers of unequal length.
if (claimed.length !== onFile.length || !timingSafeEqual(claimed, onFile)) {
  return cxmlFault(401, 'Unauthorized: shared-secret mismatch');
}

const setup = doc.first('Request/PunchOutSetupRequest');
return {
  operation: setup.attr('operation'),         // 'create' on a fresh punch-in
  buyerCookie: setup.text('BuyerCookie'),
  formPostUrl: setup.text('BrowserFormPost/URL'),
};
}

A valid setup minted a server-side session keyed to the BuyerCookie and a fresh, single-use StartPage token with a bounded lifetime. The token authorized the shopping session, not a cookie login, so a punch-in cart could never leak into or out of a normal storefront account. During the session the storefront resolved catalog visibility and pricing against the buyer’s negotiated contract, so the shopper saw their prices.

Outbound was a mapping problem. On transfer the cart became a PunchOutOrderMessage, and every field had to line up with what the buyer’s platform expected: SupplierPartID mapping, unit-of-measure normalization from the storefront’s UOM to the buyer’s codes, a UNSPSC classification per line, currency and tax, and any buyer-mandated Extrinsic fields the approval workflow required (cost center, requester, unique name). The response went back as the auto-submitting BrowserFormPost, so the hand-back stayed invisible to the shopper.

Getting the dialect exactly right

Most of the effort was not writing XML. It was aligning with the buyer’s procurement team so the two systems agreed on every detail before go-live. A Fortune 500 e-procurement platform is a governed environment, and a supplier does not publish an endpoint and flip a switch.

We started in the buyer’s test realm, exchanging identities and shared secrets and agreeing on the PunchOut, order, and Level 2 endpoints on both sides. Then came the part generic PunchOut documentation never fully pins down: the exact cXML version, and the precise set of required and optional extrinsics the platform expected in each direction. Real punch-in sessions ran from inside their sandbox, and rejected carts returned sparse platform diagnostics. Each round tightened the mapping, a UOM code here, a missing classification there, an extrinsic the approval workflow insisted on. Once carts round-tripped cleanly and requisitions rendered with the right pricing and metadata, the buyer’s team ran user-acceptance testing and formally certified the catalog before it moved to production. Cutover to production credentials and endpoints was scheduled jointly and confirmed with a live smoke-test punch-in.

That cadence, precise and artifact-driven and patient, was as much a part of shipping the integration as the code was.

Zoom out and the PunchOut integration is a single link in the buyer’s procure-to-order lifecycle. The storefront never touches the approval workflow in the middle. It shows up at two points only: the punch-in and cart hand-back, and receiving the resulting purchase order.

Fig. 2 · Procure-to-order lifecycle
01 PunchOut & Cart 02 Requisition 03 Approval workflow 04 Purchase Order 05 Fulfillment & Invoice Shaded stages = supplier-system touchpoints. The PunchOut integration plugs the storefront into the buyer's requisition-to-PO lifecycle.

Designing to that boundary was the point. The storefront returned a requisition-ready cart with clean metadata, accepted a compliant PO, and stayed entirely out of the buyer’s budget, approval, and routing logic.

Outcomes

Engineering Outcomes

6-message
the whole cXML loop, from setup to cart to PO and back
Level 1 & 2
punch-in browsing plus in-platform catalog search
Per-line
SupplierPartID, UOM, UNSPSC, and contract price on every line

Directorial Outcomes

Certified
cleared the enterprise buyer's UAT and go-live gate
New channel
storefront now reachable inside a Fortune 500 procurement system
Zero-touch
cart hand-back invisible to the shopper's workflow