Bazaar supports two commerce protocols, allowing your store to serve multiple AI agent ecosystems from a single handler implementation.

Supported Protocols

ProtocolUsed ByDiscovery
UCP (Universal Commerce Protocol)Google Shopping agents/.well-known/ucp
ACP (Agentic Commerce Protocol)OpenAI Operator, StripeCentralized registry

Internal Format: UCP

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

ACP Request → [transform to UCP] → Your Handler → [transform to ACP] → ACP Response
UCP Request → Your Handler → UCP Response (no transformation)

This means you write your handler once using UCP conventions, and Bazaar automatically translates for ACP clients.

Protocol Differences

URL Patterns

OperationUCPACP
CreatePOST /checkout-sessionsPOST /checkout_sessions
GetGET /checkout-sessions/:idGET /checkout_sessions/:id
UpdatePATCH /checkout-sessions/:idPOST /checkout_sessions/:id
CompletePOST /checkout-sessions/:id/actions/completePOST /checkout_sessions/:id/complete
CancelDELETE /checkout-sessions/:idPOST /checkout_sessions/:id/cancel

Status Values

Internal (UCP)ACP
incompletenot_ready_for_payment
requires_escalationauthentication_required
ready_for_completeready_for_payment
complete_in_progressin_progress
completedcompleted
canceledcanceled

Address Fields

UCPACP
street_addressline_one
extended_addressline_two
address_localitycity
address_regionstate
address_countrycountry
postal_codepostal_code

Item Fields

UCPACP
itemsline_items
skuproduct.id
nameproduct.name
pricebase_amount

Product Schema Comparison

Neither protocol defines a standalone product catalog schema. Product information is embedded in checkout line items. Here's how they compare:

FieldUCP (ItemResp)ACP (LineItem)
Product IDiditem.id
Nametitlename
Description—description
Unit pricepriceunit_amount
Imageimage_url (single)images[] (array)
Quantity(in LineItemResp)item.quantity
Custom attributes—custom_attributes[]
Marketplace seller—marketplace_seller_details
Disclosures—disclosures[]

Line item pricing:

FieldUCP (LineItemResp.totals[])ACP (LineItem)
Base amounttotals[type=base]base_amount
Discounttotals[type=discount]discount
Subtotaltotals[type=subtotal]subtotal
Taxtotals[type=tax]tax
Totaltotals[type=total]total

ACP is flatter with fields directly on LineItem. UCP is more structured with nested ItemResp and a totals array. ACP includes additional product fields like description, multiple images, custom attributes, and marketplace seller details.

Router Configuration

UCP Only (Default)

scope "/" do
  pipe_through :api
  bazaar_routes "/", MyApp.UCPHandler
end

ACP Only

scope "/" do
  pipe_through :api
  bazaar_routes "/", MyApp.UCPHandler, protocol: :acp
end

Both Protocols

scope "/" do
  pipe_through :api

  # UCP at /ucp (Google agents)
  bazaar_routes "/ucp", MyApp.UCPHandler

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

Discovery

UCP Discovery

UCP uses open discovery via /.well-known/ucp. Bazaar automatically generates this endpoint from your handler's business_profile/0 and capabilities/0.

curl http://localhost:4000/.well-known/ucp

ACP Discovery

ACP uses centralized discovery through Stripe's merchant registry. There's no /.well-known endpoint for ACP. Merchants register their ACP endpoints directly with Stripe/OpenAI.

When using protocol: :acp, Bazaar does not generate a discovery endpoint.

Testing Both Protocols

UCP Request

curl -X POST http://localhost:4000/ucp/checkout-sessions \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "usd",
    "items": [{"sku": "PROD-001", "quantity": 1}]
  }'

Response uses UCP format:

{
  "id": "...",
  "status": "incomplete",
  "items": [...]
}

ACP Request

curl -X POST http://localhost:4000/acp/checkout_sessions \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "usd",
    "line_items": [{"product": {"id": "PROD-001"}, "quantity": 1}]
  }'

Response uses ACP format:

{
  "id": "...",
  "status": "not_ready_for_payment",
  "line_items": [...]
}

Handler Implementation

Your handler uses UCP format regardless of the protocol:

defmodule MyApp.UCPHandler do
  use Bazaar.Handler

  @impl true
  def create_checkout(params, _conn) do
    # params are ALWAYS in UCP format
    # - params["items"] (not "line_items")
    # - params["items"][0]["sku"] (not "product.id")

    {:ok, %{
      "id" => "checkout_123",
      "status" => "incomplete",  # Always use UCP status
      "items" => [...]           # Always use "items" key
    }}
  end

  @impl true
  def update_checkout(id, params, _conn) do
    # Buyer addresses are in UCP format
    # - params["buyer"]["shipping_address"]["street_address"]
    # - params["buyer"]["shipping_address"]["address_locality"]

    {:ok, updated_checkout}
  end
end

Bazaar handles the transformation automatically:

  • ACP line_items → UCP items (before your handler)
  • UCP incomplete → ACP not_ready_for_payment (after your handler)

Validation

Each protocol uses its own validation schemas:

  • UCP: Bazaar.Schemas.Ucp.*
  • ACP: Bazaar.Schemas.Acp.*

Bazaar validates incoming requests against the appropriate schema based on the protocol option.

Next Steps