cXML punchout orders
Send purchase orders from Ariba, Coupa, JAGGAER, Oracle or Workday into SpendOne as cXML
OrderRequest documents, and receive order confirmations and supplier invoices back.
This page is written for the administrator of the buying organization's procurement platform. It is everything you need to configure SpendOne as a cXML supplier: the endpoint, the credential, the response contract, the fields we read, and a worked example you can copy. No SpendOne login is needed to follow it.
Direction, before anything else
SpendOne is the supplier side of this integration. Your platform is shopped into, not out from:
- Your user raises and approves a requisition in your own system, under your own rules.
- Your platform POSTs the resulting
OrderRequestto SpendOne. - SpendOne executes it: the order becomes a purchase request, already approved, with a virtual payment card behind it.
- SpendOne confirms the outcome back to your endpoint, and can relay the supplier's invoice to you.
Most punchout material you will find describes the opposite arrangement, where a buyer
punches out to a supplier's hosted catalog. SpendOne serves no catalog session. A
PunchOutSetupRequest sent to the endpoint below is answered 450 Not Implemented; the
only document this endpoint accepts is OrderRequest.
What you need from SpendOne first
Your SpendOne counterpart creates a punchout connection in their SpendOne administration area and hands you these:
| What | Example | Notes |
|---|---|---|
| Endpoint URL | https://api.spendone.tech/punchout/v1/tenant_acme/cxml | The tenant id is part of the path and is fixed per customer |
| Shared secret | so_px_... | Generated by SpendOne, shown once, never retrievable afterwards. Not on SAP Business Network, where the network presents SpendOne's own secret (see below) |
| The identity we expect from you | AN01000000123 | Goes in your From/Credential/Identity |
| The identity you address us as | SPENDONE-ACME | Goes in your To/Credential/Identity |
| The cost center extrinsic name | CostCenter | The header Extrinsic name your platform carries the cost center in |
| The cost center segment name | CostCenter | The AccountingSegment type your order lines carry the cost center in |
The last two are configuration on our side, not constants: Ariba and Coupa do not agree on where the cost center goes or what it is called, and one customer may run both platforms at once. Tell your SpendOne counterpart which one your platform emits, and under which name, and they set it on the connection.
The endpoint
POST https://api.spendone.tech/punchout/v1/{tenant_id}/cxml
Content-Type: text/xml; charset=utf-8
| Method | POST only |
| Transport | HTTPS. There is no HTTP listener |
| Body | One cXML document, UTF-8 |
| Body ceiling | 5 MB. A larger body is rejected before parsing |
| Content type | Not enforced. Send text/xml; charset=utf-8 or application/xml |
| Compression | Not supported. Send the document uncompressed |
| Rate limit | Burst of 20 deliveries per source IP per tenant, then one every 30 seconds |
| Authentication | The shared secret inside the document. No HTTP header, no mTLS, no OAuth |
The rate limit is sized far above a real integration, which delivers a handful of orders
an hour. A throttled delivery is answered with the same 401 an authentication failure
gets, so retry with backoff rather than treating it as a credential problem.
The endpoint is deliberately absent from the API reference:
it is served outside /api/v1, it carries no bearer token, and it answers cXML rather
than the JSON error format the rest of the API uses. This page is its documentation.
Authentication
The credential travels inside the document, in Header/Sender/Credential/SharedSecret:
<Sender>
<Credential domain="NetworkID">
<Identity>AN01000000123</Identity>
<SharedSecret>so_px_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</SharedSecret>
</Credential>
<UserAgent>Ariba Buyer 9r1</UserAgent>
</Sender>
Three things are checked, and all three have to hold:
- The shared secret resolves to a live credential. SpendOne generates it (every
SpendOne punchout secret starts with
so_px_), shows it to your SpendOne counterpart exactly once at creation, and stores only a hash. If it is lost, a new one is minted and the old one revoked; it cannot be read back. The one exception is SAP Business Network, where the secret is the one on SpendOne's own supplier account and SpendOne enrolls it on your connection (see the SAP Business Network section). - The tenant id in the path is a tenant the credential is enrolled on. One secret can be enrolled on several tenants, which is how one SAP Business Network secret serves every SpendOne customer, but a delivery to a tenant it is not enrolled on is refused exactly like an unknown secret.
From/Credential/Identitymatches the buyer identity configured on the connection. It is compared case-insensitively, with leading and trailing whitespace ignored.
:::warning The trailing-whitespace trap
Paste the secret with no surrounding whitespace, and check what your platform stored.
A secret copied out of a PDF, an email or a spreadsheet routinely carries a trailing
newline or a non-breaking space, which is invisible in every configuration screen and
authenticates as a completely different secret. This is the single most common failure in
this integration family. If your first live delivery comes back 401 and everything looks
right, re-enter the secret by hand before opening a ticket.
:::
To/Credential/Identity is the identity you were given for SpendOne. Send it as agreed at
onboarding, alongside its @domain.
The answer contract
Every response is HTTP 200. The outcome is in the cXML Status element, never in the
transport status code. A buyer platform that branches on the HTTP status will read every
rejection as a success.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE cXML SYSTEM "http://xml.cxml.org/schemas/cXML/1.2.071/cXML.dtd">
<cXML payloadID="01K6...@spendone.com" timestamp="2026-09-22T07:41:08Z">
<Response>
<Status code="200" text="OK"></Status>
</Response>
</cXML>
This is the whole vocabulary SpendOne can emit on this endpoint:
Status/@code | @text | Body | What it means | What to do |
|---|---|---|---|---|
200 | OK | empty | Accepted. The order is queued for conversion into a purchase request | Nothing. Do not redeliver |
200 | OK | duplicate | This exact payloadID already arrived. Nothing was created twice | Nothing. Your retry was safe |
400 | Bad Request | empty | The document could not be parsed or could not be accepted for this connection | Fix the document or the configuration and send again. See below |
401 | Unauthorized | empty | Authentication failed, or the delivery was rate limited | Check the secret, the tenant id, the From identity and the connection's state |
450 | Not Implemented | empty | The document is well formed but is not an OrderRequest | Send an OrderRequest. No other document type is accepted here |
500 | Internal Server Error | empty | SpendOne could not handle the delivery at all: a backend failure, not a problem with your document | Retry with backoff. Raise it with SpendOne if it persists |
401 is deliberately undifferentiated. A wrong secret, a revoked secret, an unknown
tenant, a tenant that does not match the credential, an identity mismatch, an inactive
connection and a rate-limited delivery all produce one byte-identical response, after a
random delay. That is a security property, not a gap in the documentation: telling those
cases apart would turn the endpoint into an enumeration oracle. Do not spend a day trying
to distinguish "wrong secret" from "wrong tenant" from the wire. Ask your SpendOne
counterpart, who can see which one it was.
Two classes of 400 exist, and they behave differently on our side:
- Before authentication, a document that is not well formed XML, that is missing a required field, or that fails one of the validity rules below. Nothing is stored.
- After authentication, an order the customer's own configuration will not take: the nominated contact resolves to nobody who can own an order, or the connection names a legal entity that can no longer be booked against. The document is stored and is visible to the customer's SpendOne administrator with the offending value, so they can fix it.
What makes a document malformed
Any of these is answered 400, before any credential work:
- Not well formed XML.
- Missing
payloadIDoncXML, or missingorderIDonOrderRequestHeader. - No
ItemOutelements at all. Totalmissing, empty, zero or negative.- A negative unit price, a quantity that is not a positive number, a
lineNumberthat is not a positive integer, or the samelineNumberon two lines. - A line currency that differs from the order currency, or a currency code that is not three ASCII letters.
- A negative tax amount, a tax currency that differs from the order currency, or a
TaxDetailwith nocategory. - A number with more than six fractional digits or more than 15 significant digits.
What SpendOne reads out of your OrderRequest
| Element | How it is used |
|---|---|
cXML/@payloadID | Wire-level idempotency key. A repeat is answered 200 OK / duplicate |
OrderRequestHeader/@orderID | Your order number. It is the business key, and it is what appears on everything we send back |
OrderRequestHeader/@type | What the document is: new (the default), update (a change order), or delete (a cancel order). See Idempotency, retries, change orders and cancel orders |
OrderRequestHeader/@orderVersion | Your revision counter for the document, carried onto the record when an order is held or canceled. Not compared: the order id and its content decide what a delivery revises |
OrderRequestHeader/DocumentReference/@payloadID | The delivery your change or cancel order supersedes, carried onto the record beside the version. Not a resolution key |
OrderRequestHeader/@orderDate | Carried onto the confirmation we send you |
OrderRequestHeader/Total/Money | The payable. It funds the virtual card, whatever the tax treatment |
OrderRequestHeader/Tax, TaxDetail@percentageRate | The VAT rate, and the net-versus-gross decision (below) |
OrderRequestHeader/Contact | Who the order belongs to. role="endUser" wins, then role="buyer"; the first non-empty Email is matched against the customer's SpendOne users |
OrderRequestHeader/Extrinsic | The cost center, under the extrinsic name configured on the connection. The value is matched case-insensitively and trimmed. Read first |
ItemOut/Distribution/Accounting/AccountingSegment | The cost center, when the header extrinsic names none of the owner's cost centers: the segment whose Name (or Segment/@type on the deprecated form) is the segment name configured on the connection, its @id being the cost center code. One cost center per order, so the first such segment in document order wins. This is the Ariba shape |
OrderRequestHeader/ShipTo | Rendered to a one-line delivery address. Read leniently, never a reason to reject |
ItemOut/@quantity, @lineNumber | One purchase-request line per ItemOut. A missing lineNumber falls back to the element's position |
ItemOut/ItemID/SupplierPartID | Kept on the line, and compared when a revised order arrives |
ItemOut/ItemDetail/UnitPrice/Money | Line unit price. Line amount is unit price times quantity |
ItemOut/ItemDetail/Description | The line's name in SpendOne |
ItemOut/ItemDetail/UnitOfMeasure | Carried onto the relayed invoice |
ItemOut/SupplierID, ItemDetail/ManufacturerName | The merchant named on the virtual card. First SupplierID across the lines wins, then the first ManufacturerName |
Header/From, Header/Sender | Authentication, as described above |
Deliberately not read: BillTo (which legal entity the spend books to is connection
configuration, not document data), Request@deploymentMode,
Comments, Shipping, Classification, and every Distribution segment other than the
configured cost center one. Sending them is harmless.
@type is validated, not ignored: a value outside the DTD's new | update | delete
vocabulary (or one spelled differently, DELETE included) is answered 400 as a
malformed document, because that attribute decides whether money moves and guessing
new for an unreadable value would swallow exactly the corrections the attribute exists
to carry.
Net or gross is derived from the arithmetic
cXML carries no tax-inclusive flag on Total, so SpendOne decides from the figures, with a
one-cent tolerance for your own per-line rounding:
Totalequals the sum of the lines plus the stated tax: the line prices are net andTotalis tax inclusive. This is the classic German B2B shape, and the one in the worked example below.Totalequals the sum of the lines: the prices already include the tax, or carry none.- Neither matches (shipping folded into
Total, or inconsistent figures): the order is booked gross, at the document's own rate if it stated one.
A document that states no tax at all is accepted and booked at the default VAT rate configured on the connection, with the assumption recorded on the purchase request. If your orders never carry tax, agree that rate at onboarding.
Three things that stop an order, and one that does not
SpendOne never bounces an order over metadata we would have had to guess at. A cost center
we cannot match, a VAT rate we cannot read and a delivery address we cannot parse all fall
back, because you already approved the order on your side and we already answered 200.
Three answers are yours or the customer's, and those do stop the order:
- The nominated contact does not resolve to a user who can own the order. The email
must match a SpendOne user, that user must be active, and they must hold at least one
cost center. Answered
400at delivery. - The connection names a legal entity that no longer exists or cannot be booked.
Answered
400at delivery. This is customer-side configuration; there is nothing to change in your document. - The order names a supplier the customer has blocked. This one is answered
200, because the block is discovered during conversion, after the acknowledgement. The order does not become a purchase request, and the customer's administrator sees it in their punchout document list. The customer unblocks the supplier, and you resend.
Idempotency, retries, change orders and cancel orders
Two separate keys protect you, and they cover different mistakes:
- The same
payloadIDtwice (a plain retry): answered200 OKwith bodyduplicate. Nothing is stored twice. - The same
orderIDtwice under differentpayloadIDs (your platform minted a fresh envelope for the retry): SpendOne compares a content digest of the two documents. Identical content converges onto the purchase request already created, and the delivery is recorded against it. One order, one purchase request.
The digest covers currency, Total, the tax figures, the line count, and each line's part
id, quantity and unit price. It ignores payloadID, the timestamp, the credentials, the
contacts, the extrinsics and ShipTo, precisely so that an ordinary retry is not mistaken
for a change.
What a document declares itself to be outranks what its content digests to. Ariba routes
New, Changed and Cancelled orders as three separately configured methods, and all three
keep the original orderID: Ariba versions the document, not the number.
type="new"(or notypeat all) is an order. Identical content under the sameorderIDconverges as a retry; differing content is held as an undeclared revision (below).type="update"is a change order. When theorderIDmatches an order SpendOne already converted, it is never applied automatically and never swallowed as a retry: identical content or not, SpendOne records it, answers200, and leaves the live purchase request untouched. (The one exception: an update whoseorderIDmatches nothing already converted becomes a new order, because the buyer's latest stated content is the order and there is nothing to revise.) The record names it a change order with yourorderVersionand the supersededpayloadID, and the customer's SpendOne administrator resolves it: applying the revision means canceling and deleting the existing purchase request and reprocessing the held document by hand.type="delete"is a cancel order. It does not create anything and it is not recorded as a retry: SpendOne cancels the purchase request theorderIDresolves to. The order, the payment card behind it and the buyer's request all close, and a reject confirmation is sent to your endpoint if confirmations are enabled. Two special cases, both deliberate:- The
orderIDmatches no order SpendOne converted (it was refused at delivery, or it never arrived): answered200and recorded. There is nothing the buyer could fix, so SpendOne does not pretend otherwise. - The order was already placed with the supplier (you canceled after
ConfirmationRequest type="accept"was sent): the cancel is held for the customer's operator rather than executed, because a cancellation cannot undo a placed order and the payment card may already have been charged. Your platform sees its200either way; resolve the discrepancy with the customer out of band. Cancel before placing whenever you can.
- The
:::tip Change orders are held, not applied
If your platform sends change orders under the original orderID, coordinate with your
SpendOne counterpart: applying the revision means canceling and deleting the existing
purchase request and reprocessing the held document by hand. A new orderID is a new
order and needs no intervention. Note that a correction to the delivery address alone is
not visible to the digest and is treated as an ordinary retry on a type="new" delivery.
:::
What SpendOne sends back to you
Documents flow the other way over the same protocol, POSTed to an endpoint you provide. This half is optional and is enabled per connection.
| Document | When | Notes |
|---|---|---|
ConfirmationRequest, type="accept" | When the order is actually placed with the supplier | Not at receipt: the synchronous Status already acknowledged that |
ConfirmationRequest, type="reject" | When the order is canceled, or a non-delivery is resolved | operation="update" when an accept already went out, naming it in a DocumentReference |
InvoiceDetailRequest | When the supplier's invoice is verified and handed over to accounting | One document per punchout order, carrying that order's lines. Optional, enabled separately |
No ShipNoticeRequest is sent. There is no per-line acknowledgement either: a cXML
ConfirmationRequest from SpendOne is always whole-order, because a SpendOne purchase
order is accepted or not as a whole.
A rejection always carries a buyer-readable sentence in ConfirmationHeader/Comments, from
a closed vocabulary rather than free text, so it is safe to display in your procurement UI:
| Reason | Sentence |
|---|---|
supplier_unavailable | The supplier cannot fulfill this order. |
item_discontinued | The item has been discontinued. |
price_changed | The price has changed since the order was placed. |
lead_time_unacceptable | The lead time is not acceptable. |
duplicate_order | This order duplicates an existing order. |
requested_by_buyer | Canceled at the buyer's request. |
supplier_blocked | The supplier is blocked by company policy. |
| (default) | Order could not be fulfilled. |
Every outbound document carries Request/@deploymentMode, set from the connection
(test or production) rather than derived from which SpendOne environment sent it. That
matters during onboarding, when a customer's production realm frequently points at a
SpendOne test environment for a while.
What your receiving endpoint has to do
- Be HTTPS, on a publicly resolvable address. Private and link-local addresses are refused, at configuration time and again at dial time.
- Answer a cXML
Status. We read your cXML status in preference to your HTTP status wherever one parses. - Deduplicate on
payloadID. A redelivery repeats the originalpayloadIDand the original bytes on purpose, so your side can recognize it as the same document.
How SpendOne treats your answer:
| Your answer | SpendOne |
|---|---|
cXML 2xx | Done |
cXML 4xx, or HTTP 4xx | Terminal. No retry. A SpendOne operator has to redeliver it |
HTTP 5xx, 429, 408, or a transport failure | Retried with backoff. Retry-After is honored when it is under 15 minutes |
HTTP 2xx with a body we cannot parse | Terminal, same as a 4xx |
A 401 from your side is terminal even though it is the most likely thing to be fixed
within minutes. The credential is inside the document, so retrying through a rotation would
turn every rotation into a burst of failures.
:::note Supplier identity on a relayed invoice
On an InvoiceDetailRequest, cXML's From is the invoicing party, which is the real
supplier, not SpendOne. Some buyer networks refuse a document whose invoicing party is not
the vendor on the purchase order. That is a relationship or AP configuration question on
your network, not a defect on either side. The usual answer is to run the connection with
confirmations only until the relationship is established; the supplier's invoice continues
to reach you the way it does today.
:::
If your platform is SAP Business Network (Ariba)
Everything above is generic cXML. On SAP Business Network the network sits between your procurement system and SpendOne, and that changes who configures what.
Your system sends the order to the network, addressed to SpendOne's supplier account. The
network authenticates you and then POSTs the order to SpendOne itself, rewriting the
credentials on the way: From stays your ANID, To is SpendOne's ANID, and Sender
becomes the network (AN01000000001), carrying the shared secret of SpendOne's
supplier account in place of yours. So on Ariba you never handle a SpendOne secret or
endpoint URL. Where orders go, and how they authenticate, is configured on SpendOne's
supplier account, by SpendOne.
What you do
-
Relate to SpendOne as a supplier in your network account, the way your realm onboards any supplier, under the SpendOne ANID your counterpart gives you.
-
Hand your counterpart your ANID (the
AN0100...value of your account) early: they need it to create the connection that expects you. It must be an ANID; the connection refuses anything else on an Ariba platform. -
Say where your orders carry the cost center. Ariba books accounting per line, so the cost center usually travels as an accounting segment on each
ItemOut, and there is no headerExtrinsicat all. Tell your counterpart the segment name your realm uses:<ItemOut quantity="2" lineNumber="1">...<Distribution><Accounting name="DistributionCharge"><AccountingSegment id="CC-4001"><Name xml:lang="en">CostCenter</Name><Description xml:lang="en">Berlin depot</Description></AccountingSegment></Accounting><Charge><Money currency="EUR">200.00</Money></Charge></Distribution></ItemOut>Here the segment name is
CostCenterand the cost center isCC-4001. The name is matched exactly, theidcase-insensitively. -
Agree the VAT rate your orders do not carry (below).
-
Name both realms. Test and production are different accounts (below).
The VAT rate your orders do not carry
Ariba purchase orders frequently state no Tax element at all. SpendOne
accepts such an order and books it at the connection's default VAT rate (see
Net or gross is derived from the arithmetic).
What that rate should be depends on how your realm prices: a realm that
prices net needs a rate that is not in the order total, and there is no value
that is right for both. Agree the rate with your SpendOne counterpart at
onboarding. They are asked to state it explicitly when the connection is
created and cannot leave it at a default, precisely because the answer is
yours. If your orders do carry tax, nothing of this applies and the figures
in the document win.
Test and production are two different accounts
Your SAP Business Network test account has its own ANID; nothing of its configuration carries over to production. The ANID your SpendOne connection expects is realm-specific, so a cutover from test to production is a second SpendOne connection or an edit of the existing one's identity, never just a URL change. Tell your counterpart both ANIDs, and expect to prove the integration once per realm.
What SpendOne configures on its supplier account
None of these is yours to set. They are listed so that you can recognize them when a delivery fails, and so that you know what to ask your counterpart about.
- An enterprise account. SAP Business Network excludes cXML and EDI integration from a Standard supplier account, so cXML order routing needs an enterprise one.
- All three order types routed. The network routes new orders, change orders and cancellations as three separate routing methods. All three point at SpendOne, with a customer-specific route to your tenant's endpoint URL. With only the first routed, the first cancellation would never arrive and SpendOne would keep a purchase request you withdrew.
- Shared-secret authentication on every routing method. With the
certificate method the network authenticates by client TLS and sends no
SharedSecretat all, which the SpendOne endpoint answers401. - Both attachment settings off: Leave order attachments online and do not include them in the cXML order, and Include PDF document.
- No PunchOut profile URL. SpendOne serves no catalog session. Left
empty, the network falls back to the order-routing URL; filled in, it sends
a
ProfileRequest, which is answered450, and caches that answer for 24 hours. - NetworkID identities on both sides, in both directions. A document
naming you under DUNS or another domain the connection does not expect is
answered
401. - The return path. Order confirmations, and supplier invoices if that is
enabled on your connection, are posted into the network at
https://service.ariba.com/service/transaction/cxml.aspunder SpendOne's ANID, and reach you from there.
:::warning The attachment settings refuse every order
With either attachment setting enabled, the network posts the order as a
multipart/related message: the cXML document with the attachment, or a PDF
copy of it, packaged alongside. SpendOne parses the request body as one XML
document, so a multipart message fails to parse and the order is answered
400 before anything else about it is looked at. Every order, not only
orders that carry attachments. From your side nothing looks wrong: your
platform shows the order as transmitted, and it is refused all the same. If
every order on an otherwise working setup comes back refused, this is the
first thing to ask your counterpart about.
:::
Two answers to questions buyers ask
Every document SpendOne sends declares its cXML version. All documents
SpendOne posts, the synchronous Status answer to your orders included, open
with the XML declaration and a DOCTYPE naming cXML 1.2.071. The network
reads the version of a document from that declaration, so a document without
one gives it nothing to read. The same tolerance holds in the other
direction: your documents may carry their own DOCTYPE declaration, and it
is read as version metadata, never fetched.
The rate limit is not relaxed for authenticated connections. The
endpoint's rate limit (a burst of 20, then one delivery every 30 seconds) is
sized far above what the network posts for a live integration, so you will
not meet it. It is deliberately not exempted per connection either: a
throttled delivery is answered with the same 401 a wrong secret gets, the
network cannot tell the two apart, it retries hourly and gives up after ten
attempts, and the order ends Failed on the network. The burst sits far
above anything the network sends, which is the deliberate answer: the
ceiling is never met, and keeping every refusal indistinguishable keeps the
endpoint from becoming an oracle (see the authentication section).
Onboarding sequence
On SAP Business Network, steps 2 to 4 and 6 are replaced by what the SAP Business Network section above lists: SpendOne enrolls its network secret on the connection and configures the routing and the return path on its own supplier account, and your part is the supplier relationship, your ANIDs, the cost center segment and the VAT rate.
- The customer creates the connection in SpendOne, under Administration, Punchout. It names your platform, the identity pair, the legal entity the spend books to, where the cost center comes from and the default VAT rate. On an Ariba connection the rate must be stated explicitly and the identity must be your ANID; the connection API refuses anything else.
- The customer mints a credential on that connection. The secret is displayed once.
- The customer hands you the endpoint URL and the secret, over a channel that does not add whitespace. The endpoint URL is shown on the connection page, ready to copy.
- You configure SpendOne as a supplier in your procurement platform: the endpoint as
the order routing destination, the secret as the shared secret, your identity in
From, SpendOne's inTo. - You send one test order. Start in your test realm with a small, single-line order
and a contact whose email exists in the customer's SpendOne directory. Expect
200 OK. The customer can see the received document in their punchout document list straight away. - For the return direction, give the customer your receiving endpoint URL and a shared
secret of your own. They configure it on the same connection and can trigger a test
delivery from SpendOne, which reports your cXML
Statusback verbatim. - Go live: switch the connection's deployment mode to
productionwhen both directions have been proven.
A worked OrderRequest
Net-priced lines with the VAT stated on top, which is the most common European shape.
The lines sum to 250.00, the stated tax is 47.50, and Total is the tax-inclusive 297.50.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE cXML SYSTEM "http://xml.cxml.org/schemas/cXML/1.2.071/cXML.dtd">
<cXML payloadID="ord-net-1@buyer.example.com" timestamp="2026-09-20T09:00:00+02:00">
<Header>
<From><Credential domain="NetworkID"><Identity>AN01000000123</Identity></Credential></From>
<To><Credential domain="NetworkID"><Identity>SPENDONE-ACME</Identity></Credential></To>
<Sender><Credential domain="NetworkID"><Identity>AN01000000123</Identity>
<SharedSecret>so_px_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</SharedSecret></Credential>
<UserAgent>Ariba Buyer 9r1</UserAgent></Sender>
</Header>
<Request deploymentMode="test">
<OrderRequest>
<OrderRequestHeader orderID="PO-9002" orderDate="2026-09-20T09:00:00+02:00" type="new">
<Total><Money currency="EUR">297.50</Money></Total>
<ShipTo><Address isoCountryCode="DE"><Name xml:lang="en">Berlin Depot</Name>
<PostalAddress><Street>1 Alexanderplatz</Street><City>Berlin</City>
<PostalCode>10178</PostalCode>
<Country isoCountryCode="DE">Germany</Country></PostalAddress>
</Address></ShipTo>
<BillTo><Address isoCountryCode="DE"><Name xml:lang="en">Northgate GmbH</Name>
<PostalAddress><Street>1 Alexanderplatz</Street><City>Berlin</City>
<Country isoCountryCode="DE">Germany</Country></PostalAddress>
</Address></BillTo>
<Contact role="endUser"><Name xml:lang="en">Dana Weber</Name>
<Email>dana.weber@northgate.example</Email></Contact>
<Tax>
<Money currency="EUR">47.50</Money>
<Description xml:lang="de">Umsatzsteuer 19%</Description>
<TaxDetail category="vat" purpose="tax" percentageRate="19">
<TaxableAmount><Money currency="EUR">250.00</Money></TaxableAmount>
<TaxAmount><Money currency="EUR">47.50</Money></TaxAmount>
</TaxDetail>
</Tax>
<Extrinsic name="CostCenter">CC-4001</Extrinsic>
</OrderRequestHeader>
<ItemOut quantity="2" lineNumber="1">
<ItemID><SupplierPartID>MSC-1001</SupplierPartID></ItemID>
<ItemDetail>
<UnitPrice><Money currency="EUR">100.00</Money></UnitPrice>
<Description xml:lang="en">Kestrel A4 copier paper, 80gsm</Description>
<UnitOfMeasure>BX</UnitOfMeasure>
<Classification domain="UNSPSC">14111507</Classification>
</ItemDetail>
</ItemOut>
<ItemOut quantity="1" lineNumber="2">
<ItemID><SupplierPartID>MSC-2002</SupplierPartID></ItemID>
<ItemDetail>
<UnitPrice><Money currency="EUR">50.00</Money></UnitPrice>
<Description xml:lang="en">Vantor gel pen, black</Description>
<UnitOfMeasure>EA</UnitOfMeasure>
<Classification domain="UNSPSC">44121704</Classification>
</ItemDetail>
</ItemOut>
</OrderRequest>
</Request>
</cXML>
Sent with:
curl -i -X POST \
-H 'Content-Type: text/xml; charset=utf-8' \
--data-binary @order.xml \
https://api.spendone.tech/punchout/v1/tenant_acme/cxml
The answer, HTTP 200:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE cXML SYSTEM "http://xml.cxml.org/schemas/cXML/1.2.071/cXML.dtd">
<cXML payloadID="01K6ZPQ8F7N2R4V6X8Z0B2D4F6@spendone.com" timestamp="2026-09-20T07:00:02Z">
<Response>
<Status code="200" text="OK"></Status>
</Response>
</cXML>
Troubleshooting
| Symptom | Where to look |
|---|---|
401 on the first live delivery | Whitespace around the secret, first. Then the tenant id in the path, then From/Credential/Identity against what the connection has, then whether the connection is still active |
401 after months of working traffic | The credential was revoked or rotated, or the connection was deactivated. Ask for a fresh secret |
401 in bursts under load | The rate limit. Back off and retry; a throttled delivery looks like an authentication failure by design |
450 | Something other than an OrderRequest, usually a PunchOutSetupRequest, or a Request element naming a document SpendOne does not read. SpendOne serves no catalog session |
400 on every document | Work through the malformed list above. orderID, payloadID, a zero Total and a missing ItemOut are the frequent ones |
400 on every document, on SAP Business Network, with everything configured right | The attachment settings on SpendOne's supplier account. Leave order attachments online or Include PDF document makes the network post multipart/related instead of plain XML, and the order fails to parse before anything else about it is read. Ask your SpendOne counterpart to check both are off (see the SAP Business Network section) |
400 on some documents only | Usually the contact: the email on the order does not match an active SpendOne user who holds a cost center. Your SpendOne counterpart can see the address we could not resolve |
200 OK, but no purchase request appears | A blocked supplier, or a change order under an existing orderID. Both are visible to the customer's SpendOne administrator with the reason |
400 on change and cancel orders only | The @type value is outside the DTD's new | update | delete vocabulary, or two DocumentReference elements are present. SpendOne validates @type rather than guessing, because it decides whether money moves |
| Orders land on the wrong cost center | The extrinsic name or the segment name configured on the connection does not match what your platform emits, or the value matched none of the owner's cost centers. Both names are matched exactly. SpendOne falls back rather than rejecting, so this is silent by design |
| Confirmations never arrive | Confirmations are enabled per connection and are sent at order placement, not at receipt. Ask the customer to run a test delivery, which reports your endpoint's answer verbatim |
Anything this page does not answer: tech@spendone.com.