How to publish /.well-known/ucp: UCP merchant profile guide
Publish /.well-known/ucp so AI shopping agents see what your store supports: 3 required fields, hosting rules, negotiation and the v2026-08-25 breaking changes.

To publish a Universal Commerce Protocol (UCP) merchant profile, serve one JSON document at /.well-known/ucp that declares your protocol version, your services and endpoints, your capabilities, and your payment handlers. An AI shopping agent fetches that file first, compares it with its own profile, and uses only what both sides support (UCP specification).
The profile is the entry point of the commerce layer in the readiness stack: before an agent can buy anything, it has to learn what your store can do. This guide is the UCP deep dive behind the agentic commerce checklist for stores. Product data itself is the job of structured data, and the wider picture lives in what AI agent readiness means.
One caveat first: UCP is young. Shopify and Google published it on 11 January 2026 (Shopify Engineering, Google Developers Blog), and the spec has shipped four dated releases, the latest on 25 August 2026 (releases). Support is uneven across versions: on 2 October 2026 Google's merchant guide still documented profile versions 2026-04-08 and 2026-01-23 (Google), while Shopify storefronts already advertise 2026-08-25 (Shopify changelog).
Key takeaways
- The merchant profile is a JSON file at
/.well-known/ucp. Onlyucp.version,ucp.services, anducp.payment_handlersare required, and the last two must exist even when empty (UCP spec).- Serve it over HTTPS with no redirects and
Cache-Control: publicwith amax-ageof at least 60 seconds (UCP spec, Hosting). The spec has no CORS rule; checking for that header is aiSiteReady's own choice.- The business picks the active capabilities: it intersects its profile with the agent's by exact name and exact dated version, then drops orphaned extensions.
- Release v2026-08-25 broke profiles in four ways:
keys[]replacessigning_keys[], fulfillment config flags were renamed, buyer consent was reshaped, and payment extensions moved todev.ucp.common.payment.*(release notes).- Shopify generates the profile for its storefronts and staff say developers cannot edit it (Shopify forum); custom stores host their own.
What is the /.well-known/ucp merchant profile?
It is the discovery document of a UCP business: a public JSON file that tells an agent what your store supports before the agent sends a single checkout request. Businesses publish it at /.well-known/ucp; platforms, meaning agents and apps, publish theirs at a URL they pass along with every request, so any platform with a discoverable profile can work with any business without prior registration (UCP spec).
The profile is a map, not the API. Every endpoint an agent will call is declared inside it; the shopping REST description says the base URL has to come from the profile's services block (UCP OpenAPI). Each block answers one question.
Two facts are worth knowing before you invest. First, ucp was not in IANA's well-known URI registry as of its 16 September 2026 update, unlike api-catalog and agent-card.json (IANA); the path rests on the spec, not a registration. Second, anyone can contribute, but governance is weighted: the Shopping Tech Council has 16 members, with four permanent seats each for Google and Shopify, eight of 16 votes (UCP governance).
UCP is not the only commerce protocol. It calls itself compatible with AP2 and ships an ap2_mandate extension for signed payments (UCP and AP2). The OpenAI and Stripe Agentic Commerce Protocol has its own /.well-known/acp.json, but its discovery RFC is still marked as a proposal and unreleased (ACP RFC). I leave it out of the rest of this guide.
What goes inside a UCP profile, from minimal to full?
Six members matter, and only three are mandatory. The table follows the business_schema definition in the spec's JSON Schema at release v2026-08-25 (profile.json).
| Member | What you set | What to know |
|---|---|---|
ucp.version | A date, YYYY-MM-DD | Required. Must be a dated release, never a string like draft |
ucp.supported_versions | Older date mapped to a profile URL | Optional. Each URL serves a complete profile for that date |
ucp.services | Key dev.ucp.shopping, one entry per transport | Required, may be empty. Entry: version, transport, endpoint, schema, spec |
ucp.capabilities | Key per capability, array of versioned entries | Optional. Entry: version, schema (required for businesses), spec, extends, config |
ucp.payment_handlers | Key per handler, array of entries | Required, may be empty. Entry: id, version, spec, schema, config |
keys | Public JWKs: kid, kty, crv, x, y, alg | Optional. Public keys only |
Transports are rest, mcp, a2a, and embedded. REST, MCP, and A2A entries need an endpoint; an A2A endpoint points at an agent card (make your site agent-callable). UCP's mcp transport is a commerce API described by an OpenRPC file, which is not the same thing as an MCP Server Card (MCP Server Card explained).
The smallest document that passes the schema has three members, with services and payment_handlers present even when empty (UCP spec).
{
"ucp": {
"version": "2026-08-25",
"services": {},
"payment_handlers": {}
}
}
That file validates and says nothing: no endpoint, no capability, and the spec notes that a useful profile normally advertises at least one capability. The smallest useful profile is read-only: one REST service and the two catalog capabilities, so an agent can search and look up products. It has no payment handlers because nothing is bought yet.
{
"ucp": {
"version": "2026-08-25",
"services": {
"dev.ucp.shopping": [
{
"version": "2026-08-25",
"spec": "https://ucp.dev/2026-08-25/specification/overview/",
"transport": "rest",
"endpoint": "https://shop.example.com/ucp/v1",
"schema": "https://ucp.dev/2026-08-25/services/shopping/rest.openapi.json"
}
]
},
"capabilities": {
"dev.ucp.shopping.catalog.search": [
{
"version": "2026-08-25",
"spec": "https://ucp.dev/2026-08-25/specification/shopping/catalog/search",
"schema": "https://ucp.dev/2026-08-25/schemas/shopping/catalog_search.json"
}
],
"dev.ucp.shopping.catalog.lookup": [
{
"version": "2026-08-25",
"spec": "https://ucp.dev/2026-08-25/specification/shopping/catalog/lookup",
"schema": "https://ucp.dev/2026-08-25/schemas/shopping/catalog_lookup.json"
}
]
},
"payment_handlers": {}
}
}
With this profile an agent resolves a product search to POST https://shop.example.com/ucp/v1/catalog/search, because the OpenAPI paths are appended to the declared endpoint. A store that takes orders through an agent adds checkout, its extensions, the order lifecycle, a payment handler, and a signing key.
{
"ucp": {
"version": "2026-08-25",
"supported_versions": {
"2026-04-08": "https://shop.example.com/.well-known/ucp/2026-04-08"
},
"services": {
"dev.ucp.shopping": [
{
"version": "2026-08-25",
"spec": "https://ucp.dev/2026-08-25/specification/overview/",
"transport": "rest",
"endpoint": "https://shop.example.com/ucp/v1",
"schema": "https://ucp.dev/2026-08-25/services/shopping/rest.openapi.json"
},
{
"version": "2026-08-25",
"spec": "https://ucp.dev/2026-08-25/specification/overview/",
"transport": "mcp",
"endpoint": "https://shop.example.com/ucp/mcp",
"schema": "https://ucp.dev/2026-08-25/services/shopping/mcp.openrpc.json"
}
]
},
"capabilities": {
"dev.ucp.shopping.checkout": [
{
"version": "2026-08-25",
"spec": "https://ucp.dev/2026-08-25/specification/shopping/checkout",
"schema": "https://ucp.dev/2026-08-25/schemas/shopping/checkout.json"
}
],
"dev.ucp.shopping.fulfillment": [
{
"version": "2026-08-25",
"spec": "https://ucp.dev/2026-08-25/specification/shopping/extensions/fulfillment",
"schema": "https://ucp.dev/2026-08-25/schemas/shopping/fulfillment.json",
"extends": "dev.ucp.shopping.checkout",
"config": {
"multi_destination": [{ "method": "shipping" }],
"method_combinations": [["shipping", "pickup"]]
}
}
],
"dev.ucp.shopping.discount": [
{
"version": "2026-08-25",
"spec": "https://ucp.dev/2026-08-25/specification/shopping/extensions/discount",
"schema": "https://ucp.dev/2026-08-25/schemas/shopping/discount.json",
"extends": "dev.ucp.shopping.checkout"
}
],
"dev.ucp.shopping.order": [
{
"version": "2026-08-25",
"spec": "https://ucp.dev/2026-08-25/specification/shopping/order",
"schema": "https://ucp.dev/2026-08-25/schemas/shopping/order.json"
}
]
},
"payment_handlers": {
"com.example.processor_tokenizer": [
{
"id": "processor_tokenizer",
"version": "2026-08-25",
"spec": "https://example.com/specs/payments/processor_tokenizer",
"schema": "https://example.com/specs/payments/merchant_tokenizer.json",
"available_instruments": [
{
"type": "card",
"constraints": {
"properties": { "brand": { "enum": ["visa", "mastercard"] } }
}
}
]
}
]
}
},
"keys": [
{
"kid": "business_2025",
"kty": "EC",
"crv": "P-256",
"x": "qIVYZVLCrPZHGHjP17CTW0_-D9Lfw0EkjqF7xB4FivA",
"y": "Mc4nN9LTDOBhfoUeg8Ye9WedFRhnZXZJA12Qp0zZ6F0",
"use": "sig",
"alg": "ES256"
}
]
}
I validated all three profiles against the JSON Schema in the spec repository at the v2026-08-25 tag, and they pass. Treat the hosts, the handler, and the key as placeholders: real payment handlers come from their providers, and the key must be your own public key. The schema rejects private-key members such as d. Fulfillment and discount carry extends because they are extensions that only work on top of checkout.
One naming rule catches people. Names are reverse-domain: dev.ucp.* belongs to ucp.dev and com.example.* to example.com. A platform must reject an entity whose schema URL origin does not match the domain in its name, and must not follow redirects when fetching a schema (UCP spec, authority binding). Host your own schemas on a domain you control.
Who hosts /.well-known/ucp: Shopify or you?
On Shopify the platform writes the file; on a custom store you write and host it. Shopify's agent documentation places the business profile at the merchant storefront, {shop}.myshopify.com/.well-known/ucp (Shopify developer docs). A Shopify staff reply on the developer forum is blunter: Shopify generates the profile, you should not need to host your own, and there is no supported way to edit or replace it (forum, July 2026). A second reply adds that developers cannot add to or adjust it (forum, August 2026).
Shopify's Help Center says Agentic storefronts is active by default for eligible stores (Shopify Help Center), but that page does not mention UCP, so fetch your own domain instead of assuming. I did that for one Shopify-hosted storefront on its own domain on 2 October 2026. The response was a direct 200 with application/json; charset=utf-8, Cache-Control: public, max-age=60, a weak ETag, and Access-Control-Allow-Origin: *. The 4.8 KB body passed the same schema check, declared version 2026-08-25, listed the two older releases under supported_versions, and carried an MCP endpoint, ten capabilities, and three payment handlers, with no keys[].
On a custom store you publish the file and own every endpoint it declares, so advertise only what your backend serves. A January 2026 forum thread reports a headless Shopify store whose profile appeared on its checkout domain, not its storefront domain (forum); I found no Shopify documentation on proxying it, so check what your own domain returns.
The hosting rules are short and mostly normative (UCP spec, Hosting).
| Requirement | Level | Source and notes |
|---|---|---|
| HTTPS | MUST | UCP spec |
| No redirects (3xx) on the profile endpoint | MUST NOT | UCP spec; platforms do not follow them |
Cache-Control with public and max-age of 60 or more | MUST | UCP spec; never private, no-store, or no-cache |
ETag or Last-Modified | SHOULD | UCP spec, so consumers can revalidate |
| Reachable without authentication | Required in practice | Google's guide says the file must be publicly accessible without authentication |
Content-Type: application/json | Not stated for the profile | The spec's REST transport requires it for API traffic; Shopify serves it |
Access-Control-Allow-Origin | Not in the spec | I found no CORS rule in the spec text; Shopify sends * |
On CORS: a browser lets script read a cross-origin response only if the server opts in with Access-Control-Allow-Origin (MDN). A browser-based agent needs it; a server-side agent does not. A plain GET needs no preflight, so you do not need an OPTIONS handler. The header costs nothing and aiSiteReady looks for it, but it is a courtesy, not a spec rule.
A static file behind nginx is enough, since the spec notes that documented profiles are under 5 KiB. An add_header inside a location replaces headers inherited from the server block, so repeat your security headers there.
location = /.well-known/ucp {
alias /var/www/shop/ucp-profile.json;
default_type application/json;
add_header Cache-Control "public, max-age=300";
add_header Access-Control-Allow-Origin "*";
# nginx adds ETag and Last-Modified for static files by default
}
In a Nuxt app, a Nitro route does the same job. aiSiteReady's own .well-known documents are Nitro routes in server/routes/.well-known/, and the file name ucp.get.ts becomes the route /.well-known/ucp.
// server/routes/.well-known/ucp.get.ts
import profile from "../../data/ucp-profile.json";
export default defineEventHandler((event) => {
setResponseHeader(event, "Content-Type", "application/json");
setResponseHeader(event, "Cache-Control", "public, max-age=300");
setResponseHeader(event, "Access-Control-Allow-Origin", "*");
return profile;
});
How does UCP negotiation turn two profiles into one session?
The business decides. UCP is server-selects: the business chooses the protocol version and the active capabilities from the intersection of both profiles (Google). The agent hosts its own profile at an HTTPS URL and sends that URL with each request, in a UCP-Agent header for HTTP or a meta object for MCP (UCP spec).
POST /ucp/v1/checkout-sessions HTTP/1.1
Host: shop.example.com
UCP-Agent: profile="https://agent.example/profiles/shopping-agent.json"
Content-Type: application/json
The business fetches and validates that profile, then runs the intersection algorithm (UCP spec).
- Keep a business capability only if the agent lists a capability with the same name.
- Among the versions both sides list, select the highest. If none is shared, drop the capability.
- Prune extensions whose
extendsparents are all absent from the result. - Repeat the pruning until nothing else is removed.
Versions are matched by exact equality, not by date order. A dated version does not imply compatibility with later dates, and older dev.ucp.* releases are served only through supported_versions profiles, never as extra entries in the current profile (UCP spec, versioning). The business answers with the negotiated set, so the agent always knows what is active.
{
"ucp": {
"version": "2026-08-25",
"capabilities": {
"dev.ucp.shopping.checkout": [{ "version": "2026-08-25" }],
"dev.ucp.shopping.fulfillment": [{ "version": "2026-08-25" }]
}
}
}
When negotiation fails, the failure is specific, and it can carry a continue_url so the agent can hand the buyer to your normal web checkout (UCP spec, error handling).
| Code | Meaning | REST status |
|---|---|---|
invalid_profile_url | Profile URL malformed, missing, or unresolvable | 400 |
profile_unreachable | Resolved, but the fetch failed or returned non-2xx | 424 |
profile_malformed | Not valid JSON, or violates the schema | 422 |
version_unsupported | The agent's protocol version is not supported | 422 |
capabilities_incompatible | The intersection is empty | 200 |
What changed in UCP release v2026-08-25?
The four dated releases are 2026-01-11, 2026-01-23, 2026-04-08, and 2026-08-25 (releases). The latest notes list several backward-incompatible changes (release notes); these are the ones that touch a profile.
| Area | Before (2026-04-08) | After (2026-08-25) |
|---|---|---|
| Signing keys | Top-level signing_keys[] | Top-level keys[], a JWK Set; signing_keys[] removed |
| Fulfillment config | allows_multi_destination, allows_method_combinations | multi_destination (array of objects), method_combinations |
| Buyer consent | Fixed booleans such as marketing | Map keyed by reverse-domain purposes such as dev.ucp.consent.marketing |
| Payment extensions | dev.ucp.shopping.* names | dev.ucp.common.payment.*, for example ap2_mandate |
| Version contract | Dated versions, rules less explicit | Clarified and tightened: every dev.ucp.* entry declares the profile's version |
The last row is listed as a clarification, not a breaking change, but it bites: a platform must reject any dev.ucp.* entry whose version differs from ucp.version (UCP spec). The fulfillment rename is the one you can see in the profile itself, first as the 2026-04-08 spec shows it, then as 2026-08-25 does.
{
"config": {
"allows_multi_destination": { "shipping": true },
"allows_method_combinations": [["shipping", "pickup"]]
}
}
{
"config": {
"multi_destination": [{ "method": "shipping" }],
"method_combinations": [["shipping", "pickup"]]
}
}
The upgrade path is short. Serve the new version at /.well-known/ucp. Keep the older release as a complete profile at its own URL, such as /.well-known/ucp/2026-04-08, and map it in supported_versions; that leaf profile must not carry a supported_versions of its own (UCP spec, protocol version). Bump every dev.ucp.* entry together with ucp.version. To hear about breaking changes early, watch the repository: breaking pull requests are announced in GitHub Discussions two weeks before merging (UCP versioning).
What mistakes break a UCP profile?
Most failures are one of eight patterns, and the nasty ones pass schema validation, because the schema is open to unknown members and the version rules live in prose.
| Mistake | What happens | Fix |
|---|---|---|
| Copying an early-2026 sample | capabilities as an array with name fields and a top-level payment.handlers fail the current schema | Start from the current spec example |
Keys left in signing_keys[] | Schema passes, but a 2026-08-25 verifier reads keys[] | Move the keys |
Bumping ucp.version alone | Schema passes; a platform rejects each dev.ucp.* entry with another version | Bump every entry together |
No payment_handlers on a catalog-only store | Schema error: the member must exist | Publish "payment_handlers": {} |
Redirecting /.well-known/ucp | The spec forbids redirects on the profile endpoint | Serve a direct 200 |
no-store, private, a tiny max-age, or an HTML 200 from an app fallback | Caching rules broken, or the body is not JSON | Fix headers; return 404 for a missing file |
schema origin that does not match the name | The entity is rejected and never activated | Host schemas under the matching domain |
| Bot protection or login in front of the file | Google requires the file to be public; scanners and agents see 403 or 429 | Exempt the path from challenges |
The first row is easy to verify. The January 2026 Google Developers Blog sample showed capabilities as an array of objects with name, and payment.handlers outside ucp (Google Developers Blog). Run through the current schema, capabilities fails the object check, services fails the array check, and payment_handlers is reported missing. Examples age; copy from the spec of your release.
How do you verify your profile with curl?
Check the headers first, then the body, and run both against the public edge URL, not just localhost. Do not pass -L: a redirect on this path is a bug, and -L would hide it.
curl -sS -D - -o /dev/null -H 'Accept: application/json' https://shop.example.com/.well-known/ucp
You should see a direct 200, a JSON type, and the caching headers.
HTTP/2 200
content-type: application/json; charset=utf-8
cache-control: public, max-age=300
etag: "7f3a1c"
access-control-allow-origin: *
Then read what an agent would read. This summary prints the version, the older releases you support, and the registry keys.
curl -sS https://shop.example.com/.well-known/ucp | jq '{
version: .ucp.version,
older: (.ucp.supported_versions // {} | keys),
services: (.ucp.services | keys),
capabilities: (.ucp.capabilities | keys),
handlers: (.ucp.payment_handlers | keys),
keys: [.keys[]?.kid]
}'
The output for the full profile above has this shape.
{
"version": "2026-08-25",
"older": ["2026-04-08"],
"services": ["dev.ucp.shopping"],
"capabilities": [
"dev.ucp.shopping.checkout",
"dev.ucp.shopping.discount",
"dev.ucp.shopping.fulfillment",
"dev.ucp.shopping.order"
],
"handlers": ["com.example.processor_tokenizer"],
"keys": ["business_2025"]
}
Wire the rules into CI so a release cannot ship a broken profile. This script checks status, headers, required members, the keys[] rename, and version drift. I tested it against a local server serving the good profile and five broken variants, and it flagged each one.
#!/usr/bin/env bash
# usage: check-ucp.sh https://shop.example.com
set -uo pipefail
URL="${1:?usage: check-ucp.sh https://shop.example.com}"
URL="${URL%/}/.well-known/ucp"
HDR="$(mktemp)"; BODY="$(mktemp)"; fail=0
bad() { echo "FAIL: $1"; fail=1; }
# No -L on purpose: the spec forbids redirects on the profile endpoint.
CODE="$(curl -sS -m 15 -H 'Accept: application/json' -D "$HDR" -o "$BODY" -w '%{http_code}' "$URL")" \
|| { echo "FAIL: request error"; exit 1; }
[ "$CODE" = "200" ] || { echo "FAIL: status $CODE (expected a direct 200)"; exit 1; }
grep -qi '^content-type: *application/json' "$HDR" || bad "Content-Type is not application/json"
CC="$(grep -i '^cache-control:' "$HDR" | head -1)"
grep -qi 'public' <<<"$CC" || bad "Cache-Control lacks public"
grep -qiE 'private|no-store|no-cache' <<<"$CC" && bad "forbidden Cache-Control directive"
MAXAGE="$(grep -oi 'max-age=[0-9]*' <<<"$CC" | head -1 | cut -d= -f2)"
[ "${MAXAGE:-0}" -ge 60 ] || bad "max-age is missing or below 60"
grep -qiE '^(etag|last-modified):' "$HDR" || echo "WARN: no ETag or Last-Modified"
grep -qi '^access-control-allow-origin:' "$HDR" || echo "WARN: no CORS header (not in the spec)"
jq -e 'type == "object"' "$BODY" >/dev/null 2>&1 || { bad "body is not a JSON object"; exit 1; }
V="$(jq -r '.ucp.version // empty' "$BODY")"
[[ "$V" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || bad "ucp.version is missing or not YYYY-MM-DD"
jq -e '.ucp.services | type == "object"' "$BODY" >/dev/null 2>&1 || bad "ucp.services must be an object"
jq -e '.ucp.payment_handlers | type == "object"' "$BODY" >/dev/null 2>&1 \
|| bad "ucp.payment_handlers must exist, even when empty"
if [[ ! "$V" < "2026-08-25" ]]; then
jq -e 'has("signing_keys") | not' "$BODY" >/dev/null || bad "signing_keys was removed; publish keys[]"
fi
# Every dev.ucp.* entry must carry the profile's own version.
stale() { jq -r --arg v "$V" --arg f "$1" \
'[(.ucp[$f] // {}) | to_entries[] | select(.key | startswith("dev.ucp.")) | .key as $k | .value[] | select(.version != $v) | $k] | unique | join(", ")' "$BODY"; }
CAPS="$(stale capabilities)"; SVCS="$(stale services)"
[ -z "$CAPS" ] || bad "capabilities not at ucp.version $V: $CAPS"
[ -z "$SVCS" ] || echo "WARN: services not at ucp.version $V: $SVCS"
[ "$fail" = 0 ] && echo "OK: $URL ($V)"
exit "$fail"
For a full structural check, validate the body against source/schemas/profile.json, definition business_schema, from the spec repository at your release tag, using any JSON Schema 2020-12 validator. Neither the script nor the schema proves that your endpoints work. Call each declared endpoint with a test agent profile before you rely on the profile.
How do you check your whole store at once?
Two commerce checks in aiSiteReady touch this file. Both run under the E-commerce and All checks scan profiles and are switched off for Content site and API/Application. Probes are read-only and SSRF-safe. The exact checks and weights live on the methodology page.
The agentic commerce signals check (agentic_commerce_signals) requests your home page and, only if that returns a 2xx, probes /.well-known/ucp and /.well-known/acp.json in parallel with an Accept header that prefers application/json, at most three requests in total. A UCP document counts as valid when the response is 2xx, has a JSON media type, parses to a JSON object, and carries ucp.version as a YYYY-MM-DD string. A valid document that also sends an Access-Control-Allow-Origin header is a PASS; without that header the check reports a PARTIAL. A missing document never turns the check into an error. If your home page answers 403 or 429, the check reports UNKNOWN instead of failing you, and a site with no commerce surface at all gets N/A.
I built the probe narrow on purpose, so know where it stops. It checks ucp.version only, not services, capabilities, keys, or the version rules above. It is version-tolerant, because a scanner that hard-codes today's release would mislabel valid profiles after the next one. It never calls the endpoints you declare. It follows up to five redirects, so it can pass a profile that the spec says should not redirect; keep using curl without -L. The CORS rule is ours, not the spec's. The product feed hints check (product_feed_hints) requests the same path and counts any JSON object as a catalog-discovery hint. Both checks are low severity, so a missing profile moves the score modestly.
Pair this guide with the checklist and structured data linked above, and run the 30-minute agent readiness audit for the rest of the site. Run a free scan to see whether an agent can find and read your /.well-known/ucp, which commerce signals it sees, and which fixes to make first, in English, Ukrainian, or Russian.
The short version: UCP asks for one public JSON file, served over HTTPS without redirects and with real caching headers. It must list only the endpoints and capabilities you actually run, and its dates must agree with each other. If you are on Shopify, check what is already served. If you are not, publish it yourself and test it like any other API.
IMozz has 20 years in software development, with the past year spent building with LLMs. He builds aiSiteReady, a read-only scanner that checks whether AI agents can read a site. It server-renders its own content as a working example.