Bazaar schemas are generated from official UCP JSON Schemas using Smelter. They validate incoming data and provide type-safe access to fields.

Architecture

Bazaar separates generated schemas from business logic:

lib/bazaar/
├── schemas/                    # Generated from JSON Schemas
│   ├── shopping/
│   │   ├── checkout_resp.ex    # Checkout validation
│   │   ├── order.ex            # Order validation
│   │   └── types/              # Shared types (line items, totals, etc.)
│   ├── capability/             # Capability definitions
│   └── ucp/                    # Discovery profile types
├── 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

Generated schemas provide new/1 and fields/0 functions. Business logic modules add helpers and factories on top.

Regenerating Schemas

When a new UCP spec version is released, fetch its schemas and regenerate:

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

The fetch script clones the spec repo at that tag and rebuilds the resolved schema tree under priv/ucp_schemas/<version>/, replacing that directory if it exists. It writes one _resp.json plus .create_req.json, .update_req.json and .complete_req.json variant per annotated schema, produced with the official ucp-schema CLI (cargo install ucp-schema). The generator then overwrites all files in lib/bazaar/schemas/ with fresh generated code.

Key Concepts

Prices in Minor Units

UCP uses minor currency units (cents) as integers, not decimals:

# $19.99 = 1999 cents
%{"price" => 1999}

# Convert dollars to cents
Bazaar.Checkout.to_minor_units(19.99)  # => 1999

# Convert cents to dollars
Bazaar.Checkout.to_major_units(1999)  # => 19.99

Structured Totals

Totals are arrays of typed amounts, not individual fields:

"totals" => [
  %{"type" => "subtotal", "amount" => 3998},
  %{"type" => "tax", "amount" => 320},
  %{"type" => "fulfillment", "amount" => 500},
  %{"type" => "total", "amount" => 4818}
]

Total types: items_discount, subtotal, discount, fulfillment, tax, fee, total

Checkout responses require legal links:

"links" => [
  %{"type" => "privacy_policy", "url" => "https://..."},
  %{"type" => "terms_of_service", "url" => "https://..."}
]

Link types: privacy_policy, terms_of_service, refund_policy, shipping_policy, faq

Checkout Schema

The checkout schema validates shopping cart data. Use Bazaar.Schemas.Shopping.CheckoutResp for responses.

Creating a Checkout

params = %{
  "ucp" => %{"name" => "dev.ucp.shopping.checkout", "version" => "2026-01-11"},
  "id" => "checkout_123",
  "status" => "incomplete",
  "currency" => "USD",
  "line_items" => [
    %{
      "item" => %{"id" => "WIDGET-001", "title" => "Widget", "price" => 1999},
      "quantity" => 2,
      "totals" => [%{"type" => "subtotal", "amount" => 3998}]
    }
  ],
  "totals" => [%{"type" => "total", "amount" => 3998}],
  "links" => [%{"type" => "privacy_policy", "url" => "https://example.com/privacy"}],
  "payment" => %{}
}

case Bazaar.Schemas.Shopping.CheckoutResp.new(params) do
  %{valid?: true} = changeset ->
    checkout = Ecto.Changeset.apply_changes(changeset)
    # Process checkout...

  %{valid?: false} = changeset ->
    errors = Bazaar.Errors.from_changeset(changeset)
    # Handle validation errors...
end

Status Values

StatusDescription
incompleteMissing required info
requires_escalationNeeds browser handoff
ready_for_completeReady for payment
complete_in_progressPayment processing
completedOrder created
canceledSession cancelled

Buyer Fields

%{
  "buyer" => %{
    "first_name" => "Jane",
    "last_name" => "Doe",
    "email" => "jane@example.com",
    "phone_number" => "+15551234567"
  }
}

Address Fields

%{
  "shipping_address" => %{
    "street_address" => "123 Main Street",
    "extended_address" => "Apt 4B",
    "address_locality" => "New York",
    "address_region" => "NY",
    "postal_code" => "10001",
    "address_country" => "US",
    "first_name" => "Jane",
    "last_name" => "Doe"
  }
}

Order Schema

Use Bazaar.Schemas.Shopping.OrderResp for order validation and Bazaar.Order for business logic.

Creating from Checkout

checkout = %{
  "id" => "checkout_abc",
  "currency" => "USD",
  "line_items" => [...],
  "totals" => [...]
}

order = Bazaar.Order.from_checkout(checkout, "order_123", "https://shop.example/orders/123")
# A spec-valid order: line items with quantity totals and status, one fulfillment
# expectation per method from the selected option and destination, empty events and adjustments.

{:ok, order} = Bazaar.Order.apply_update(order, params)   # events and adjustments a platform PUTs
order = Bazaar.Order.add_event(order, shipped_event)      # your own fulfillment events

Order Fields

FieldTypeDescription
idstringUnique order identifier
checkout_idstringAssociated checkout ID
permalink_urlstringURL to order on merchant site
line_itemsarrayImmutable line items
totalsarrayOrder totals
fulfillmentobjectExpectations and events
adjustmentsarrayRefunds, credits, etc.

Fulfillment

"fulfillment" => %{
  "expectations" => [
    %{
      "id" => "exp_1",
      "delivery_method" => "shipping",
      "estimated_delivery_date" => "2025-01-20T00:00:00Z",
      "line_item_ids" => ["li_1", "li_2"]
    }
  ],
  "events" => [
    %{
      "id" => "evt_1",
      "type" => "shipped",
      "timestamp" => "2025-01-15T14:30:00Z",
      "carrier" => "UPS",
      "tracking_number" => "1Z999AA10123456784",
      "tracking_url" => "https://ups.com/track/...",
      "line_item_ids" => ["li_1"]
    }
  ]
}

Fulfillment event types: shipped, out_for_delivery, delivered, failed, returned

Message Factories

Use Bazaar.Message to create error, warning, and info messages:

# Create error message
error = Bazaar.Message.error(%{
  "code" => "out_of_stock",
  "content" => "Item SKU-123 is no longer available",
  "severity" => "recoverable",
  "path" => "$.line_items[0]"
})

# Create warning message
warning = Bazaar.Message.warning(%{
  "code" => "price_changed",
  "content" => "Price has increased since item was added"
})

# Create info message
info = Bazaar.Message.info(%{
  "code" => "promo_available",
  "content" => "Use code SAVE10 for 10% off"
})

# Parse message by type
changeset = Bazaar.Message.parse(%{"type" => "error", "code" => "...", ...})

# Validate list of messages
{:ok, messages} = Bazaar.Message.validate_messages([...])

Severity Values

SeverityDescription
recoverableAgent can resolve automatically
requires_buyer_inputNeed buyer action
requires_buyer_reviewNeed buyer confirmation

Discovery Profile

Use Bazaar.DiscoveryProfile to build the /.well-known/ucp response:

profile = Bazaar.DiscoveryProfile.build(
  MyApp.UCPHandler,
  base_url: "https://api.mystore.com"
)

This is usually handled automatically by Bazaar's controller.

Currency Validation

Bazaar validates currencies against ISO 4217:

Bazaar.Currencies.valid?("USD")  # => true
Bazaar.Currencies.valid?("EUR")  # => true
Bazaar.Currencies.valid?("XYZ")  # => false

# Get all supported currencies
Bazaar.Currencies.codes()
# => ["AED", "AFN", "ALL", ..., "ZWL"]

Error Handling

Convert changeset errors to UCP format:

changeset = Bazaar.Schemas.Shopping.CheckoutResp.new(%{})

errors = Bazaar.Errors.from_changeset(changeset)
# => %{
#   "error" => "validation_error",
#   "message" => "Validation failed",
#   "details" => [
#     %{"field" => "currency", "message" => "can't be blank"},
#     %{"field" => "line_items", "message" => "can't be blank"},
#     ...
#   ]
# }

Field Definitions

Access raw field definitions for custom use:

# All checkout fields
Bazaar.Schemas.Shopping.CheckoutResp.fields()

# All order fields
Bazaar.Schemas.Shopping.OrderResp.fields()

# Fulfillment configuration
Bazaar.Fulfillment.default_merchant_config()
Bazaar.Fulfillment.default_platform_config()

Tips

Always Use String Keys

Schemas expect string keys in params:

# Correct
%{"currency" => "USD"}

# Wrong - won't validate properly
%{currency: "USD"}

Integer Prices

Prices are integers in minor units (cents):

# Correct - $19.99 as 1999 cents
%{"item" => %{"price" => 1999}}

# Wrong - decimal/string
%{"item" => %{"price" => "19.99"}}

Next Steps