Field Notes
Commerce & Integration
13 min· 19 August 2026

cXML Punchout: What the Spec Doesn't Tell You

By Ganesh

A large customer tells you they can only buy through their procurement system, and someone forwards you a PDF of the cXML specification. It looks manageable — three documents, a redirect, a form post. Two months later you are explaining to your sales director why the deal still hasn't closed, and the reason is a buyer-side testing queue you have no visibility into.

Punchout is genuinely not difficult. Almost everything that goes wrong with it goes wrong in places the specification doesn't discuss.

The short version

  • →The returned cart is a requisition, not an order. The purchase order is a separate document that arrives later.
  • →The edit operation exists, buyers use it, and forgetting it is the classic UAT failure.
  • →Your cart returns through the user's browser, not server to server — log the payload before you post it.
  • →Buyer-side testing access is usually the longest item on the timeline and none of it is under your control.
  • →Line-item metadata — commodity codes, units of measure — fails downstream where you can't see it.

The shape of it

A buyer sitting in Ariba or Coupa clicks your supplier tile. Their system sends you a PunchOutSetupRequest — server to server, with a shared secret in the credential block. You validate it, create a session, and reply with a URL. Their system then redirects the user's browser to that URL, and the buyer is now shopping in your storefront, logged in as an identity you inferred from the setup request.

They add items and click something that looks like checkout. It isn't. Your page renders a hidden form containing a base64-encoded PunchOutOrderMessage and posts it to the browser form-post URL the setup request supplied. The buyer lands back in their procurement system with a populated requisition.

Nothing has been ordered

That requisition now enters an approval workflow that may take days. Only when it clears does a purchase order come to you, as a separate cXML OrderRequest posted to a different endpoint. Treating the returned cart as an order is the single most expensive misunderstanding in this integration, because it usually surfaces after go-live as orders that were never really placed.

The operation attribute

On PunchOutSetupRequest there is an operation attribute. Most tutorials show create and stop there. There are three values and buyers use all of them.

OperationWhat the buyer didWhat you must do
createStarted a new requisitionFresh empty cart
editReopened an existing requisition to change itRebuild the cart from the returned items, let them modify, post back the full revised cart
inspectOpened a line to view it, often read-onlyShow the item, do not allow changes, do not expect a post back

edit is where implementations fall over. The buyer's system sends back the previously returned items inside the setup request, and your job is to reconstruct that cart faithfully — including quantities, the prices as they were quoted, and your own supplier part IDs — so the buyer sees what they saw before. If you rebuild it by looking up current prices, a buyer who returns after a price change gets a silently different requisition, and their approval trail no longer matches.

This is almost never caught in development, because developers test create. It is caught in joint UAT with the buyer, which is the most expensive place to find it.

Your cart goes home through a browser

The return leg is a form post rendered in the buyer's browser, not a server-to-server call. That has consequences worth planning for.

You get no response you can read. If the buyer's system rejects your document, the error appears on their screen, in their system, in language written for them. You will hear about it as "it didn't work," possibly a day later, possibly relayed through a salesperson.

So log the decoded payload before you render the form, keyed to the session, and keep those logs longer than feels necessary. When a buyer reports a failure, the exact document you sent is the only thing that turns a vague complaint into a fixable defect.

csharp
// Render the return form. The browser posts it; you never see the response.
public async Task<IActionResult> ReturnCart(string sessionId, CancellationToken ct)
{
    var session = await _sessions.GetAsync(sessionId, ct)
        ?? throw new InvalidOperationException($"Unknown punchout session {sessionId}");

    var document = _builder.BuildOrderMessage(session);   // cXML PunchOutOrderMessage

    // Log before encoding. This is frequently the only record of what was sent.
    _logger.LogInformation(
        "Punchout return {SessionId} buyer={BuyerId} lines={LineCount} payload={Payload}",
        sessionId, session.BuyerId, session.Lines.Count, document);

    var encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(document));

    return View("PostBack", new PostBackModel
    {
        BrowserFormPostUrl = session.BrowserFormPostUrl,  // from the setup request
        CxmlBase64 = encoded
    });
}

On encoding

Send UTF-8 and declare it. Buyer names and addresses carry accented characters more often than test data suggests, and a mis-declared encoding produces a document that parses but renders as mojibake in the buyer's system — which reads to them as your system being broken.

Line-item metadata fails somewhere you can't see

Your cart can post back successfully and still cause a problem three steps downstream, because procurement systems validate and route on fields that look optional.

Commodity codes are the usual one. Many buyers categorise spend on UNSPSC, and a requisition without codes may be rejected or, worse, routed to the wrong approver and quietly stall. Units of measure are the other: procurement systems generally expect UN/CEFACT codes — EA, BX, CS — and a free-text unit like "each" or "box" can fail validation. Neither failure will look like your fault when it surfaces.

Get the buyer's expectations in writing during setup, including which fields they validate and what their tolerance is on price mismatches between the requisition and the eventual invoice.

Prices are per buyer, which is a data problem

Punchout customers are contract customers. Each one has negotiated pricing, and possibly a restricted catalogue — they should see their prices, and often only the subset of products their contract covers.

If your storefront was built for public retail pricing, this is the part of the project that costs real time. It is not a punchout problem exactly, but it lands in the punchout project, and it is worth being explicit about that when scoping rather than discovering it in week three.

The timeline problem nobody warns you about

Here is the thing that actually delays these projects.

You cannot meaningfully test alone. You need credentials for the buyer's test environment, and those come from their procurement or IT team, who have their own queue and no particular urgency about your deal. Joint testing needs someone on their side available at the same time as you. Some networks add a certification step of their own.

The build might be two weeks. The elapsed time to go-live is routinely two months, and almost all of the difference is waiting. Say this out loud at kickoff — to your own sales team especially — because the alternative is being blamed for a delay you have no lever over.

What to ask for on day one

Named technical contact on the buyer's side. Test environment credentials. Their cXML version and any documented deviations. Which line-item fields they validate. Whether they use the edit operation. Asking for all of it in the first email saves weeks of round trips, and it signals to the buyer that you have done this before.

Ariba and Coupa are not interchangeable

Both implement cXML. Both diverge from it, and from each other, in exactly the places that matter — how strictly line items are validated, how session timeout behaves, what the edit operation carries back, how errors are surfaced to the buyer.

Build one implementation with the buyer-specific behaviour pushed into configuration rather than branching logic scattered through the code. You will add a third network eventually, and the difference between a clean configuration point and a set of conditionals is whether that takes a week or a month.

What I'd cut first

Real-time inventory in the punchout catalogue. It sounds obviously correct and it introduces a dependency on your stock system into a flow the buyer is already halfway through. Approval takes days anyway, so the stock position at requisition time is not the one that matters. Handle availability when the order actually arrives.

Common questions

Is punchout the same as receiving orders?+

No, and assuming so is the most common scoping error. Punchout covers browsing and returning a cart. The actual purchase order arrives later as a separate cXML OrderRequest, usually after internal approval. They are two integrations with different timelines.

How long does a punchout integration take?+

The code is usually a couple of weeks. Getting test credentials from the buyer's procurement team, running joint testing and passing their certification is frequently longer than the build, and it isn't within your control.

Do Ariba and Coupa behave the same way?+

Both implement cXML and both diverge from it in places. Expect differences in how strictly line-item fields are validated, how the edit operation is handled, and what happens on session timeout. Build for one, test against each.

Do I need UNSPSC codes on every line?+

Practically, yes. Many procurement systems validate or categorise on commodity codes, and requisitions without them can fail downstream after leaving your system — which makes the failure hard to trace back to you.