Reference
What /.well-known/ucp is, and how to serve it correctly
One file on your own domain decides whether an agent following the Universal Commerce Protocol can identify your store at all. This is what belongs in it, the five rules the specification sets for serving it, and the recipe for each platform.
What the file is
An agent that wants to transact with a business does not read your homepage. It fetches a JSON document from a fixed path on your domain, reads which protocol version you speak and which services and capabilities you expose, intersects that with its own, and negotiates from there. That document is the business profile, and the path is fixed.
“Profile documents are machine-readable discovery documents. Businesses publish their profile at/.well-known/ucp… A profile document is a JSON object with a requireducpmember. For both business and platform profiles,ucp.version,ucp.servicesanducp.payment_handlersare required. The services and payment_handlers registries MUST be present even when empty.ucp.capabilitiesis optional and MAY be omitted, though useful commerce profiles normally advertise at least one capability.”
Two things follow from that paragraph and are worth stating before anything else. The profile carries protocol metadata and nothing else: there is no field for your company name, your description or a contact address, so a profile is not an “AI business card” however often it is described as one. And the smallest structurally valid document is almost empty:
{
"ucp": {
"version": "2026-08-25",
"services": {},
"payment_handlers": {}
}
}That document is conformant and worthless. It tells an agent you speak the protocol and expose nothing it can act on. Publishing it to make a checker go green is the one mistake this whole page is trying to prevent.
The five rules for serving it
Conformance is not only about the JSON. The specification puts normative requirements on the response itself, under Profile Requirements → Hosting.
“Published artifacts MUST be served over HTTPS. Profile endpoints MUST NOT use redirects (3xx). Published artifacts MUST include aCache-Controlheader withpublicandmax-ageof at least 60 seconds, and MUST NOT be served withprivate,no-store, orno-cachedirectives. Published artifacts SHOULD include a validator (ETagorLast-Modified) so consumers can revalidate cached copies efficiently.”
| Rule | The requirement | What goes wrong |
|---|---|---|
| HTTPS | Published artifacts MUST be served over HTTPS. | An agent rejects a profile URL that is not https, without fetching it. |
| No redirects | The profile endpoint MUST NOT answer with a 3xx. | Discovery fails. This is the rule that catches most stores, so it has its own page. |
| Cache-Control | MUST include public and max-age of at least 60 seconds, and MUST NOT send private, no-store or no-cache. | A profile behind a page cache that sets no-store is non-conformant even though it returns the right JSON. |
| A validator | SHOULD include an ETag or Last-Modified. | Without one, every consumer re-downloads the whole profile instead of revalidating it. |
| Stable URL | Profile URLs carry no per-transaction or per-session configuration. | A profile that varies by visitor cannot be shared-cached, which the caching rule above exists to enforce. |
All five are visible in the response headers, so you can check them yourself in one command:
curl -sS -D - -o /dev/null https://yourstore.com/.well-known/ucp
# What you are looking for, in order:
# HTTP/2 200 <- not 301, 302, 307 or 308
# content-type: application/json <- not text/html
# cache-control: public, max-age=300 <- public, and at least 60
# etag: "..." <- or last-modifiedWhat goes inside the profile
services says where an agent can reach you and over which transport — rest, mcp, a2a or embedded. Each entry needs a version, a spec URL and a transport, and REST, MCP and embedded entries also need a schema. capabilities is how an agent matches your store to a shopping intent; the intersection of your capabilities and the agent's is what gets negotiated. payment_handlers declares what your checkout accepts.
{
"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://yourstore.com/ucp/shopping",
"schema": "https://ucp.dev/2026-08-25/services/shopping/openapi.json"
}
]
},
"capabilities": {
"dev.ucp.shopping.checkout": [
{
"version": "2026-08-25",
"spec": "https://ucp.dev/2026-08-25/capabilities/checkout/",
"schema": "https://ucp.dev/2026-08-25/capabilities/checkout/schema.json"
}
]
},
"payment_handlers": {},
"supported_versions": {
"2026-04-08": "https://yourstore.com/.well-known/ucp/2026-04-08"
}
},
"keys": []
}Serving it, per platform
The hard part is rarely producing the JSON. It is getting your platform to answer at that exact path, with a 200 and your own headers. Most hosted platforms reserve the path, rewrite it, or hand you only a redirect rule.
| Platform | Approach | Notes |
|---|---|---|
| Shopify | Already served | We checked a live Shopify store and found a conformant profile at version 2026-08-25, served by the platform. There is nothing for you to publish. |
| WooCommerce | Not in core | WordPress serves /.well-known/ through the rewrite layer, so a plugin or a template_redirect hook can answer the path directly. Community plugins exist; they are young and not endorsed by the working group, so read what they emit before trusting it. |
| Magento / Adobe Commerce | Web-server mapping | The usual pattern is a controller that renders the profile, mapped onto the public path at nginx or the CDN. Map it with a rewrite, never a redirect. |
| PrestaShop | Front controller or file | Either a module front controller plus a web-server alias, or a static file written on each catalogue change. Check that your cache layer is not adding no-store. |
| BigCommerce | Edge rule | The storefront will not serve an arbitrary path, so the profile belongs at the edge: a worker or function in front of the domain, answering with 200. |
| Headless / custom | A route | The easiest case. Add a route at /.well-known/ucp that returns the JSON with the two headers above. Avoid a static asset whose cache headers you do not control. |
Whatever the platform, the mechanism is the same: answer at the requested URL. These three do that.
# nginx: serve the file at the path itself. A rewrite or proxy_pass keeps
# the 200; "return 301" would fail discovery outright.
location = /.well-known/ucp {
default_type application/json;
add_header Cache-Control "public, max-age=300" always;
alias /var/www/ucp/profile.json;
}# Apache: Alias maps the path without issuing a redirect.
Alias "/.well-known/ucp" "/var/www/ucp/profile.json"
<Files "profile.json">
ForceType application/json
Header set Cache-Control "public, max-age=300"
</Files>// Cloudflare Worker, for a platform that will not let you add a file.
// This answers at the requested URL, so it is not a redirect.
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/.well-known/ucp") {
return new Response(JSON.stringify(PROFILE), {
headers: {
"content-type": "application/json",
"cache-control": "public, max-age=300",
},
});
}
return fetch(request);
},
};Versions, and why they go stale quietly
UCP versions are dates, and matching is exact. The current release is 2026-08-25. An agent on a release your profile does not declare will not negotiate with you, and nothing anywhere logs an error you would see — the store simply stops being reachable to that agent.
“Businesses that support older protocol versions SHOULD declare asupported_versionsobject mapping each older version to a profile URI. Each URI points to a complete, self-contained profile for that version — whoseucp.versionequals its map key… Whensupported_versionsis omitted, onlyversionis supported.”
So a profile is not a file you publish once. It is a file with a maintenance schedule attached, and the cost of forgetting it is silent.
Check it on your own store
The scan fetches this path on your domain and reports what it found: whether the profile exists, parses, declares services and capabilities, and is served with the headers above. It checks the other three readiness axes at the same time. If the path is intercepted by a bot filter, it says so rather than guessing.
Run the free scanNo account, no email. It reports what it measured and says so when it could not measure something.