A cXML punchout round trip is four XML documents sent over HTTPS. The buyer's procurement system opens a session with a PunchOutSetupRequest, the supplier answers with a PunchOutSetupResponse, the cart travels back as a PunchOutOrderMessage, and after approval the purchase order arrives as an OrderRequest. This page walks through each document with a sample, the fields that matter, and the errors that show up most often during buyer testing. If the round trip itself is new to you, start with how punchout works. If your buyer runs SAP and talks about HOOK_URL, you need the OCI field reference instead.
The samples are written for this page. Identities, secrets and URLs are placeholders; real values come from the buyer's IT team and from the connection you create in PunchRelay.
The envelope every document shares
Every cXML document is a <cXML> root with a payloadID (unique per document, used to spot duplicates) and a timestamp. Requests carry a Header with three parties. From is the organization the document comes from, To is the organization it is meant for, and Sender is whoever actually transmits it, which matters when a network such as SAP Business Network relays the document. Each party is a Credential with a domain (for example NetworkID for an Ariba ANID, or DUNS) and an Identity. The Sender credential also carries the SharedSecret, the password the supplier issued to this buyer.
PunchRelay authenticates a request by the From identity together with the Sender shared secret, compared in constant time against your enabled connections. The documents PunchRelay sends declare the cXML 1.2.014 DTD, and its test suite validates them against that DTD and against the current cXML DTD (1.2.071).
PunchRelay connects your Shopify B2B store to your buyers' procurement systems: contract prices go out in the punchout catalog, and purchase orders come back to your inbox for review.
PunchOutSetupRequest: the buyer opens a session
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE cXML SYSTEM "http://xml.cxml.org/schemas/cXML/1.2.014/cXML.dtd">
<cXML payloadID="20261016091200.4821@procure.example.org"
timestamp="2026-10-16T09:12:00+02:00" xml:lang="en-US">
<Header>
<From>
<Credential domain="NetworkID">
<Identity>AN01000012345-T</Identity>
</Credential>
</From>
<To>
<Credential domain="NetworkID">
<Identity>AN02000067890</Identity>
</Credential>
</To>
<Sender>
<Credential domain="NetworkID">
<Identity>AN01000012345-T</Identity>
<SharedSecret>the-secret-you-issued</SharedSecret>
</Credential>
<UserAgent>Procurement Suite 2026</UserAgent>
</Sender>
</Header>
<Request deploymentMode="test">
<PunchOutSetupRequest operation="create">
<BuyerCookie>c41e-88f0-requisition-5521</BuyerCookie>
<Extrinsic name="UserEmail">m.keller@procure.example.org</Extrinsic>
<BrowserFormPost>
<URL>https://procure.example.org/punchout/cart-return</URL>
</BrowserFormPost>
</PunchOutSetupRequest>
</Request>
</cXML>The buyer's system posts this document to the supplier's setup URL when a user clicks the supplier's catalog. The fields that decide whether the session works:
operation:createstarts an empty session,editreopens a cart the buyer already transferred so the user can change it, andinspectreopens it read-only. PunchRelay supports all three. Foreditandinspectthe request lists the earlier cart lines asItemOutelements.BuyerCookie: an opaque value the supplier must echo back unchanged with the cart, so the buyer's system can match the cart to the requisition that started it. Required.BrowserFormPost/URL: where the user's browser posts the cart at the end of the session. Required, and it has to be an absolute https URL.Extrinsic: free-form name and value pairs. Buyers commonly send the user's email asUserEmail; PunchRelay stores it (orUserId) with the session.deploymentMode:testorproduction. Many networks issue separate test identities; SAP Business Network test accounts append-Tto the ANID.
PunchOutSetupResponse: the supplier answers with a start page
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE cXML SYSTEM "http://xml.cxml.org/schemas/cXML/1.2.014/cXML.dtd">
<cXML payloadID="20261016071201.77@relay.example.com"
timestamp="2026-10-16T07:12:01+00:00" xml:lang="en">
<Response>
<Status code="200" text="OK" xml:lang="en"/>
<PunchOutSetupResponse>
<StartPage>
<URL>https://relay.example.com/catalog?token=one-time-token</URL>
</StartPage>
</PunchOutSetupResponse>
</Response>
</cXML>The answer is synchronous: a Response with a Status and, on success, a StartPage URL that the buyer's system opens in the user's browser. The start page has to log the user in without a password, so it carries a token. PunchRelay's token is single-use and expires after 15 minutes, which is why an old start URL copied from a log will not open the catalog again.
In the catalog the user sees the Shopify B2B catalog and contract prices of the company and location that the connection is bound to.
PunchOutOrderMessage: the cart goes back
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE cXML SYSTEM "http://xml.cxml.org/schemas/cXML/1.2.014/cXML.dtd">
<cXML payloadID="20261016071900.12@relay.example.com"
timestamp="2026-10-16T07:19:00+00:00" xml:lang="en">
<Header>
<From><Credential domain="NetworkID">
<Identity>AN02000067890</Identity></Credential></From>
<To><Credential domain="NetworkID">
<Identity>AN01000012345-T</Identity></Credential></To>
<Sender><Credential domain="NetworkID">
<Identity>AN02000067890</Identity></Credential>
<UserAgent>PunchRelay/1.0</UserAgent></Sender>
</Header>
<Message>
<PunchOutOrderMessage>
<BuyerCookie>c41e-88f0-requisition-5521</BuyerCookie>
<PunchOutOrderMessageHeader operationAllowed="edit">
<Total><Money currency="EUR">187.20</Money></Total>
</PunchOutOrderMessageHeader>
<ItemIn quantity="8">
<ItemID>
<SupplierPartID>PAP-A4-80-5R</SupplierPartID>
<SupplierPartAuxiliaryID>sess-9d2f</SupplierPartAuxiliaryID>
</ItemID>
<ItemDetail>
<UnitPrice><Money currency="EUR">23.40</Money></UnitPrice>
<Description xml:lang="en">Copy paper A4 80 g · Box of 5 reams</Description>
<UnitOfMeasure>BX</UnitOfMeasure>
<Classification domain="UNSPSC">14111507</Classification>
<ManufacturerPartID>CP-A480-5</ManufacturerPartID>
<ManufacturerName xml:lang="en">Example Paper Mill</ManufacturerName>
<LeadTime>2</LeadTime>
</ItemDetail>
</ItemIn>
</PunchOutOrderMessage>
</Message>
</cXML>When the user transfers the cart, the supplier does not call the buyer's system directly. It returns an HTML page whose form posts the cart through the user's browser to the BrowserFormPost URL. The XML sits in a hidden field named cxml-urlencoded (the XML itself, form-encoded) or cxml-base64 (the XML in base64). Buyers expect one or the other; PunchRelay uses cxml-urlencoded by default and lets you switch a connection to cxml-base64.
What each part carries in the cart PunchRelay returns:
BuyerCookie, echoed from the setup request, andoperationAllowed="edit", which tells the buyer's system that it may reopen the cart with aneditsession later.Total: the sum of price times quantity over all lines.ItemIn quantitywithItemID:SupplierPartIDis the variant's SKU (or its ID when the SKU is empty), andSupplierPartAuxiliaryIDidentifies the session, so an edit request and a later purchase order can be matched to this cart.ItemDetail:UnitPricewith its currency, aDescriptionmade of product and variant title,UnitOfMeasureas a UN/CEFACT Recommendation 20 code such asEAorBX, and, when your product data has them, aClassificationin the UNSPSC domain (8 or 10 digits),ManufacturerPartID,ManufacturerNameandLeadTimein days.
A transferred cart is a requisition in the buyer's system, not an order. Nothing is created in Shopify at this point.
OrderRequest: the purchase order arrives
<Request deploymentMode="test">
<OrderRequest>
<OrderRequestHeader orderID="4500018832" orderDate="2026-10-17T10:02:00+02:00"
type="new">
<Total><Money currency="EUR">187.20</Money></Total>
<ShipTo>
<Address addressID="DC-NORTH">
<Name xml:lang="en">Example Org, North warehouse</Name>
<PostalAddress>
<DeliverTo>Goods receipt, dock 3</DeliverTo>
<Street>Industriestrasse 12</Street>
<City>Hamburg</City>
<PostalCode>20095</PostalCode>
<Country isoCountryCode="DE">Germany</Country>
</PostalAddress>
</Address>
</ShipTo>
</OrderRequestHeader>
<ItemOut quantity="8" lineNumber="1">
<ItemID>
<SupplierPartID>PAP-A4-80-5R</SupplierPartID>
<SupplierPartAuxiliaryID>sess-9d2f</SupplierPartAuxiliaryID>
</ItemID>
<ItemDetail>
<UnitPrice><Money currency="EUR">23.40</Money></UnitPrice>
<Description xml:lang="en">Copy paper A4 80 g · Box of 5 reams</Description>
<UnitOfMeasure>BX</UnitOfMeasure>
</ItemDetail>
</ItemOut>
</OrderRequest>
</Request>Once the requisition is approved, the buyer's system (or its network) posts an OrderRequest to the supplier's order URL, with the same kind of header and shared secret as the setup request. The header is omitted from the sample. The parts PunchRelay reads:
OrderRequestHeader:orderID(the buyer's PO number),orderDate,type, theTotal,ShipToandBillToaddresses, and optionalShippingandTaxamounts.ItemOut: quantity,SupplierPartID,SupplierPartAuxiliaryIDand the unit price of each line, plus line-level shipping and tax when present.
PunchRelay accepts type="new". It answers with status 200 once the purchase order is stored, checks it against the cart the buyer transferred, and places it in the Purchase orders inbox in the app. A Shopify order is created only when someone on your team reviews it and clicks Create Shopify order.
Status codes and common errors
cXML reports the result inside the document: PunchRelay answers its setup and order URLs with HTTP 200 and a Status element whose code and text say what happened. Read the cXML status in the buyer's logs, not only the HTTP status.
| Status | Meaning | Typical cause during buyer testing |
|---|---|---|
200 OK | Accepted | Session opened, or purchase order stored for review. |
400 Bad Request | The document cannot be used | Missing BuyerCookie or BrowserFormPost URL, a return URL that is not absolute https, an unknown operation, an edit request whose lines do not come from a cart PunchRelay returned, or a purchase order without a Total. |
401 Unauthorized | Authentication failed | The From identity and Sender shared secret do not match an enabled connection. Most often a test identity (with -T) against a production connection, or the reverse. |
409 | Conflicts with a purchase order already received | The same orderID sent again with different content. Resending an identical purchase order is answered 200 and does not create a second one. |
450 Not Implemented | Valid request the supplier does not process | An OrderRequest of type="update" or "delete". Agree with the buyer how change orders reach you. |
500 Internal Server Error | Temporary failure on the supplier side | The buyer's system should retry later. |
One failure shows no cXML status, because it happens in the browser after a successful setup: the start page token is single-use and lasts 15 minutes, so a test tool that replays an old start URL, or a browser that opens it twice, lands on an error page instead of the catalog. A fresh setup request fixes it.
What PunchRelay supports
In short: PunchOutSetupRequest with create, edit and inspect; PunchOutSetupResponse; PunchOutOrderMessage returned as cxml-urlencoded or cxml-base64; and new purchase orders as OrderRequest, held for your review. Documents declare the cXML 1.2.014 DTD. Lines carry UNSPSC codes (8 or 10 digits) and UN/CEFACT Recommendation 20 units. Every document an authenticated connection sends, and every document PunchRelay returns, is logged with secrets redacted, so your buyer's IT team and you can read the same XML when a test fails.
To set up the connection that issues the identity, secret and URLs above, follow the setup guide. For the choice between cXML and OCI, read cXML vs OCI, practically.