Getting Started with UCP¶
This guide will walk you through building a basic Universal Commerce Protocol (UCP) server. We provide examples for both Python (FastAPI) and Node.js (Express) using the official Python SDK and TypeScript SDK.
We will implement a simple checkout server that allows a platform to initiate a checkout session and retrieve it.
Prerequisites¶
- Python 3.10 or higher installed.
- Basic familiarity with FastAPI and Pydantic.
- We recommend using
uvfor package management.
- Node.js 18 or higher installed.
- Basic familiarity with Express and Zod.
Project Setup¶
Create a new directory and initialize the project:
Add the required dependencies. ucp-sdk contains the Pydantic models generated from UCP schemas:
Note
The Python SDK package is registered as ucp-sdk on PyPI, but is imported as ucp_sdk (with an underscore) in your Python code.
Create a new directory and initialize the project:
Configure your package.json to use ES Modules by adding "type": "module".
Add the required dependencies. @ucp-js/sdk contains the TypeScript types and Zod schemas:
npm install express uuid @ucp-js/sdk
npm install --save-dev typescript @types/express @types/node tsx
Initialize TypeScript configuration:
Implementing the Server¶
We will implement the server step-by-step. The server needs to handle:
- Create Checkout (
POST /checkout-sessions): Receives desired items and returns a checkout session with totals and available payment handlers. - Get Checkout (
GET /checkout-sessions/{id}): Allows the platform to poll the session status.
High-Level Flow¶
Here is how the components interact during the checkout process:
%%{init: {'sequence': {'actorMargin': 90, 'messageMargin': 45}}}%%
sequenceDiagram
autonumber
participant Platform as "Platform"
participant Server as "Checkout Server (Your App)"
participant DB as "In-Memory DB"
Note over Platform,Server: Create Checkout Session
Platform->>+Server: POST /checkout-sessions (with Items and Headers)
Note over Server: Validate, Calculate Totals, and Advertise Payment Handlers
Server->>DB: Store Session
Server-->>-Platform: 201 Created (Checkout Response)
Note over Platform,Server: Retrieve Checkout Session (Polling)
Platform->>+Server: GET /checkout-sessions/{id} (with Headers)
Server->>DB: Fetch Session
Server-->>-Platform: 200 OK (Checkout Response)
How it fits into the E2E Flow¶
In a complete UCP integration, the flow follows these phases:
- Discovery: The Platform discovers your UCP endpoints (like
/checkout-sessions) by fetching your UCP Discovery Profile at/.well-known/ucp. - Create Checkout (Implemented in this guide): The Platform initiates the checkout session with the items the user wants to buy.
- Update Checkout: The Platform updates the checkout session with buyer details (e.g., shipping address, email) to calculate final taxes and shipping options.
- Complete Checkout: The Platform submits the payment credentials to finalize the order.
This guide focuses on step 2 (Create Checkout) and the subsequent retrieval of the checkout session state.
1. Imports and Setup¶
Initialize the application and define an in-memory database to store sessions.
# main.py
import uuid
from typing import Annotated
from fastapi import FastAPI, Header, HTTPException, status
# Import UCP SDK models
from ucp_sdk.models.schemas.ucp import ResponseCheckoutSchema
from ucp_sdk.models.schemas.shopping.checkout import Checkout
from ucp_sdk.models.schemas.shopping.checkout_create_request import CheckoutCreateRequest
from ucp_sdk.models.schemas.shopping.types.line_item import LineItem
from ucp_sdk.models.schemas.shopping.types.item import Item
from ucp_sdk.models.schemas.common.types.totals import Total
from ucp_sdk.models.schemas.common.types.link import Link
from ucp_sdk.models.schemas.common.types.available_payment_instrument import AvailablePaymentInstrument
from ucp_sdk.models.schemas.payment_handler import ResponseSchema as PaymentHandlerResponse
# Initialize FastAPI app
app = FastAPI(title="UCP Quickstart Server")
# Simple in-memory database
checkout_sessions = {}
// server.ts
import express from 'express';
import { v4 as uuidv4 } from 'uuid';
// Import validation schemas from JS SDK
import {
CheckoutCreateRequestSchema,
CheckoutResponseSchema,
type CheckoutResponse
} from '@ucp-js/sdk';
// Initialize Express app
const app = express();
app.use(express.json());
// Simple in-memory database
const checkoutSessions: Record<string, CheckoutResponse> = {};
2. Create Checkout Endpoint (Route & Header Validation)¶
Define the endpoint to create a checkout session. UCP requires Idempotency-Key and UCP-Agent headers.
# main.py
@app.post(
"/checkout-sessions",
response_model=Checkout,
status_code=status.HTTP_201_CREATED,
response_model_exclude_none=True
)
async def create_checkout(
body: CheckoutCreateRequest,
idempotency_key: Annotated[str, Header(alias="Idempotency-Key")],
ucp_agent: Annotated[str, Header(alias="UCP-Agent")]
):
"""Create a new UCP checkout session."""
# Note: In a production environment, you must use the Idempotency-Key
# to prevent duplicate processing of the same request.
# Generate a unique checkout session ID
session_id = f"chk_{uuid.uuid4().hex[:10]}"
// server.ts
app.post('/checkout-sessions', (req, res) => {
// 1. Validate required UCP headers
const idempotencyKey = req.header('Idempotency-Key');
const ucpAgent = req.header('UCP-Agent');
if (!idempotencyKey || !ucpAgent) {
return res.status(400).json({
error: 'Missing required headers (Idempotency-Key, UCP-Agent)'
});
}
// 2. Validate request body against UCP schema using Zod
const validation = CheckoutCreateRequestSchema.safeParse(req.body);
if (!validation.success) {
return res.status(400).json({ errors: validation.error.errors });
}
const body = validation.data;
// Generate a unique checkout session ID
const sessionId = `chk_${uuidv4().replace(/-/g, '').substring(0, 10)}`;
Note
If the required UCP headers are missing, FastAPI (Python) will automatically return an HTTP 422 Unprocessable Entity error due to its built-in validation. In our Express (Node.js) implementation, we manually return an HTTP 400 Bad Request error. For simplicity, this quickstart returns plain JSON errors (such as {"error": "..."} or FastAPI's {"detail": "..."}) for missing headers, validation failures, and 404 responses; production servers must return the specification's standard error format (see Error Responses and Core Specification — Error Handling).
3. Business Logic (Process Items & Calculate Totals)¶
Process the incoming line items, resolve their prices, and calculate the subtotal, tax, and total. Prices are always in minor units (e.g., cents for USD).
# main.py
# Map input line items to output line items with pricing
output_line_items = []
subtotal = 0
tax = 0
for index, item_req in enumerate(body.line_items):
# Mock product database lookup
# In production, perform real-time inventory verification and stock
# reservation here (catalog availability signals are advisory and can drift).
price = 2500 # $25.00 in minor units (cents)
title = f"Flower Bouquet {item_req.item.id}"
item_subtotal = price * item_req.quantity
item_tax = int(item_subtotal * 0.08) # 8% tax
item_total = item_subtotal + item_tax
subtotal += item_subtotal
tax += item_tax
output_line_items.append(
LineItem(
id=f"li_{index}",
item=Item(id=item_req.item.id, title=title, price=price),
quantity=item_req.quantity,
totals=[
Total(type="subtotal", amount=item_subtotal),
Total(type="tax", amount=item_tax),
Total(type="total", amount=item_total)
]
)
)
total = subtotal + tax
// server.ts
// Map input line items to output line items with pricing
const outputLineItems = body.line_items.map((item, index) => {
// Mock product database lookup
// In production, perform real-time inventory verification and stock
// reservation here (catalog availability signals are advisory and can drift).
const price = 2500; // $25.00 in minor units (cents)
const title = `Flower Bouquet ${item.item.id}`;
const itemSubtotal = price * item.quantity;
const itemTax = Math.floor(itemSubtotal * 0.08); // 8% tax
const itemTotal = itemSubtotal + itemTax;
return {
id: `li_${index}`,
item: { id: item.item.id, title, price },
quantity: item.quantity,
totals: [
{ type: 'subtotal', amount: itemSubtotal },
{ type: 'tax', amount: itemTax },
{ type: 'total', amount: itemTotal }
]
};
});
// Calculate order totals from line items
const subtotal = outputLineItems.reduce((acc, item) => {
const subtotalEntry = item.totals.find(t => t.type === 'subtotal');
return acc + (subtotalEntry ? subtotalEntry.amount : 0);
}, 0);
const tax = outputLineItems.reduce((acc, item) => {
const taxEntry = item.totals.find(t => t.type === 'tax');
return acc + (taxEntry ? taxEntry.amount : 0);
}, 0);
const total = subtotal + tax;
4. UCP Response Construction¶
Construct the UCP metadata block, advertising supported payment handlers, and assemble the final checkout response.
Capability Negotiation
In production, a business uses the UCP-Agent header to resolve the platform's profile, calculates the intersection of supported capabilities, and returns the active negotiated set in capabilities. For simplicity in this quickstart, full capability negotiation is omitted. See Discovery & Capability Negotiation for details.
# main.py
# Configure available payment handlers.
# We advertise support for a generic mock payment handler.
payment_handlers = {
"com.example.mock_pay": [
PaymentHandlerResponse(
id="mock_pay_handler_1",
version="2026-08-25",
available_instruments=[
AvailablePaymentInstrument(type="mock_instrument")
]
)
]
}
# Construct UCP protocol metadata
# (In production, populate active capabilities from UCP-Agent negotiation)
ucp_metadata = ResponseCheckoutSchema(
version="2026-08-25",
status="success",
payment_handlers=payment_handlers
)
# Assemble the final Checkout payload
checkout = Checkout(
ucp=ucp_metadata,
id=session_id,
status="incomplete",
currency="USD",
line_items=output_line_items,
totals=[
Total(type="subtotal", amount=subtotal),
Total(type="tax", amount=tax),
Total(type="total", amount=total)
],
links=[
Link(type="terms_of_service", url="https://example.com/terms"),
Link(type="privacy_policy", url="https://example.com/privacy")
]
)
# Save to database and return
checkout_sessions[session_id] = checkout
return checkout
// server.ts
// Configure available payment handlers.
// We advertise support for a generic mock payment handler.
const ucpMetadata = {
version: '2026-08-25',
status: 'success' as const,
capabilities: {}, // In production, populate via capability negotiation from UCP-Agent
payment_handlers: {
'com.example.mock_pay': [
{
id: 'mock_pay_handler_1',
version: '2026-08-25',
available_instruments: [
{ type: 'mock_instrument' }
]
}
]
}
};
// Assemble the final Checkout payload
const checkout: CheckoutResponse = {
ucp: ucpMetadata,
id: sessionId,
status: 'incomplete',
currency: 'USD',
line_items: outputLineItems,
totals: [
{ type: 'subtotal', amount: subtotal },
{ type: 'tax', amount: tax },
{ type: 'total', amount: total }
],
links: [
{ type: 'terms_of_service', url: 'https://example.com/terms' },
{ type: 'privacy_policy', url: 'https://example.com/privacy' }
]
};
// Validate output matches CheckoutResponse schema before sending
const outputValidation = CheckoutResponseSchema.safeParse(checkout);
if (!outputValidation.success) {
console.error('Output validation failed:', outputValidation.error);
return res.status(500).json({ error: 'Internal server error' });
}
// Save to database and return
checkoutSessions[sessionId] = checkout;
res.status(201).json(checkout);
});
5. Get Checkout Endpoint¶
Implement the retrieval route so the platform can fetch the checkout state. Per UCP Header Requirements, UCP requires the UCP-Agent header on all requests (not just mutating operations like create, update, or complete).
# main.py
@app.get(
"/checkout-sessions/{id}",
response_model=Checkout,
response_model_exclude_none=True
)
async def get_checkout(
id: str,
ucp_agent: Annotated[str, Header(alias="UCP-Agent")]
):
"""Retrieve an existing checkout session."""
if id not in checkout_sessions:
raise HTTPException(status_code=404, detail="Checkout session not found")
return checkout_sessions[id]
// server.ts
app.get('/checkout-sessions/:id', (req, res) => {
// 1. Validate required UCP headers
const ucpAgent = req.header('UCP-Agent');
if (!ucpAgent) {
return res.status(400).json({
error: 'Missing required header (UCP-Agent)'
});
}
const session = checkoutSessions[req.params.id];
if (!session) {
return res.status(404).json({ error: 'Checkout session not found' });
}
res.json(session);
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Running the Server¶
Your server is now running at http://127.0.0.1:8080 (Python) or http://127.0.0.1:3000 (Node.js).
Testing the Server¶
You can test your server using curl.
1. Create a Checkout Session¶
Send a POST request to create a checkout session with one item:
You should receive a response containing the UCP metadata, calculated totals, and the configured payment handler:
{
"ucp": {
"version": "2026-08-25",
"status": "success",
"payment_handlers": {
"com.example.mock_pay": [
{
"version": "2026-08-25",
"id": "mock_pay_handler_1",
"available_instruments": [
{
"type": "mock_instrument"
}
]
}
]
}
},
"id": "chk_...",
"line_items": [
{
"id": "li_0",
"item": {
"id": "prod_roses",
"title": "Flower Bouquet prod_roses",
"price": 2500
},
"quantity": 2,
"totals": [
{
"type": "subtotal",
"amount": 5000
},
{
"type": "tax",
"amount": 400
},
{
"type": "total",
"amount": 5400
}
]
}
],
"status": "incomplete",
"currency": "USD",
"totals": [
{
"type": "subtotal",
"amount": 5000
},
{
"type": "tax",
"amount": 400
},
{
"type": "total",
"amount": 5400
}
],
"links": [
{
"type": "terms_of_service",
"url": "https://example.com/terms"
},
{
"type": "privacy_policy",
"url": "https://example.com/privacy"
}
]
}
2. Retrieve the Checkout Session¶
Retrieve the session using the id returned from the previous step:
Full File Reference¶
If you want to verify your code, expand the section below to see the complete files.
Complete Code Reference
main.py
# main.py
import uuid
from typing import Annotated
from fastapi import FastAPI, Header, HTTPException, status
# Import UCP SDK models
from ucp_sdk.models.schemas.ucp import ResponseCheckoutSchema
from ucp_sdk.models.schemas.shopping.checkout import Checkout
from ucp_sdk.models.schemas.shopping.checkout_create_request import CheckoutCreateRequest
from ucp_sdk.models.schemas.shopping.types.line_item import LineItem
from ucp_sdk.models.schemas.shopping.types.item import Item
from ucp_sdk.models.schemas.common.types.totals import Total
from ucp_sdk.models.schemas.common.types.link import Link
from ucp_sdk.models.schemas.common.types.available_payment_instrument import AvailablePaymentInstrument
from ucp_sdk.models.schemas.payment_handler import ResponseSchema as PaymentHandlerResponse
# Initialize FastAPI app
app = FastAPI(title="UCP Quickstart Server")
# Simple in-memory database
checkout_sessions = {}
@app.post(
"/checkout-sessions",
response_model=Checkout,
status_code=status.HTTP_201_CREATED,
response_model_exclude_none=True
)
async def create_checkout(
body: CheckoutCreateRequest,
idempotency_key: Annotated[str, Header(alias="Idempotency-Key")],
ucp_agent: Annotated[str, Header(alias="UCP-Agent")]
):
"""Create a new UCP checkout session."""
# Note: In a production environment, you must use the Idempotency-Key
# to prevent duplicate processing of the same request.
# Generate a unique checkout session ID
session_id = f"chk_{uuid.uuid4().hex[:10]}"
# Map input line items to output line items with pricing
output_line_items = []
subtotal = 0
tax = 0
for index, item_req in enumerate(body.line_items):
# Mock product database lookup
# In production, perform real-time inventory verification and stock
# reservation here (catalog availability signals are advisory and can drift).
price = 2500 # $25.00 in minor units (cents)
title = f"Flower Bouquet {item_req.item.id}"
item_subtotal = price * item_req.quantity
item_tax = int(item_subtotal * 0.08) # 8% tax
item_total = item_subtotal + item_tax
subtotal += item_subtotal
tax += item_tax
output_line_items.append(
LineItem(
id=f"li_{index}",
item=Item(id=item_req.item.id, title=title, price=price),
quantity=item_req.quantity,
totals=[
Total(type="subtotal", amount=item_subtotal),
Total(type="tax", amount=item_tax),
Total(type="total", amount=item_total)
]
)
)
total = subtotal + tax
# Configure available payment handlers.
# We advertise support for a generic mock payment handler.
payment_handlers = {
"com.example.mock_pay": [
PaymentHandlerResponse(
id="mock_pay_handler_1",
version="2026-08-25",
available_instruments=[
AvailablePaymentInstrument(type="mock_instrument")
]
)
]
}
# Construct UCP protocol metadata
# (In production, populate active capabilities from UCP-Agent negotiation)
ucp_metadata = ResponseCheckoutSchema(
version="2026-08-25",
status="success",
payment_handlers=payment_handlers
)
# Assemble the final Checkout payload
checkout = Checkout(
ucp=ucp_metadata,
id=session_id,
status="incomplete",
currency="USD",
line_items=output_line_items,
totals=[
Total(type="subtotal", amount=subtotal),
Total(type="tax", amount=tax),
Total(type="total", amount=total)
],
links=[
Link(type="terms_of_service", url="https://example.com/terms"),
Link(type="privacy_policy", url="https://example.com/privacy")
]
)
# Save to database and return
checkout_sessions[session_id] = checkout
return checkout
@app.get(
"/checkout-sessions/{id}",
response_model=Checkout,
response_model_exclude_none=True
)
async def get_checkout(
id: str,
ucp_agent: Annotated[str, Header(alias="UCP-Agent")]
):
"""Retrieve an existing checkout session."""
if id not in checkout_sessions:
raise HTTPException(status_code=404, detail="Checkout session not found")
return checkout_sessions[id]
server.ts
// server.ts
import express from 'express';
import { v4 as uuidv4 } from 'uuid';
// Import validation schemas from JS SDK
import {
CheckoutCreateRequestSchema,
CheckoutResponseSchema,
type CheckoutResponse
} from '@ucp-js/sdk';
// Initialize Express app
const app = express();
app.use(express.json());
// Simple in-memory database
const checkoutSessions: Record<string, CheckoutResponse> = {};
app.post('/checkout-sessions', (req, res) => {
// 1. Validate required UCP headers
const idempotencyKey = req.header('Idempotency-Key');
const ucpAgent = req.header('UCP-Agent');
if (!idempotencyKey || !ucpAgent) {
return res.status(400).json({
error: 'Missing required headers (Idempotency-Key, UCP-Agent)'
});
}
// 2. Validate request body against UCP schema using Zod
const validation = CheckoutCreateRequestSchema.safeParse(req.body);
if (!validation.success) {
return res.status(400).json({ errors: validation.error.errors });
}
const body = validation.data;
// Generate a unique checkout session ID
const sessionId = `chk_${uuidv4().replace(/-/g, '').substring(0, 10)}`;
// Map input line items to output line items with pricing
const outputLineItems = body.line_items.map((item, index) => {
// Mock product database lookup
// In production, perform real-time inventory verification and stock
// reservation here (catalog availability signals are advisory and can drift).
const price = 2500; // $25.00 in minor units (cents)
const title = `Flower Bouquet ${item.item.id}`;
const itemSubtotal = price * item.quantity;
const itemTax = Math.floor(itemSubtotal * 0.08); // 8% tax
const itemTotal = itemSubtotal + itemTax;
return {
id: `li_${index}`,
item: { id: item.item.id, title, price },
quantity: item.quantity,
totals: [
{ type: 'subtotal', amount: itemSubtotal },
{ type: 'tax', amount: itemTax },
{ type: 'total', amount: itemTotal }
]
};
});
// Calculate order totals from line items
const subtotal = outputLineItems.reduce((acc, item) => {
const subtotalEntry = item.totals.find(t => t.type === 'subtotal');
return acc + (subtotalEntry ? subtotalEntry.amount : 0);
}, 0);
const tax = outputLineItems.reduce((acc, item) => {
const taxEntry = item.totals.find(t => t.type === 'tax');
return acc + (taxEntry ? taxEntry.amount : 0);
}, 0);
const total = subtotal + tax;
// Configure available payment handlers.
// We advertise support for a generic mock payment handler.
const ucpMetadata = {
version: '2026-08-25',
status: 'success' as const,
capabilities: {}, // In production, populate via capability negotiation from UCP-Agent
payment_handlers: {
'com.example.mock_pay': [
{
id: 'mock_pay_handler_1',
version: '2026-08-25',
available_instruments: [
{ type: 'mock_instrument' }
]
}
]
}
};
// Assemble the final Checkout payload
const checkout: CheckoutResponse = {
ucp: ucpMetadata,
id: sessionId,
status: 'incomplete',
currency: 'USD',
line_items: outputLineItems,
totals: [
{ type: 'subtotal', amount: subtotal },
{ type: 'tax', amount: tax },
{ type: 'total', amount: total }
],
links: [
{ type: 'terms_of_service', url: 'https://example.com/terms' },
{ type: 'privacy_policy', url: 'https://example.com/privacy' }
]
};
// Validate output matches CheckoutResponse schema before sending
const outputValidation = CheckoutResponseSchema.safeParse(checkout);
if (!outputValidation.success) {
console.error('Output validation failed:', outputValidation.error);
return res.status(500).json({ error: 'Internal server error' });
}
// Save to database and return
checkoutSessions[sessionId] = checkout;
res.status(201).json(checkout);
});
app.get('/checkout-sessions/:id', (req, res) => {
// 1. Validate required UCP headers
const ucpAgent = req.header('UCP-Agent');
if (!ucpAgent) {
return res.status(400).json({
error: 'Missing required header (UCP-Agent)'
});
}
const session = checkoutSessions[req.params.id];
if (!session) {
return res.status(404).json({ error: 'Checkout session not found' });
}
res.json(session);
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Next Steps¶
To build a fully compliant UCP server, you will also need to:
- Implement the checkout update endpoint (
PUT /checkout-sessions/{id}) to handle buyer information updates (like shipping address). - Implement the checkout completion endpoint (
POST /checkout-sessions/{id}/complete) to process the payment instrument provided by the platform. - Advertise your service using a UCP Discovery Profile at
/.well-known/ucp. - Run the conformance suite from the UCP Conformance repository against your server to verify protocol compliance.
For a complete reference implementation, check out the UCP Samples repository.