How to use this

A hosted virtual supplier speaking cXML and OCI. Point your buyer-side punchout client at it, shop, and get a real cart back — cXML validated against the actual DTDs on the way through. The validator is open to everyone; running a punchout round trip needs one email for credentials. Nothing you send is stored beyond an hour.

1. Your endpoint

POST a PunchOutSetupRequest here. This is the only URL your system needs.

https://punchoutsandbox.com/punchout/setup
MethodPOST
Content-Typetext/xml
Credentials Put the identity from /signup in To/Credential/Identity and your secret in Sender/Credential/SharedSecret. Exchanging a shared secret out of band is how real punchout works, so this is faithful rather than fussy. Whatever arrives is echoed back in the response Status, so you can see exactly which field it landed in.
Required Only BrowserFormPost/URL. Without it there is nowhere to return the cart, so that one is refused up front rather than ten minutes into shopping.

2. What comes back

A PunchOutSetupResponse with a StartPage URL. Open it in a browser (or an iframe, as most buyer platforms do) and you are in the shop.

PunchOutSetupResponse
<Response>
  <Status code="200" text="OK">operation=create; from=YOURID; ...
      conformant=true; errors=0; advisories=1</Status>
  <PunchOutSetupResponse>
    <StartPage>
      <URL>https://punchoutsandbox.com/shop?session=…</URL>
    </StartPage>
  </PunchOutSetupResponse>
</Response>

Read the Status text. It reports which credentials arrived, whether your BuyerCookie was present, and how many conformance errors and advisories your request had. That is the fastest feedback loop here.

3. Shop, then return the cart

Add items, go to the cart, press Return cart. The sandbox renders a browser form that POSTs a PunchOutOrderMessage to your BrowserFormPost URL — which is how the protocol really works. It is not a server-to-server call, and if your endpoint depends on cookies or blocks framing, that is exactly where it breaks.

What you pressWhat your system receives
Return cart A PunchOutOrderMessage with your items. Total excludes tax and shipping, per the spec.
Return empty cart A message with no ItemIn. On an edit operation this instructs you to delete the requisition lines.
Return 204 Status code="204" — the user cancelled; change nothing.
Those last two are the sharpest thing to test. They look almost identical on the wire and mean opposite things — a supplier whose “user closed the tab” path sends an empty cart instead of a 204 silently wipes the buyer’s requisition.

What the status codes mean

Sessions last an hour. These are the states you will actually meet while testing, and what each one is telling you.

You getIt means
410 Gone
“That punchout session has ended”
The session expired, or that cart was already returned. Not a failure of the sandbox — start a new punchout from your buyer system. You do not need an account to be shown this.
409
“No punchout session”
You are signed in but have no live session: the StartPage URL was never followed, or the cart is already back.
200
“Sign up to continue”
No session and no account cookie. Your cookie jar is empty — check the StartPage GET actually stored one.
cXML Status code="401" Credentials not recognised. The HTTP status is still 200 — read the cXML, not the response code. A client checking only for HTTP 200 will never see this.

Two habits worth having: start each test run with a clean cookie jar, and remember a returned cart is single-use — returning it clears it, so a back-button resubmit cannot double your requisition. To test a second return, start a second punchout.

4. Try it without writing any code

Paste this into a terminal:

curl -s -X POST https://punchoutsandbox.com/punchout/setup \
  -H 'Content-Type: text/xml' \
  --data-binary @- <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE cXML SYSTEM "http://xml.cxml.org/schemas/cXML/1.2.071/cXML.dtd">
<cXML payloadID="demo@example.com" timestamp="2026-01-01T00:00:00+00:00">
 <Header>
  <From><Credential domain="NetworkID">
    <Identity>my-company</Identity></Credential></From>
  <To><Credential domain="NetworkID">
    <Identity>YOUR-IDENTITY</Identity></Credential></To>
  <Sender><Credential domain="NetworkID">
    <Identity>my-company</Identity>
    <SharedSecret>YOUR-SECRET</SharedSecret></Credential>
   <UserAgent>curl</UserAgent></Sender>
 </Header>
 <Request deploymentMode="test">
  <PunchOutSetupRequest operation="create">
   <BuyerCookie>my-session-123</BuyerCookie>
   <BrowserFormPost><URL>https://example.com/return</URL></BrowserFormPost>
  </PunchOutSetupRequest>
 </Request>
</cXML>
XML

Open the StartPage URL it returns. To watch the cart come back without building a receiver, point BrowserFormPost/URL at a request-capture service.

5. The catalogue lies on purpose

Eight products carry a quirk badge. Each reproduces a documented real-world failure, and each is labelled with what goes wrong and on which platform. Add one deliberately and watch what your system does with it.

QuirkWhat it breaks
Free-text unit (EACH) JAGGAER silently maps unknown units to EA; Coupa fails the import outright
Pack size in the unit (100/BX) Read as 100 boxes rather than a box of 100
Description over 256 chars Silently truncated on the PO, after passing validation
Aux ID over 100 chars Hard cart-return failure on JAGGAER; fine on Coupa
Non-ASCII description Mojibake, if you return the cart as cxml-urlencoded
Price with 5 decimals Rounding disagreement on the extended price
Punctuated UNSPSC Not categorised; spend analytics silently wrong
Delimiters in the part number Invoice matching fails later, not now

6. Using OCI instead of cXML

SAP SRM, ECC and S/4HANA speak OCI, not cXML. There is no handshake — point your external web service straight at this URL and the user's browser arrives with the return address attached.

https://punchoutsandbox.com/oci/setup
Required HOOK_URL — the return address. OCI has no other session mechanism, so this is the session.
Credentials USERNAME = your identity, PASSWORD = your secret, both from /signup.
Optional OCI_VERSION, returntarget, http_content_charset
MethodGET or POST — both work

In SRM: Define External Web Services, with row 1 as the URL above and a second row naming the return-URL parameter HOOK_URL.

why this matters more for OCI
OCI has no status mechanism at all. cXML can answer <Status code="406">; OCI can say nothing. If SRM dislikes a line it drops it and shows “Incomplete items in catalog”, with the detail buried in transaction SLG1 under log object BBP_OCI. A buyer integration can be losing lines on every cart and never see an error.
So this sandbox reports every truncation and normalisation it performs, because SAP will not.
OCI trapWhat happens
DESCRIPTION is 40 chars The tightest limit of any platform. Silently truncated; LONGTEXT is the escape hatch.
UNIT is 3 chars, ISO only EACH becomes EAC. SAP's internal ST means sheet in ISO, not Stück.
PRICEUNIT is a divisor PRICE=50, PRICEUNIT=5 means 10.00 each. Ignore it and you book a 5× overcharge. No cXML equivalent.
HOOK_URL must be split Its query parameters become hidden form fields; the action is the bare URL. POST the whole thing and SRM loses parameters.
Indices start at 1 A zero-based cart is ignored or truncated at the gap.
Default charset is ISO-8859-1 Not UTF-8 — the reverse of cXML.

FUNCTION=VALIDATE

SRM calls this to re-check a line it already holds — typically when a requisition is built from a template and the price may have moved. QUANTITY comes with the call, so scale pricing is resolved at that quantity:

https://punchoutsandbox.com/oci/setup?FUNCTION=VALIDATE&PRODUCTID=MSC-1001
    &QUANTITY=1     → NEW_ITEM-PRICE[1] = 4.85
    &QUANTITY=100   → NEW_ITEM-PRICE[1] = 3.60

A catalogue that ignores QUANTITY and returns list price is why a requisition rebuilt from a template silently loses its volume discount.

three rules, all easy to break
The response must contain no visible elements (hidden inputs only), it must auto-submit by JavaScript — there is no button, because the first rule forbids one — and if the product no longer exists it must return no data at all, not an empty form. An empty form would tell SRM the item is still valid and costs nothing.
BACKGROUND_SEARCH inverts the first two: visible results, no auto-submit. Same protocol, one letter apart in the parameter.

FUNCTION=BACKGROUND_SEARCH

Cross-catalog search. SRM runs this against every catalogue configured for it and merges the hits into one list. You are not being browsed — you are being scraped, once, from the DOM you return.

https://punchoutsandbox.com/oci/setup?FUNCTION=BACKGROUND_SEARCH
    &SEARCHSTRING=paper&HOOK_URL=…
the rules invert VALIDATE's
The form must not auto-submit — VALIDATE requires one, and doing the same here would add the first hit to the user's cart unbidden the moment a search ran. And every result must be reachable without scrolling, which in practice bans pagination, lazy loading and any script that builds results after load: SRM reads the DOM it is given, once. A “load more” button returns a catalogue that appears to hold one product.
Results are capped at 25 and the cap is stated on the page. A silent truncation reads as “that is all there is”, and a supplier returning hundreds crowds every other catalogue out of the merged list.

FUNCTION=DETAIL

A pure human drill-down — it returns the product page and no data. Needs PRODUCTID matching an EXT_PRODUCT_ID the catalogue issued; no HOOK_URL required, since nothing is returned.

7. Validate any document

/validate takes any cXML document and checks it against the real DTDs. It separates errors (the DTD says you are wrong — not a matter of opinion) from advisories (valid, and will still cause you trouble). Useful on its own, without a punchout session.

8. The order flow — PO, confirmation, ship notice, invoice

A punchout ends with a requisition, not an order. What happens next is where most integrations actually break, and where nobody can test: your buyer system turns the cart into a purchase order and sends it to the supplier, and the supplier sends back three documents that your system either ingests silently or drops silently.

Send us the order. POST an OrderRequest to:

https://punchoutsandbox.com/order

Same credentials as the punchout endpoint — your identity in To/Credential/Identity, your secret in Sender/Credential/SharedSecret. Everything that parses is accepted, however non-conformant; a document being wrong is the usual reason for sending it here.

The response puts everything we noticed in the Status text: line count, whether lineNumber was present, whether your header Total reconciles with the sum of the lines. That lands in your own transaction log, which is where you will actually look.

Then send documents back. On the order screen you can generate:

ConfirmationRequest Accept, reject, backorder, or accept-with-changes. The header type constrains which line statuses are legal and the DTD cannot express that rule — we check it before building, because a buyer rejects the combination with no useful explanation.
ShipNoticeRequest With carrier, tracking number and service level. Partial shipments supported; the same order line twice in one notice is refused, as the spec requires.
InvoiceDetailRequest Priced from the order, taxed in the ShipTo jurisdiction across 27 countries, with the reasoning shown rather than just a rate.

Each is validated against the real DTD and shown to you before it goes anywhere. Sending is a separate, deliberate step: set your cXML inbox in Settings and press send.

One attempt per press, no automatic retries. cXML tells suppliers to retry a transport failure hourly for ten hours. That is right for production and wrong here — you are trying to see the failure, and a retry that eventually succeeds hides the flakiness you came to find.

We will only POST to https on port 443, to a hostname whose every resolved address is public, connecting to the exact address we checked, following no redirects, with a valid certificate. "Take a URL from a stranger and POST to it" is the definition of server-side request forgery, and this sandbox runs inside someone else's AWS account. Settings lists the rules in full.

9. What survives ingestion — the other half of "is it valid"

/validate tells you whether a document is conformant. That is necessary and not sufficient, because cXML imposes almost no length limits — every limit that truncates your data is imposed by the buyer platform on ingestion, and none of them publish it. A perfectly valid cart can arrive with the wrong price on it.

/ingest answers that question instead. Paste a PunchOutOrderMessage (or use your current cart) and it applies what each platform is known to do on receipt:

SAP Ariba Accepts a long description and shows only the first 255 on the requisition and the PO. Limits are counted in bytes, so a Japanese description runs out around 666 characters. ? { } in a part ID are a hard reject.
JAGGAER An unrecognised UnitOfMeasure silently becomes EA. A box of 30 at £9.99 becomes 30 each at £9.99 and the buyer computes £299.70. Prices round to 4 decimal places.
Coupa Fails the cart import outright when the unit does not already exist in the buyer's configuration — loud rather than silent, which is the better failure to have.
Oracle Commonly configured to require EACH where everyone else says EA.
Safe target Every limit at the tightest value any platform imposes. A cart that survives this survives all of them.

Each finding is labelled with one of four outcomes: reject loudly, reject vaguely, accept and corrupt, accept and preserve. Only the third should frighten you — a rejection is a bad afternoon, a silent change is a wrong price nobody notices until reconciliation.

Where the numbers come from, and how sure we are. Vendor documentation, support notes and observed behaviour, gathered because no platform publishes a consolidated table — the full working is on the conformance reference. Rules we cannot fully evidence are marked unverified on the page rather than quietly asserted. If you know one of them is wrong, that is the single most useful thing you can tell us.

10. The machine path — if you are an agent, start here

Everything above assumes a browser somewhere. If you are a script, an agent, or CI, you never need one. Nothing below requires a form, a cookie or a human.

Get credentials.

curl -sX POST https://punchoutsandbox.com/api/signup -d '{"email":"you@company.com"}'

Returns your identity, shared secret and a map of where each one goes. No password, no confirmation email, no browser.

Authenticate. The same credentials, in headers — or HTTP Basic, with the identity as the username:

-H "X-Sandbox-Identity: PSB…" -H "X-Sandbox-Secret: …"

Validate, and get the findings as data.

curl -sX POST https://punchoutsandbox.com/api/validate $AUTH --data-binary @order.xml
POST /api/signup Issues credentials. 201.
POST /api/validate The full conformance report as JSON — every error with its line number, element and hint, not just a count. Metered against your account, not your IP address.
POST /api/ingest What each buyer platform would do to a cart, as JSON.

Send the document as the raw body or as {"document": "<cXML…"} — both work. Refusals are honest HTTP: 401 unauthenticated, 422 refused before parsing, 429 quota. That is deliberately unlike the cXML endpoints, which answer 200 with a cXML Status because the spec makes any other reply a transport error that clients retry for ten hours.

The punchout round trip needs no browser either. POST a PunchOutSetupRequest, follow the StartPage URL with a cookie jar, add to the cart, POST to /cart/return, and read the cart out of the cxml-base64 field. A live punchout session is authorisation in itself — the shopper is your employee and you already authenticated for them, so the storefront never asks them to sign up.

11. Worked examples of every document

/samples serves one valid example of each message this sandbox speaks — PunchOutSetupRequest, PunchOutOrderMessage, ConfirmationRequest, ShipNoticeRequest, InvoiceDetailRequest.

curl https://punchoutsandbox.com/samples/shipnoticerequest

Each is generated by the same builder that produces the real thing, so it cannot drift out of date, and the test suite asserts every one validates against the real DTD.

Worth pulling the ship notice specifically if you are writing an extractor: ItemID is optional there and UnitOfMeasure is not — the reverse of every other item block in cXML, and the usual reason a parser written from the other documents fails on that one. ShipControl is also a sibling of ShipNoticePortion, not inside it: one shipment can cover several orders, so carrier and tracking sit at the request level.

And then the ones designed to break your parser. Each of the samples above agrees with itself — the ship notice despatches exactly what was ordered, in the same unit, at the same price — so an extractor that ignores UnitOfMeasure entirely passes all of them. These do not:

shipnoticerequest-unit-change Ordered 1 BX, despatched 30 EA. An extractor with no unit field reads 30 and believes 30 boxes arrived.
shipnoticerequest-partial One of three, fulfillmentType="partial". Closes an order that is two thirds outstanding.
confirmationrequest-price-change Accepted at a higher price and a later date. Discovered by the invoice if you assume confirmations are acceptances.
confirmationrequest-backordered Confirmed, and nothing is coming for eight weeks.
invoicedetailrequest-split-line One PO line invoiced as two. One-to-one matching drops a line or double-counts one.
punchoutordermessage-quirks Every catalogue quirk at once: free-text unit, pack size in the unit, over-length description and auxiliary id, delimiters in a part number, non-ASCII text, sub-penny price, punctuated UNSPSC.

All of them are conformant cXML that a real supplier really sends. The point is not malformed input — the validator covers that — but well-formed input carrying a fact your code may be assuming away.

Limits, honestly

SessionsExpire after 1 hour. Nothing is kept.
OrdersKept 7 days. A session is someone shopping; an order is a flow you work through over days.
Daily limit25 validations without an account, 500 with one. If you hit either legitimately, say so — it is a number, not a policy.
CredentialsNot enforced. Anything is accepted and echoed back.
OCI functions DETAIL, VALIDATE and BACKGROUND_SEARCH are implemented. SOURCING is not — SAP itself has never implemented it.
Companies Every supplier, brand and document here is invented. Generated documents are test artifacts and must not be used as anything else.
Tax rates Illustrative, compiled 2026-08-14. Do not use them as a tax reference.

Found something wrong, or need a case this does not cover? That is worth knowing — the source is here.