
What a UCP profile is and where it goes
A UCP profile is a discovery document that sits at the /.well-known/ucp path at the root of your domain, has no file extension and consists of a single JSON object. The full address takes this form: https://yourdomain.com/.well-known/ucp. The file is named ucp, not ucp.json.
The Universal Commerce Protocol is an open commerce protocol developed by Google together with retailers such as Shopify, Etsy, Wayfair, Target and Walmart. The problem it tackles isn't a search problem; it's the way integrations multiply. Instead of writing a separate integration for every sales surface, the business declares the capabilities it supports in a single machine-readable document. A shopping agent fetches this document and learns from it which protocol version the business speaks, which endpoints to send requests to, which payment methods are supported and which public key to use to verify signed requests.
There's a critical distinction here. The profile file is a directory, not a storefront. Every endpoint declared in it must actually work. Placing the file doesn't complete the integration; it only makes the integration discoverable.
Why the .well-known directory is used
RFC 8615 reserves the /.well-known/ path prefix for metadata that applies to an entire site, and it replaces the older RFC 5785. The goal is to prevent collisions. The prefix is reserved so that the site owner's own content paths and protocol files don't compete in the same namespace.
Two rules of the standard have direct consequences in practice. First, these paths are valid only at the top of the path hierarchy. /.well-known/ucp is valid; /shop/.well-known/ucp is not a valid well-known address. Second, the name that follows the prefix must be a single path segment and cannot contain a slash.
The prefix is fixed, while the names that follow it are kept in IANA's Well-Known URIs registry, and registration is subject to expert review. One honest caveat: the name ucp doesn't currently appear in that registry. By contrast, the agent-card.json entry for the Agent2Agent protocol in the same ecosystem is registered with permanent status. The practical consequence is limited, because you already have the authority to publish a path at the root of your own domain, and the file works without registration. Still, this detail tells you how mature the standard really is.
Not every machine-readable file at the root belongs in this directory. robots.txt, which governs crawler behavior, predates the standard, so it sits directly at the root and is governed by its own rules; we cover its impact on crawl budget separately in our article on crawl budget and robots.txt optimization. llms.txt, which offers language models a content map, likewise lives at the root, and you can use our llms.txt generator to prepare the file quickly. The UCP profile, on the other hand, is deliberately placed under .well-known, because it isn't a human-facing document but a protocol-level discovery endpoint.
What's inside a UCP profile?
Required fields
The only required member at the root of the document is the ucp object. Next to it sits the optional keys array. Inside the ucp object, version, services and payment_handlers are required. services and payment_handlers must be present in the document even if they're empty; they can't be removed entirely. capabilities is optional, but a commerce profile that declares no capabilities serves no practical purpose.
{
"ucp": {
"version": "YYYY-MM-DD",
"services": {
"dev.ucp.shopping": [
{
"version": "YYYY-MM-DD",
"spec": "https://ucp.dev/YYYY-MM-DD/specification/overview/",
"transport": "rest",
"endpoint": "https://business.example.com/ucp/v1",
"schema": "https://ucp.dev/YYYY-MM-DD/services/shopping/rest.openapi.json"
}
]
},
"capabilities": {
"dev.ucp.shopping.checkout": [
{
"version": "YYYY-MM-DD",
"spec": "https://ucp.dev/YYYY-MM-DD/specification/shopping/checkout",
"schema": "https://ucp.dev/YYYY-MM-DD/schemas/shopping/checkout.json"
}
]
},
"payment_handlers": {}
},
"keys": []
}
The YYYY-MM-DD placeholder above is deliberate. The protocol version identifier is in date format, and you enter the exact identifier of the release you implement. The version field is validated against a strict date pattern, so a non-date value such as draft is not valid. The specification also separately prohibits announcing unstable pre-release versions through public discovery.
How service and capability entries are written
services, capabilities and payment_handlers are each registries. Their keys are in reverse domain name format, for example dev.ucp.shopping or dev.ucp.shopping.checkout. The value of each key is an array, and each item in the array defines a specific version of that entry.
| Field | Where | What it does |
|---|---|---|
version | In every entry item | The entry's dated version identifier |
spec | In every entry item | The address of the relevant specification text |
transport | Required in a service item | rest, mcp, a2a or embedded |
endpoint | In a service item | The HTTPS address requests are sent to |
schema | Required in a capability item | The capability's JSON Schema address |
extends | In an extension item | The name of the parent capability being extended |
supported_versions | Inside ucp | Maps older versions to their own profile addresses |
Version consistency is a silent trap here. Whatever date you declare as ucp.version in the profile, the version value of every service and capability entry starting with dev.ucp. must be the same date. The other side treats an entry with a different date as if it doesn't exist and disables it.
The second trap is the namespace binding. The origin of a capability's schema address must match the capability's namespace authority. You can't host the schema of a capability starting with dev.ucp. on your own server and point to it there. If you're writing your own extension, you use your own reverse domain namespace.
The keys array is a valid JWK set in RFC 7517 format. It's used to verify signed requests and signed webhooks. Adding, rotating and revoking keys is done through this single array. The revocation side is critical for security: a leaked key isn't considered revoked until it's actually deleted from the keys array.
Steps to publish the file
- Choose the protocol release to implement and note its dated version identifier. The entire profile hinges on this single decision.
- Get the endpoints you'll declare up and running first. Write the profile on top of a working API, not the other way around.
- Generate the signing key pair. Convert the public key to JWK format and put it in the
keysarray; keep the private key on the server side. - Create the JSON document. Make sure the
ucp.version,ucp.servicesanducp.payment_handlersfields exist, even if they're empty. - Validate the document against the profile JSON Schema file published by the protocol. This step catches most field errors in hand-written profiles before they go live.
- Place the file on the server at the
/.well-known/ucppath, without an extension. If you use a framework, put it in the static files directory and verify that the router doesn't swallow this path. - Set the media type and cache headers on the server manually. Since there's no extension, automatic MIME mapping doesn't kick in.
- Verify that the path responds over HTTPS without a redirect. Your normalization rule between the www and non-www versions affects this path as well.
- Review WAF, bot protection and server rules that block directories starting with a dot. The party fetching this file isn't a browser but an automated client.
- Test accessibility from outside, then notify the party that will consume the profile. On Google's side, you do this by entering the profile address in the UCP integration screen in Merchant Center and triggering discovery.
How should the server serve the file?
The specification's hosting rules are clear. Published documents must be served over HTTPS. Profile endpoints cannot use 3xx redirects. The response must carry a Cache-Control header that includes public and a max-age of at least 60 seconds; it cannot be served with the private, no-store or no-cache directives. Adding a validator such as ETag or Last-Modified is also recommended.
The rules on the consumer side point in the same direction. The party fetching the profile rejects non-HTTPS addresses, doesn't follow redirects and rejects addresses that resolve to private-use IP ranges. It's also expected to apply a cache floor of at least 60 seconds regardless of the header the origin sends. This last detail matters operationally: when you change the profile, the change isn't reflected on the other side instantly.
The hosting section doesn't separately mandate a media type, but the document is a JSON object, and application/json is required for the protocol's requests and responses. The correct behavior is to serve the profile with the same type. The problem is that servers mostly resolve the media type from the file extension, and this file has no extension. As a result, the server sends nginx's default application/octet-stream or, depending on the configuration, text/plain.
# nginx
location = /.well-known/ucp {
default_type application/json;
add_header Cache-Control "public, max-age=300";
}
# Apache, in the .htaccess file inside the .well-known directory
<Files "ucp">
ForceType application/json
Header set Cache-Control "public, max-age=300"
</Files>
There's also a classic security hardening trap. Many off-the-shelf configurations block paths starting with a dot wholesale. On nginx, location ~ /\. { deny all; }, and on Apache a similar RedirectMatch rule, shut this directory off completely. The same rule has been breaking certificate validation files for years. The solution is to define an explicit exception for the .well-known directory.
How to test accessibility
- Fetch the response headers:
curl -sI https://yourdomain.com/.well-known/ucp. The expected result is a 200 status code, theapplication/jsonmedia type and aCache-Controlheader that includespublicand a sufficientmax-age. If you see aLocationheader in the response, there's a redirect and the rule has been violated. - Parse the body:
curl -s https://yourdomain.com/.well-known/ucp | python3 -m json.tool. If the server returns an HTML error page, this step fails immediately. - Repeat the test from outside your own network. CDN and WAF rules often grant privileges to the office IP that they don't grant to an automated client coming from outside.
- Try with a client that doesn't imitate a browser, without cookies. Bot protection usually kicks in right here.
- Test the exact host name you declared. Whichever of the www and non-www versions you announced, that address must return 200 without a 301.
- Test every address inside the profile separately. The
endpoint,specandschemaaddresses must all respond over HTTPS without redirects. - Check version consistency. The
versionvalue of every entry starting withdev.ucp.must matchucp.version. - After an edit, purge the CDN cache and repeat the test. A stale copy staying live is the most common silent error with this file.
The accessibility of root directory files, redirect chains and server response codes are site-wide issues that affect one another. To see the full picture, run a general technical crawl with our on-page SEO analysis tool; it reveals configuration problems that a single-file test misses.
Common mistakes and their causes
| Symptom | Real cause | Fix |
|---|---|---|
| 404 response | The file was placed at /well-known/ucp or /.well-known/ucp.json | Make the path exactly /.well-known/ucp and don't add an extension |
| 404 in a framework project | The router catches the request and the static file is never served | Move the file to the static assets directory and exempt the path from the router |
| 301 or 302 response | www normalization or a trailing slash rule | Configure the declared address to return 200 directly |
| Wrong media type | MIME mapping doesn't work because there's no extension | Define application/json specifically for that path on the server |
no-store header | The CDN or application layer adds a default security header | Force a header with public and max-age for this path |
| 403 response | WAF, bot protection or a block on directories starting with a dot | Define an exception for .well-known |
| Edit not reflected | The CDN holds the old copy and the consumer side also caches | Purge the cache and accept that propagation will be delayed |
| Capability not showing | The entry's version doesn't match ucp.version | Align all dev.ucp. entries to a single date |
| Capability rejected | The schema address doesn't match the namespace authority | Point to the schema from the origin that owns the namespace |
| Key revocation not working | The old key is still in the keys array | Remove the key from the array entirely |
Who should publish this file today
This file is not a visibility file. It isn't a ranking signal and it doesn't generate organic traffic. It's the machine-side entry point of a commerce integration. Base the decision on that.
It makes sense today for online sellers that have their own payment and order API, can process signed HTTP requests and have the engineering capacity to keep those endpoints up without interruption.
The group that should wait is larger. If you run on an off-the-shelf e-commerce platform, writing the profile by hand conflicts with the platform's own rollout plan. In that case, the right move is to wait for the platform's UCP support and enable it. For content sites without a cart, service sites and B2B sites that work through quote forms, this file has no use. A profile published with empty services and empty payment_handlers is technically valid but declares nothing, and in return leaves you with one more surface to maintain.
Those who can't keep their endpoints running shouldn't publish this file at all. A profile pointing to dead addresses is a broken commitment that a machine builds transactions on, and it doesn't fail silently.
One more reality check on timing. The agentic checkout flow on Google's own surface is currently open to merchants selected through an interest form, and the rollout is focused on the US, Canada and Australia. Being outside this program doesn't make publishing the profile wrong, because UCP is an open protocol and Google isn't its only consumer. But going in expecting concrete volume in the short term isn't realistic.
Frequently Asked Questions
Can the file be named ucp.json?
No. The discovery path is defined as /.well-known/ucp. Using a file name with an extension and redirecting from that path isn't a solution either, because profile endpoints can't use 3xx redirects and the consumer side doesn't follow redirects.
Can UCP be used without a profile file?
Public discovery starts from this file, so a business that wants to operate in the open ecosystem publishes the profile. If the two parties already know each other, they can agree on pre-release implementations outside of discovery, but this comes with no guarantee of stability or compatibility, and pre-release identifiers can't be declared in a public profile.
Does robots.txt block this file?
robots.txt governs the behavior of the crawlers that consult it. The party fetching the UCP profile, however, isn't a crawler but a protocol client. The layers that actually cut off access are the WAF, bot protection and server access rules. Still, keeping a blocking rule that covers the .well-known directory has no benefit and creates unnecessary risk.
Can a single file support more than one protocol version?
Not directly. The profile at the root defines a single current version. Older versions are declared with the supported_versions object, and each one is mapped to its own separate profile address. The profiles at those addresses are leaf documents; each defines a single version and cannot itself carry supported_versions.
What should you do if a signing key leaks?
Remove the key from the keys array. It isn't considered revoked until it's deleted from the array. Even after deletion, you need to account for the fact that propagation won't be instant, because consumers store the profile with a cache floor of at least one minute.



