After the Buy Button: What UCP and ACP Don't Specify (and Your ERP Has To)
Both big agentic checkout protocols now have an order object. Google and Shopify’s Universal Commerce Protocol (UCP) has had one since launch and the 2026-08-25 release added attribution and a policies[] snapshot to it. OpenAI and Stripe’s Agentic Commerce Protocol (ACP) added “native orders” in 2026-04-17, alongside cart, feeds, and an MCP binding. It is tempting to read that and assume the post-purchase side is now standardized.
It is not, and the specs are fairly honest about it. This post reads both specs field by field against what a back office actually needs: create a sales order in an ERP like NetSuite or Business Central, release it to a 3PL, and reconcile it against the payment processor at month end. I read the JSON Schemas at the UCP v2026-08-25 tag and the spec/2026-04-17 folder of the ACP repo. Where I give an opinion rather than a reading of the spec, I label it.
First, which way does the order flow?
This is the part people miss. In both protocols the order object is something the merchant produces and pushes to the agent platform, not something the platform hands to the merchant.
- UCP: “Businesses push order lifecycle updates to the platform via webhooks,” and the platform’s webhook URL is discovered from its profile (UCP order capability). The only REST operation is
GET /orders/{id}, called by the platform. - ACP: the webhook receiver is “hosted by OpenAI to accept merchant order lifecycle events” (ACP webhook OpenAPI). The orders RFC lists “Order management APIs (create, update, cancel orders via API), inventory management, returns authorization flows” as out of scope (rfc.orders.md).
So the input to your ERP is the completed checkout session that your own server just finalized, plus whatever your PSP integration recorded. The protocol order object is an output you owe the platform, generated from ERP and WMS events. That changes the question. It is not “what does the order webhook give me”, it is “what does the checkout session leave me with when it completes, and what do I have to send back”.
Order creation: checkout fields vs ERP fields
Here is the field-by-field read for creating the sales order. ERP field names are from the NetSuite SuiteScript Records Browser for salesorder (2024.2) and the Business Central API v2.0 salesOrder and salesOrderLine resources.
| Back-office need | UCP 2026-08-25 | ACP 2026-04-17 | NetSuite / BC landing spot |
|---|---|---|---|
| Customer identity | buyer: first_name, last_name, email, phone_number (checkout only) | buyer with required email, plus customer_id, account_type, company (tax_id, cost_center) | NS entity; BC customerNumber / customerId |
| Ship-to | fulfillment extension destinations; expectations[].destination on the order | fulfillment_details.address; fulfillments[].destination on the order | NS shipaddress; BC shipToAddressLine1… shipToPostCode |
| SKU | item.id, “often the SKU” | sku, variant_id, product_id on checkout line items | NS item sublist item; BC lineObjectNumber / itemId |
| Tax | totals[] entry of type tax, amount only | totals[] with breakdown[] of jurisdiction, rate, amount; line tax_exempt; buyer.tax_exemption | NS taxdetails sublist, taxdetailsoverride; BC totalTaxAmount (read-only) |
| Prices include tax? | not found | not found | BC pricesIncludeTax (read-only) |
| Discounts | discount extension: code, title, amount, automatic, method, allocations[] (cart and checkout only) | discounts.applied[]: code, coupon, amount, allocations[] (checkout) | NS couponcode, promocode, promotions sublist, discountitem; BC discountAmount, line discountAmount |
| Payment reference | payment.instruments[]: handler_id, type, credential | payment_data: handler_id, instrument.credential (type, token); purchase_order_number, payment_terms | NS pnrefnum, authcode, paymentmethod, otherrefnum; BC salesOrder has no payment reference property |
| Platform’s own reference | none on the order | client_reference_id on the order | NS otherrefnum or a custom field; BC externalDocumentNumber |
| Agent identity | UCP-Agent header (profile URL) plus RFC 9421 signature; not on the order | Authorization bearer; User-Agent in MCP meta; not on the order | nothing standard; custom field |
| Authorization evidence | AP2 extension: ap2.checkout_mandate (SD-JWT) and ap2.merchant_authorization (JWS) | delegate payment allowance (max_amount, merchant_id, checkout_session_id, expires_at), held by the PSP | nothing standard |
| Protocol version | ucp.version and per-capability versions, on the order too | API-Version header; protocol.version on checkout; not on the order | nothing standard |
| Idempotency | Idempotency-Key SHOULD on REST; required in MCP meta | Idempotency-Key required on all REST POSTs | NS externalid; BC externalDocumentNumber (by convention) |
| Notes, gift message | not found in core schemas | order_notes | NS memo, message |
A few observations behind that table.
The UCP order carries no buyer and no payment. The order schema requires ucp, id, checkout_id, permalink_url, line_items, fulfillment, currency and totals. That is reasonable for a document whose audience is the agent, but it means you cannot rebuild the ERP order from the order object alone. You need the checkout. The same is true of ACP, whose Order also has no currency field; currency appears on adjustments and on the checkout session.
Tax is a number, not a calculation. UCP gives you a tax total with an optional display breakdown. ACP gives you jurisdiction, rate and amount, plus exemption certificates, which is closer to what a B2B order needs. Neither tells you which tax engine computed it or carries a transaction reference you can commit later, and neither says whether prices include tax. In BC the tax amounts on the API are read-only, so the ERP will compute its own. Opinion: the most common reconciliation break will be “buyer was charged X tax, ERP computed Y”. Decide now which system is authoritative and how you post the difference.
Discount detail stops at checkout. Both specs allocate discounts to lines by JSONPath during checkout, but the order object only carries discount and items_discount totals. If NetSuite needs couponcode to drive promotion reporting, persist the code from the checkout response before the session expires. UCP’s default checkout TTL is six hours per the expires_at description.
Payment references come from your side, not the protocol. The credential the agent hands you is a token (an ACP shared payment token, a UCP handler instrument). The charge or PaymentIntent ID is created by your own PSP call. That is fine, but it means the ERP’s settlement match key is not standardized by either spec. ACP’s client_reference_id is the only field that gives you the platform’s transaction ID for cross-referencing.
Agent identity and authorization evidence
UCP is the more complete of the two here. Every request carries a UCP-Agent header with the platform’s profile URL, requests are signed with RFC 9421 HTTP Message Signatures, the 2026-08-25 release added Web Bot Auth interop, and the optional AP2 mandate extension puts a user-signed checkout mandate on the complete request. ACP authenticates the platform with a bearer token and constrains the payment through the delegate payment allowance.
What neither does is put any of this on the order. The order has no agent, platform, or mandate field in either schema. Opinion: this is the most important gap for a back office. Chargeback evidence, per-channel dispute monitoring and fraud rule exceptions all need “which agent, verified how, with what user authorization” attached to the sales order and retained for the dispute window. Today you have to capture it at request time and carry it yourself.
Updates, cancellations and the webhook model
| Concern | UCP | ACP |
|---|---|---|
| Payload | Full order snapshot, never deltas | Full order in data, type is order_create or order_update |
| Event ID | Webhook-Id header, required (Standard Webhooks) | none required; Request-Id header optional, WebhookEvent allows only type and data |
| Ordering | Webhook-Timestamp; no sequence or order version field | Merchant-Signature timestamp; no sequence or updated_at on Order |
| Signing | RFC 9421, business key from /.well-known/ucp | HMAC-SHA256, t=...,v1=..., 300 s tolerance recommended |
| Retries | business MUST retry failed deliveries | not specified beyond status codes |
| Reconciliation | platform SHOULD use GET /orders/{id} | no order GET in the spec |
| Order status | none at order level; derived per line (processing, partial, fulfilled, removed) | created, confirmed, manual_review, processing, shipped, completed, canceled |
| Cancel / modify API | order:manage scope defined (“cancellation, returns, and other modifications”), no operation defined in this version | explicitly out of scope |
Full-snapshot payloads are good news: a dropped event is repaired by the next one. The weakness is ordering. Without a monotonic version on the order, a receiver that processes a shipped snapshot after a later canceled snapshot will regress state. You are the sender here, so this is mostly the platform’s problem, but opinion: keep an internal per-order sequence and only emit snapshots from a single writer, so you never race yourself.
A protocol-level wrinkle worth knowing: ACP’s REST spec made Idempotency-Key mandatory on every POST in 2026-04-17, but the MCP binding doc lists meta.idempotency_key as not required, “recommended for create and complete”. If you accept both transports, enforce the stricter rule yourself.
Cancellation is where the ERP state machine leaks through. Neither spec gives an agent a way to request a cancel or address change on a placed order. When it comes, it will run into the usual reality: once a NetSuite order has an item fulfillment, or a 3PL has picked it, a cancel is a request, not a command. Both specs can at least express the outcome, via an adjustments[] entry of type cancellation and a line quantity.total (UCP) or quantity.current (ACP) that drops.
Partial fulfillment, returns and refunds
This is the strongest part of both specs, and they converged. Both use a three-number line quantity (UCP original / total / fulfilled, ACP ordered / current / fulfilled) with the same derived line status, an append-only fulfillment event log with carrier and tracking, and an adjustments[] ledger for refunds, returns, credits, disputes and cancellations. That maps cleanly to NetSuite’s line quantityfulfilled and quantitybilled and BC’s shippedQuantity and invoicedQuantity. A 3PL shipment confirmation becomes one fulfillment event and one quantity update.
The gaps are on the return side. Neither spec has an RMA: no return authorization ID, no reason taxonomy beyond a free-text description (plus an unconstrained reason string in ACP), no return label, no disposition. An adjustment can say “refund, completed, -3000” but not which return authorization or credit memo produced it, and not which original capture it was refunded against. UCP’s policies[] snapshot on the order helps a support agent explain the return window. It does not help finance.
Multi-merchant carts
Both protocols are single-merchant per session. ACP’s changelog says carts “are scoped to a single seller”; UCP carts and checkouts are per business profile. A multi-merchant basket, like Google’s Universal Cart, is therefore N independent checkouts orchestrated by the platform. ACP has a marketplace_seller_details object on line items, but it only carries a name.
For a single merchant this is simpler, not harder: you only ever see your slice. Opinion: what you lose is any shared basket identifier, so you cannot tell that your order was one leg of a five-merchant basket where two legs failed. If the platform gives you one (in UCP attribution or ACP client_reference_id), keep it.
A proposed minimal agent order envelope
This is not a spec, it is what I would put between the protocol adapter and the ERP connector. One internal shape for both protocols, built at checkout completion from the session, the request metadata and the PSP response. Everything protocol-specific stays in source and evidence.
{
"envelope_version": "0.1",
"envelope_id": "env_01J9Z6Q2X8",
"idempotency_key": "ucp:shop.example:order_abc123",
"received_at": "2026-10-12T02:14:07Z",
"sequence": 1,
"source": {
"protocol": "ucp",
"protocol_version": "2026-08-25",
"capabilities": { "dev.ucp.shopping.checkout": "2026-08-25", "dev.ucp.shopping.order": "2026-08-25" },
"transport": "rest",
"channel_code": "AGENT-UCP-PLATFORMX",
"checkout_id": "checkout_xyz789",
"order_id": "order_abc123",
"platform_reference": null,
"basket_reference": null,
"attribution": { "utm_source": "platformx" }
},
"agent": {
"platform_profile": "https://platform.example/.well-known/ucp",
"verification": "rfc9421",
"key_id": "platform-2026",
"verified": true,
"authorization": {
"type": "ap2_checkout_mandate",
"evidence_uri": "evidence://orders/order_abc123/mandate.sdjwt",
"sha256": "9f2c...e1"
}
},
"customer": {
"match": { "strategy": "email", "value": "buyer@example.com" },
"first_name": "Ana",
"last_name": "Ruiz",
"phone": "+15125550100",
"erp_customer_ref": null,
"tax_exemption": null
},
"ship_to": {
"name": "Ana Ruiz",
"line1": "123 Main St",
"line2": null,
"city": "Austin",
"region": "TX",
"postal_code": "78701",
"country": "US"
},
"currency": "USD",
"prices_include_tax": false,
"lines": [
{
"line_id": "li_shoes",
"sku": "RUN-SHOE-42",
"quantity": 1,
"unit_price": 3000,
"discount": -300,
"tax": [{ "jurisdiction": "TX", "rate": 0.0825, "amount": 223 }]
}
],
"discounts": [{ "code": "FALL10", "title": "Fall 10%", "amount": -300, "automatic": false }],
"shipping": { "method_ref": "std_ground", "amount": 800, "promised_by": "2026-10-16" },
"totals": { "subtotal": 3000, "discount": -300, "shipping": 800, "tax": 223, "total": 3723 },
"tax_authority": "merchant_engine",
"payment": {
"psp": "stripe",
"psp_payment_ref": "pi_3Q...",
"credential_type": "network_token",
"credential_ref": "tok_...",
"amount_authorized": 3723,
"captured": false
},
"policies": [{ "type": "return", "url": "https://shop.example/returns" }],
"notes": null
}
Design notes, all opinion:
idempotency_keyis derived from protocol, merchant and protocol order ID, never from the agent’sIdempotency-Key, which is scoped to one HTTP operation. It goes into NetSuiteexternalidor BCexternalDocumentNumber.evidence_uriplus a hash, not the mandate itself. Mandates can contain personal data; keep the blob in an access-controlled store with a retention policy that covers your dispute window.tax_authorityrecords which system’s number is authoritative, so finance knows which side to adjust.channel_codeis per platform and per protocol. One “AI” channel will make dispute and authorization-rate monitoring useless.- Updates use the same envelope with
sequenceincremented and aneventblock (type,occurred_at,line_items). Cancellations and returns are events, not mutations.
Merchant checklist
- Build the ERP order from the completed checkout session and your PSP response, not from the protocol order object.
- Persist the full checkout response (buyer, discounts with codes, tax breakdown, selected fulfillment) at completion. UCP sessions expire.
- Pick one idempotency key per order and enforce it at ERP insert. Treat the agent’s
Idempotency-Keyas transport-level only. - Record protocol, protocol version and capability versions on every order. You will be debugging version-specific behavior within a year.
- Capture agent identity and authorization evidence at request time (UCP signature key ID and profile URL, AP2 mandate if negotiated; ACP platform credential and allowance), store it by reference, and link it from the sales order.
- Create a channel code per platform and protocol, and carry it to the order, the payment record and the dispute record.
- Decide whether the checkout tax or the ERP tax is authoritative, and build the variance posting before launch.
- Store the PSP charge ID and the agent credential reference on the order so refunds can find the original capture.
- Emit order snapshots from one writer with a per-order sequence. Include
Webhook-Idon UCP; add your ownRequest-Idon ACP. - Write down your cancel and change cutoffs by fulfillment state (not released, released to 3PL, picked, shipped), because the protocols will eventually ask.
- Model returns in the ERP first (return authorization, receipt, credit memo or refund), then project them into
adjustments[]. - Keep a reconciliation job: UCP lets the platform
GETyour order, so make sure what it reads matches the ERP.
What I could not settle
The UCP webhook example body includes event_id and created_time, but those fields are not in the order schema, so I would not rely on them. I found no notes or gift-message field in UCP’s core checkout schema; an extension may exist that I missed. And none of this says how Shopify, Google or OpenAI’s production implementations behave, which can differ from the published spec. If you have seen something different in production, I would like to hear about it.