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
| Method | POST |
|---|---|
| Content-Type | text/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.
<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 press | What 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. |
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 get | It 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.
| Quirk | What 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 |
| Method | GET 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.
<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.
| OCI trap | What 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.
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=…
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:
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.
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:
Validate, and get the findings as data.
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.
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
| Sessions | Expire after 1 hour. Nothing is kept. |
|---|---|
| Orders | Kept 7 days. A session is someone shopping; an order is a flow you work through over days. |
| Daily limit | 25 validations without an account, 500 with one. If you hit either legitimately, say so — it is a number, not a policy. |
| Credentials | Not 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.