cimplify

Universal Commerce Protocol (UCP)

The developer-preview REST contract for signed agent carts, Mobile Money checkout, and consent-scoped order reads.

UCP is Cimplify's developer-preview REST surface for agent-operated commerce. The implementation currently supports product discovery, agent-session-owned carts, guest checkout through the PurchaseIntent door, and consent-scoped order reads. REST is the only implemented transport.

Availability and rollout

UCP has two fail-closed deployment controls:

  • ucp.enabled is the service-wide UCP switch. When it is off, UCP resources are not exposed.
  • native_checkout.purchase_intents_enabled is the deployment-wide checkout emergency switch.

Checkout is a platform capability for every eligible merchant; there is no merchant preference or opt-out. The manifest advertises checkout as enabled when the deployment switch is on, the merchant is active, and at least one UCP-supported payment method is configured. Payment-method configuration is executable tender authority, not a checkout opt-out. Discovery and order reads do not depend on the checkout switches, but every UCP route remains subject to the service-wide UCP switch.

REST routes

The manifest route includes the /manifest suffix. The bare /ucp/v1/{business_handle} path is not a UCP route.

MethodRouteCurrent authorization
GET/ucp/v1/{business_handle}/manifestPublic discovery
POST/ucp/v1/{business_handle}/productsPublic product search
GET/ucp/v1/{business_handle}/products/{product_id}Public product read
POST/ucp/v1/{business_handle}/cartSigned agent; add_to_cart, purchase, or full_access consent
GET/ucp/v1/{business_handle}/cart/{cart_id}Signed owner agent
POST/ucp/v1/{business_handle}/cart/{cart_id}/itemsSigned owner agent; add_to_cart, purchase, or full_access consent
POST/ucp/v1/{business_handle}/checkoutSigned owner agent; purchase or full_access consent; Idempotency-Key
GET/ucp/v1/{business_handle}/orders?customer_id={customer_id}Signed agent; view_orders or full_access consent
GET/ucp/v1/{business_handle}/orders/{order_id}Signed agent; view_orders or full_access consent
GET/ucp/v1/{business_handle}/orders/{order_id}/trackingSigned agent; view_orders or full_access consent

The manifest and product-discovery routes do not currently invoke the agent authenticator. Do not assume that preview behavior will remain the public-access policy at GA.

Manifest

Fetch the REST manifest at:

GET https://api.cimplify.io/ucp/v1/sweet-bakery/manifest

The response advertises only implemented REST bindings. A checkout-enabled business with Mobile Money may return a shape like:

{
  "version": "1.0",
  "business": {
    "id": "biz_sweet_bakery",
    "name": "Sweet Bakery",
    "handle": "sweet-bakery",
    "category": "food",
    "currency": "GHS",
    "contact": {
      "email": "hello@sweetbakery.test",
      "phone": "+233244000000"
    }
  },
  "capabilities": {
    "product_discovery": {
      "enabled": true,
      "binding": "rest",
      "endpoint": "https://api.cimplify.io/ucp/v1/sweet-bakery/products",
      "features": ["search", "browse", "filter"]
    },
    "checkout": {
      "enabled": true,
      "binding": "rest",
      "endpoint": "https://api.cimplify.io/ucp/v1/sweet-bakery/checkout",
      "payment_handlers": ["mobile_money"],
      "supported_methods": ["mobile_money"],
      "features": ["guest_checkout"]
    },
    "order_management": {
      "enabled": true,
      "binding": "rest",
      "endpoint": "https://api.cimplify.io/ucp/v1/sweet-bakery/orders",
      "features": ["tracking", "history"]
    }
  },
  "shipping": {
    "regions": ["GH"],
    "methods": ["standard", "pickup"],
    "same_day_available": false,
    "pickup_available": true
  },
  "extensions": {
    "loyalty": false,
    "discounts": true,
    "gift_cards": false,
    "subscriptions": false,
    "reservations": false
  }
}

Business fields are derived from current configuration. Shipping advertises only standard, plus pickup when the business supports it; regions is omitted when the business has no canonical country metadata instead of inventing a default. Reservations remain false because there is no UCP reservation route. Disabled checkout is represented by checkout.enabled: false with empty payment-handler and supported-method lists. There is no advertised UCP identity-linking route.

Agent identity and request signatures

Authenticated routes require UCP-Agent. Its value is a compact JSON object:

UCP-Agent: {"provider":"openai","agent_id":"shopping-agent-42","version":"1.0","capabilities":["commerce"]}

provider and agent_id are required. version and capabilities are optional. The header does not carry a principal or a client-selected session ID. provider and agent_id must be non-empty canonical values with no surrounding whitespace or control characters; their limits are 100 and 200 bytes respectively.

When ucp.require_signatures is enabled (the default), authenticated requests also require an HMAC-SHA256 signature:

X-Request-Signature: t=1787500000,v2=<hex_hmac_sha256>
X-Body-Hash: <hex_sha256_of_exact_request_body>

Hash the exact UTF-8 UCP-Agent header value, including its JSON formatting:

agent_hash = hex(SHA256(exact_ucp_agent_header_bytes))
evidence = timestamp.method.path_and_query.body_hash.agent_hash

For a bodyless read, body_hash is the empty string; the separators remain in the evidence. For POST, PUT, and PATCH, X-Body-Hash is required, including for an empty-body POST, and is the lowercase hex SHA256 of the exact bytes sent. Use the exact HTTP method and path including its query string.

Domain-separate and length-frame the HMAC input:

domain = "cimplify.ucp.request-signature.v2"
message = u64be(domain_length) || domain || u64be(evidence_length) || evidence
signature = hex(HMAC-SHA256(configured_ucp_secret, message))

Only v2 is accepted; v1 signatures are rejected. The default timestamp tolerance is 300 seconds and is deployment-configurable. Because the exact agent header is signed, changing provider, agent_id, optional fields, or JSON formatting after signing invalidates the request.

Consent is a separate signed value carried in X-Consent-Proof. Send compact JSON as the header value rather than as a checkout request-body member.

X-Consent-Proof: {"token":"consent_01","action":"purchase","granted_at":"2026-08-23T12:00:00Z","expires_at":"2026-08-23T13:00:00Z","scope":{"business_id":"biz_sweet_bakery","max_amount":"120.00","currency":"GHS","product_ids":["prod_sourdough"]},"signature":"<hex_hmac_sha256>"}

The current proof schema is:

  • token: opaque consent identity;
  • action: browse, add_to_cart, purchase, view_orders, link_identity, or full_access;
  • granted_at and expires_at: an active, ordered UTC validity window;
  • scope.business_id: the internal business ID, not its handle;
  • optional scope.customer_id, scope.max_amount, scope.currency, and scope.product_ids;
  • signature: lowercase hex HMAC-SHA256.

The proof signature uses the configured UCP secret and the domain cimplify.ucp.consent-signature.v2. Its signed evidence includes the token, action, Unix grant and expiry timestamps, and the complete scope. Changing an amount, currency, customer, product list, action, or validity timestamp after issuance invalidates the signature.

Action enforcement is route-specific:

  • Cart creation and item addition require add_to_cart; the broader purchase and full_access actions are also accepted.
  • Checkout requires purchase or full_access.
  • Order history, order detail, and tracking require view_orders or full_access. A customer-scoped view_orders proof can read only that customer's orders. full_access is treated as business-wide for these reads, so do not use it as a customer-limited grant.

Purchase is currently guest-only. A purchase or full_access proof with scope.customer_id fails closed because no production delegated Customer-to-Account issuer exists yet. Customer-scoped view_orders remains supported. For purchase consent, max_amount and currency must be supplied together; the server checks the signed amount, currency, and optional product allowlist against the immutable PurchaseIntent terms before confirmation.

AgentSession cart ownership

POST /cart creates an opaque AgentSession and binds the new Cart to it. The session ID is server-owned and is not selected by the caller. Subsequent Cart reads, item additions, and checkout resolve the session from the Cart and require an exact active tuple:

session + business + cart + UCP-Agent.provider + UCP-Agent.agent_id

Expired, completed, abandoned, or revoked sessions cannot authorize the Cart. Session lifetime defaults to 24 hours and is configurable up to 30 days. The same session becomes the PurchaseIntent's AgentSession owner, so a public Cart or order ID is not replay authority. Continue sending the same provider and agent_id; storing a consent on the session does not replace the required per-request consent header.

Checkout

Checkout requires a canonical Idempotency-Key: it must be non-empty, have no leading or trailing whitespace or control characters, and be at most 200 bytes. The server domain-separates it with the exact session, business, and Cart before minting and confirming the PurchaseIntent. Reuse the same key and request to recover the same operation; do not generate a new key merely because provider completion is still pending.

The current UCP tender is Mobile Money only:

  • accepted payment.method values are mobile_money, momo, mtn_momo, vodafone_cash, and airtel_money;
  • payment.data, when present, must be an object containing only phone_number and provider;
  • generic mobile_money/momo requires payment.data.provider; a provider-specific method supplies its corresponding network when omitted;
  • canonical network values are mtn, telecel, and airtel;
  • card, Google Pay, Apple Pay, PayPal, cash, and offline collection are not UCP tenders.

The phone number may come from payment.data.phone_number, top-level phone, or shipping_address.phone. UCP accepts no provider token or reusable payment credential. The requested network is a hint into immutable terms: confirmation selects the exact eligible server-issued Mobile Money payment option matching that network, and that option determines the provider command.

POST /ucp/v1/sweet-bakery/checkout
UCP-Agent: {"provider":"openai","agent_id":"shopping-agent-42"}
X-Request-Signature: t=1787500000,v2=<signature>
X-Body-Hash: <body_hash>
X-Consent-Proof: <compact_purchase_consent_json>
Idempotency-Key: checkout-cart-01
Content-Type: application/json
{
  "cart_id": "cart_01",
  "shipping_address": {
    "name": "Ama Mensah",
    "line1": "1 High Street",
    "city": "Accra",
    "state": "Greater Accra",
    "postal_code": "GA-100",
    "country": "GH",
    "phone": "+233241234567"
  },
  "shipping_method": "standard",
  "payment": {
    "method": "mobile_money",
    "data": {
      "phone_number": "+233241234567",
      "provider": "mtn"
    }
  },
  "email": "ama@example.test"
}

Checkout outcomes

The UCP response preserves the exact PurchaseIntent confirmation result:

{
  "data": {
    "outcome": {
      "status": "collection_processing",
      "intent_id": "pi_01",
      "attempt_id": "pia_01",
      "order_id": "ord_01"
    },
    "order": {
      "order_id": "ord_01",
      "status": "pending",
      "totals": {
        "subtotal": "100.00",
        "tax": "0.00",
        "shipping": null,
        "discount": "0.00",
        "total": "100.00",
        "currency": "GHS"
      },
      "created_at": "2026-08-23T12:00:00Z"
    }
  },
  "meta": {
    "timestamp": "2026-08-23T12:00:00Z"
  },
  "status": {
    "code": 202,
    "success": true
  }
}

Agents must branch on data.outcome.status. The closed result set is order_created, collection_processing, payment_outcome_unknown, payment_failed, redirect_required, card_popup_required, authorization_required, terms_changed, selection_unavailable, offer_ended, and source_unavailable.

order is present when an Order was committed, including pending, action-required, outcome-unknown, and failed collection attempts. Its presence and HTTP 202 do not mean payment succeeded. Pre-commit outcomes such as terms_changed and selection_unavailable have no fabricated Order and return HTTP 200. Checkout responses use Cache-Control: no-store.

The preview REST surface does not yet expose a separate intent-action or intent-poll endpoint. Preserve the exact checkout idempotency key for recovery and treat action-required or unknown outcomes as non-terminal unless the returned outcome says otherwise.

Developer-preview limitations

UCP is not GA. The implemented transport is REST only, purchase principal is guest-only, checkout tender is Mobile Money only, and the consent issuer is the configured HMAC integration. The manifest, authentication policy, and response shapes may change before GA. Integrations must respect the manifest and kill switches at runtime instead of assuming a capability is enabled.

On this page