Open your store to AI agents. Elixir SDK for UCP and ACP.

Bazaar helps you build commerce APIs in Elixir/Phoenix that work with both Google Shopping agents (UCP) and OpenAI/Stripe agents (ACP) from a single handler.

[!TIP] examples/flower_shop is a runnable merchant that passes the official UCP conformance suite, and CI runs that suite against it on every push.

Supported Protocols

ProtocolUsed BySpec
UCP (Universal Commerce Protocol)Google Shopping agentsucp.dev
ACP (Agentic Commerce Protocol)OpenAI Operator, StripeGitHub

Both protocols enable AI agents to discover what your store offers, create and manage shopping carts, complete checkouts, and track orders.

UCP was announced by Google at NRF 2026, co-developed with Shopify, Walmart, Etsy, and Target. ACP is backed by OpenAI and Stripe.

Features

  • Dual Protocol Support: Serve both UCP and ACP clients from one handler with automatic request/response translation
  • Generated UCP Schemas: Smelter-generated Ecto schemas from official UCP JSON Schemas
  • ACP Schema Validation: JSON Schema validation for ACP checkout sessions and delegate payment, plus an Ecto schema for the OpenAI product feed
  • Phoenix Router Macro: Mount UCP and ACP routes with a single line each
  • Handler Behaviour: Write commerce logic once, serve both protocols
  • Built-in Plugs: Request validation, idempotency, and UCP headers
  • Auto-generated Discovery: /.well-known/ucp endpoint from your handler
  • Protocol Transformer: Automatic field/status mapping between UCP and ACP formats
  • Business Logic Helpers: Currency conversion, message factories, order creation

How It Works

Bazaar uses UCP as its internal format. Your handler always works with UCP field names and status values, regardless of which protocol the client uses:

UCP Request → Bazaar Router → Your Handler → UCP Response
ACP Request → [transform to UCP] → Your Handler → [transform to ACP] → ACP Response

Bazaar handles the HTTP/JSON plumbing. You write the commerce logic.

BazaarYou
Routes requests from UCP and ACP agentsWrite business logic
Transforms between protocol formatsQuery your database
Validates request/response structureCalculate prices, tax, shipping
Handles UCP headers and discoveryIntegrate with payment/fulfillment

Architecture

lib/bazaar/
├── schemas/
│   ├── ucp/           # Generated UCP schemas (from Smelter)
│   │   ├── shopping/  # Checkout, Order, Payment types
│   │   ├── capability/# Capability definitions
│   │   └── ucp/       # Discovery profile, response types
│   └── acp/           # ACP schemas: OpenAI product feed
├── protocol.ex        # UCP/ACP status mappings
├── protocol/
│   └── transformer.ex # Request/response translation between protocols
├── validator.ex       # Schema validation (UCP via JSV, ACP via JSV/$defs, product feed via Ecto)
├── checkout.ex        # Business logic: currency helpers
├── order.ex           # Order documents: from a checkout, platform updates, fulfillment events
├── message.ex         # Business logic: error/warning/info factories
├── fulfillment.ex     # Fulfillment types and default configuration
├── handler.ex         # Handler behaviour
├── phoenix/           # Router and controller
├── plugs/             # Request validation, headers, idempotency
├── webhook.ex         # Order event delivery with retries
├── webhook/           # Event struct and retry schedule
├── signing/           # Signing keys and RFC 9421 HTTP message signatures
└── platform.ex        # Platform profile lookup (webhook URL)

Installation

Add bazaar to your dependencies in mix.exs:

def deps do
  [
    {:bazaar, "~> 0.2"}
  ]
end

Quick Start

Step 1: Create a Handler

The handler defines your store's capabilities and commerce logic:

defmodule MyApp.CommerceHandler do
  use Bazaar.Handler

  @impl true
  def capabilities, do: [:checkout, :orders]

  @impl true
  def business_profile do
    %{
      "name" => "My Awesome Store",
      "description" => "We sell amazing products"
    }
  end

  @impl true
  def create_checkout(params, _conn) do
    # params already validated by Bazaar
    {:ok, %{"id" => "chk_123", "status" => "incomplete", ...}}
  end

  @impl true
  def get_checkout(id, _conn) do
    {:ok, checkout} or {:error, :not_found}
  end

  # ... other callbacks: update_checkout, cancel_checkout, get_order, cancel_order
end

Step 2: Mount Routes

Add UCP and ACP routes to your Phoenix router:

defmodule MyAppWeb.Router do
  use Phoenix.Router
  use Bazaar.Phoenix.Router

  pipeline :api do
    plug :accepts, ["json"]
    plug Bazaar.Plugs.UCP   # UCP headers, version negotiation, idempotent replay
  end

  scope "/" do
    pipe_through :api

    # UCP routes (Google agents)
    bazaar_routes "/", MyApp.CommerceHandler

    # ACP routes (OpenAI/Stripe agents)
    bazaar_routes "/acp", MyApp.CommerceHandler, protocol: :acp
  end
end

UCP endpoints:

MethodPathDescription
GET/.well-known/ucpDiscovery endpoint
POST/checkout-sessionsCreate checkout
GET/checkout-sessions/:idGet checkout
PUT/checkout-sessions/:idUpdate checkout
POST/checkout-sessions/:id/completeComplete checkout
POST/checkout-sessions/:id/cancelCancel checkout
GET/orders/:idGet order
POST/orders/:id/actions/cancelCancel order
POST/webhooks/ucpReceive webhooks

ACP endpoints:

MethodPathDescription
POST/acp/checkout_sessionsCreate checkout
GET/acp/checkout_sessions/:idGet checkout
POST/acp/checkout_sessions/:idUpdate checkout
POST/acp/checkout_sessions/:id/completeComplete checkout
POST/acp/checkout_sessions/:id/cancelCancel checkout

bazaar_routes is a convenience, not a requirement. If you'd rather own the routes and controllers, skip it: build the discovery document with Bazaar.DiscoveryProfile.from_handler(MyApp.CommerceHandler, base_url: url), call the handler callbacks from your own actions, and keep using the plugs and helpers. You can also mix the two, which is what examples/flower_shop does: bazaar_routes for the standard routes and a few hand-written ones for what the conformance suite needs beyond the spec.

Step 3: Test It

# UCP discovery
curl http://localhost:4000/.well-known/ucp

# Create a checkout via UCP
curl -X POST http://localhost:4000/checkout-sessions \
  -H "Content-Type: application/json" -d '{"items": [...]}'

# Create a checkout via ACP
curl -X POST http://localhost:4000/acp/checkout_sessions \
  -H "Content-Type: application/json" -d '{"line_items": [...]}'

Protocol Differences

Bazaar automatically handles the differences between UCP and ACP. Your handler code stays the same:

AspectUCPACP
URL style/checkout-sessions/checkout_sessions
Update methodPUTPOST
Cancel methodPOST /cancelPOST /cancel
Discovery/.well-known/ucpNone
Status: incompleteincompletenot_ready_for_payment
Status: readyready_for_completeready_for_payment
Address: streetstreet_addressline_one
Address: cityaddress_localitycity
Items keyitemsline_items

Validation

Bazaar bundles schema validation for both protocols:

# UCP schemas (via JSV against bundled JSON Schemas)
Bazaar.Validator.validate(data, :checkout)
Bazaar.Validator.validate(data, :order)
Bazaar.Validator.validate(data, :profile)

# ACP schemas (via JSV against bundled JSON Schemas with $defs)
Bazaar.Validator.validate(data, :checkout_session)
Bazaar.Validator.validate(data, :checkout_create_req)
Bazaar.Validator.validate(data, :checkout_complete_req)
Bazaar.Validator.validate(data, :delegate_payment_req)
Bazaar.Validator.validate(data, :delegate_payment_resp)

# OpenAI product feed (via Ecto embedded schema)
Bazaar.Validator.validate(data, :openai_product_feed)

# List all available schemas
Bazaar.Validator.available_schemas()
# => %{ucp: [:checkout, :order, :profile], acp: [:checkout_session, ...]}

UCP schemas track the UCP spec (currently 2026-08-25). ACP schemas track the open ACP repo (currently 2026-01-30).

Capabilities

CapabilityDescriptionCallbacks
:checkoutShopping cart managementcreate_checkout, get_checkout, update_checkout, cancel_checkout
:ordersOrder trackingget_order, cancel_order
:fulfillmentShipping and pickupExtends checkout/order with fulfillment options
:identityUser identity linkinglink_identity
:catalogProduct discoverylist_products, get_product, search_products
:discountDiscount codesExtends checkout with discount support

Schemas

UCP schemas are generated from official JSON Schemas using Smelter:

# Validate checkout response
changeset = Bazaar.Schemas.Shopping.CheckoutResp.new(params)

# Create order params from checkout
order_params = Bazaar.Order.from_checkout(checkout, "order_123", "https://shop.com/orders/123")

# Currency helpers
cents = Bazaar.Checkout.to_minor_units(19.99)  # => 1999
dollars = Bazaar.Checkout.to_major_units(1999)  # => 19.99

# Message factories
error = Bazaar.Message.error(%{"code" => "out_of_stock", "content" => "Item unavailable"})

When the spec is updated, fetch the new version's schemas (needs cargo install ucp-schema) and regenerate:

mix run scripts/fetch_ucp_schemas.exs 2026-08-25
mix bazaar.gen.schemas priv/ucp_schemas/2026-08-25

Webhooks

Platforms learn about orders through webhooks: the full order document, POSTed to the URL the platform advertises in its profile, with Webhook-Id and Webhook-Timestamp headers and retries that keep both. Bazaar finds the URL and delivers the event; your handler decides when.

def complete_checkout(id, conn) do
  # ... authorize payment, build the order ...
  {:ok, url} = Bazaar.Platform.webhook_url(conn.assigns.ucp_agent_profile, http_client: &MyApp.Http.get/1)
  event = Bazaar.Webhook.event(order, url)

  Task.Supervisor.start_child(MyApp.TaskSupervisor, fn ->
    Bazaar.Webhook.deliver(event, http_client: &MyApp.Http.post/3, signer: {key, "https://shop.example/.well-known/ucp"})
  end)

  {:ok, checkout}
end

Delivery is signed with RFC 9421 HTTP message signatures when you pass a Bazaar.Signing.Key (EC P-256 or Ed25519, loaded from PEM or JWK). Publish its public half as "keys" in business_profile/0 and platforms verify against it. Bazaar bundles no HTTP client: the two functions above are yours, a few lines on Req or whatever you use.

Plugs

Optional plugs for production use:

pipeline :ucp do
  plug :accepts, ["json"]
  plug Bazaar.Plugs.UCP              # UCPHeaders (version negotiation) then Idempotency (replay)
  plug Bazaar.Plugs.ValidateRequest  # Validate request body
end

Bazaar.Plugs.UCP composes Bazaar.Plugs.UCPHeaders and Bazaar.Plugs.Idempotency; use them individually if you need something in between. Idempotency needs a store: Bazaar.Idempotency.ETS in your supervision tree for development and a single node, or Bazaar.Idempotency.Cachex on a Cachex cache for production and any multi-node deployment. Errors from the plugs and the controller are spec-shaped: the UCP error response for UCP routes, the ACP Error object for ACP routes. See the plugs guide.

Guides

License

Apache 2.0