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 needUCP 2026-08-25ACP 2026-04-17NetSuite / BC landing spot
Customer identitybuyer: 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-tofulfillment extension destinations; expectations[].destination on the orderfulfillment_details.address; fulfillments[].destination on the orderNS shipaddress; BC shipToAddressLine1… shipToPostCode
SKUitem.id, “often the SKU”sku, variant_id, product_id on checkout line itemsNS item sublist item; BC lineObjectNumber / itemId
Taxtotals[] entry of type tax, amount onlytotals[] with breakdown[] of jurisdiction, rate, amount; line tax_exempt; buyer.tax_exemptionNS taxdetails sublist, taxdetailsoverride; BC totalTaxAmount (read-only)
Prices include tax?not foundnot foundBC pricesIncludeTax (read-only)
Discountsdiscount 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 referencepayment.instruments[]: handler_id, type, credentialpayment_data: handler_id, instrument.credential (type, token); purchase_order_number, payment_termsNS pnrefnum, authcode, paymentmethod, otherrefnum; BC salesOrder has no payment reference property
Platform’s own referencenone on the orderclient_reference_id on the orderNS otherrefnum or a custom field; BC externalDocumentNumber
Agent identityUCP-Agent header (profile URL) plus RFC 9421 signature; not on the orderAuthorization bearer; User-Agent in MCP meta; not on the ordernothing standard; custom field
Authorization evidenceAP2 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 PSPnothing standard
Protocol versionucp.version and per-capability versions, on the order tooAPI-Version header; protocol.version on checkout; not on the ordernothing standard
IdempotencyIdempotency-Key SHOULD on REST; required in MCP metaIdempotency-Key required on all REST POSTsNS externalid; BC externalDocumentNumber (by convention)
Notes, gift messagenot found in core schemasorder_notesNS 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

ConcernUCPACP
PayloadFull order snapshot, never deltasFull order in data, type is order_create or order_update
Event IDWebhook-Id header, required (Standard Webhooks)none required; Request-Id header optional, WebhookEvent allows only type and data
OrderingWebhook-Timestamp; no sequence or order version fieldMerchant-Signature timestamp; no sequence or updated_at on Order
SigningRFC 9421, business key from /.well-known/ucpHMAC-SHA256, t=...,v1=..., 300 s tolerance recommended
Retriesbusiness MUST retry failed deliveriesnot specified beyond status codes
Reconciliationplatform SHOULD use GET /orders/{id}no order GET in the spec
Order statusnone at order level; derived per line (processing, partial, fulfilled, removed)created, confirmed, manual_review, processing, shipped, completed, canceled
Cancel / modify APIorder:manage scope defined (“cancellation, returns, and other modifications”), no operation defined in this versionexplicitly 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_key is derived from protocol, merchant and protocol order ID, never from the agent’s Idempotency-Key, which is scoped to one HTTP operation. It goes into NetSuite externalid or BC externalDocumentNumber.
  • evidence_uri plus 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_authority records which system’s number is authoritative, so finance knows which side to adjust.
  • channel_code is per platform and per protocol. One “AI” channel will make dispute and authorization-rate monitoring useless.
  • Updates use the same envelope with sequence incremented and an event block (type, occurred_at, line_items). Cancellations and returns are events, not mutations.

Merchant checklist

  1. Build the ERP order from the completed checkout session and your PSP response, not from the protocol order object.
  2. Persist the full checkout response (buyer, discounts with codes, tax breakdown, selected fulfillment) at completion. UCP sessions expire.
  3. Pick one idempotency key per order and enforce it at ERP insert. Treat the agent’s Idempotency-Key as transport-level only.
  4. Record protocol, protocol version and capability versions on every order. You will be debugging version-specific behavior within a year.
  5. 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.
  6. Create a channel code per platform and protocol, and carry it to the order, the payment record and the dispute record.
  7. Decide whether the checkout tax or the ERP tax is authoritative, and build the variance posting before launch.
  8. Store the PSP charge ID and the agent credential reference on the order so refunds can find the original capture.
  9. Emit order snapshots from one writer with a per-order sequence. Include Webhook-Id on UCP; add your own Request-Id on ACP.
  10. Write down your cancel and change cutoffs by fulfillment state (not released, released to 3PL, picked, shipped), because the protocols will eventually ask.
  11. Model returns in the ERP first (return authorization, receipt, credit memo or refund), then project them into adjustments[].
  12. Keep a reconciliation job: UCP lets the platform GET your 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.