Skip to main content
BLOG

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.

By IMozzUpdated 2026-10-05
Publish your UCP profile — aiSiteReady

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. Only ucp.version, ucp.services, and ucp.payment_handlers are required, and the last two must exist even when empty (UCP spec).
  • Serve it over HTTPS with no redirects and Cache-Control: public with a max-age of 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[] replaces signing_keys[], fulfillment config flags were renamed, buyer consent was reshaped, and payment extensions moved to dev.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.

Anatomy of a UCP business profile. Five blocks of the JSON document each answer one agent question: ucp.version and supported_versions answer which protocol dates can we speak; ucp.services answers where do I call and over which transport; ucp.capabilities answers what can I do in this store; ucp.payment_handlers answers how can the buyer pay; the top-level keys array answers how do I verify your signatures. Version, services, and payment_handlers are required, capabilities and keys are optional.

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).

MemberWhat you setWhat to know
ucp.versionA date, YYYY-MM-DDRequired. Must be a dated release, never a string like draft
ucp.supported_versionsOlder date mapped to a profile URLOptional. Each URL serves a complete profile for that date
ucp.servicesKey dev.ucp.shopping, one entry per transportRequired, may be empty. Entry: version, transport, endpoint, schema, spec
ucp.capabilitiesKey per capability, array of versioned entriesOptional. Entry: version, schema (required for businesses), spec, extends, config
ucp.payment_handlersKey per handler, array of entriesRequired, may be empty. Entry: id, version, spec, schema, config
keysPublic JWKs: kid, kty, crv, x, y, algOptional. 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).

RequirementLevelSource and notes
HTTPSMUSTUCP spec
No redirects (3xx) on the profile endpointMUST NOTUCP spec; platforms do not follow them
Cache-Control with public and max-age of 60 or moreMUSTUCP spec; never private, no-store, or no-cache
ETag or Last-ModifiedSHOULDUCP spec, so consumers can revalidate
Reachable without authenticationRequired in practiceGoogle's guide says the file must be publicly accessible without authentication
Content-Type: application/jsonNot stated for the profileThe spec's REST transport requires it for API traffic; Shopify serves it
Access-Control-Allow-OriginNot in the specI 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).

  1. Keep a business capability only if the agent lists a capability with the same name.
  2. Among the versions both sides list, select the highest. If none is shared, drop the capability.
  3. Prune extensions whose extends parents are all absent from the result.
  4. Repeat the pruning until nothing else is removed.

Negotiation between two profiles. The business profile lists checkout, fulfillment, discount, and order; the agent profile lists checkout, fulfillment, buyer_consent, and cart. Matching names at the same exact version, 2026-08-25, survive, and extensions without a parent are pruned. The result is the active set for this session: checkout and fulfillment. Discount and order exist only on the business side, buyer_consent and cart only on the agent side, so none of them is active.

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).

CodeMeaningREST status
invalid_profile_urlProfile URL malformed, missing, or unresolvable400
profile_unreachableResolved, but the fetch failed or returned non-2xx424
profile_malformedNot valid JSON, or violates the schema422
version_unsupportedThe agent's protocol version is not supported422
capabilities_incompatibleThe intersection is empty200

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.

AreaBefore (2026-04-08)After (2026-08-25)
Signing keysTop-level signing_keys[]Top-level keys[], a JWK Set; signing_keys[] removed
Fulfillment configallows_multi_destination, allows_method_combinationsmulti_destination (array of objects), method_combinations
Buyer consentFixed booleans such as marketingMap keyed by reverse-domain purposes such as dev.ucp.consent.marketing
Payment extensionsdev.ucp.shopping.* namesdev.ucp.common.payment.*, for example ap2_mandate
Version contractDated versions, rules less explicitClarified 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.

MistakeWhat happensFix
Copying an early-2026 samplecapabilities as an array with name fields and a top-level payment.handlers fail the current schemaStart 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 aloneSchema passes; a platform rejects each dev.ucp.* entry with another versionBump every entry together
No payment_handlers on a catalog-only storeSchema error: the member must existPublish "payment_handlers": {}
Redirecting /.well-known/ucpThe spec forbids redirects on the profile endpointServe a direct 200
no-store, private, a tiny max-age, or an HTML 200 from an app fallbackCaching rules broken, or the body is not JSONFix headers; return 404 for a missing file
schema origin that does not match the nameThe entity is rejected and never activatedHost schemas under the matching domain
Bot protection or login in front of the fileGoogle requires the file to be public; scanners and agents see 403 or 429Exempt 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.

Frequently asked questions

Is /.well-known/ucp a product feed?
No. It is a capability profile: protocol version, services with their endpoints, capabilities, payment handlers, and optional signing keys. It contains no products. An agent reads it first, then calls the catalog or checkout endpoints it declares. Product data still belongs in your structured data and catalog capabilities, so the profile and your Product markup solve different problems.
Do Shopify stores need to publish a UCP profile themselves?
Generally no. Shopify's documentation places the business profile at the storefront's /.well-known/ucp, and a Shopify staff reply on the developer forum says Shopify generates it and there is no supported way to edit or replace it. Custom and non-Shopify stores publish their own file and run the endpoints it declares. Always fetch your own domain to see what is served.
Which HTTP headers does /.well-known/ucp need?
The spec requires HTTPS, no redirects, and Cache-Control with public and a max-age of at least 60 seconds, never private, no-store, or no-cache. An ETag or Last-Modified is recommended. Serve JSON as application/json. The spec does not mention CORS, though a Shopify-hosted profile sends Access-Control-Allow-Origin with a wildcard, and the aiSiteReady check looks for it.
What changed in UCP release v2026-08-25 for merchants?
Four things touch a profile. A top-level keys array replaces signing_keys. Fulfillment config flags were renamed, so allows_multi_destination became multi_destination. Buyer consent moved from fixed booleans to a map keyed by reverse-domain purposes. Payment extensions moved under dev.ucp.common.payment. Every dev.ucp entry must also declare the profile's own version.