---
title: "Publish a UCP Profile for a Store That Is Not on Shopify"
slug: publish-ucp-profile-non-shopify
published: 2026-10-13T08:17:32.074393+00:00
updated: 2026-10-13T08:17:32.074393+00:00
author: "AIScan Content Team"
author_url: https://aiscan.site/about
category: "AI Readiness"
tags: check:M1, platform:woocommerce, platform:wordpress, universal commerce protocol, ucp, agentic commerce, fix guide
description: "Publish a UCP profile for a WooCommerce or custom store: the required fields, hosting rules, a placeholder file, plugin options and a script to verify it."
url: https://aiscan.site/blog/publish-ucp-profile-non-shopify
---

A store that is not on Shopify gets no UCP profile by default. Shopify serves one for its merchants. WooCommerce, BigCommerce, and hand-built carts publish one only if somebody puts a JSON file at `/.well-known/ucp`. This guide shows two ways to do that, how to serve the file so an agent will accept it, and how to check the result. It is current as of 13 October 2026, against UCP version `2026-08-25`.

## Quick summary

| Question | Answer | Source |
|---|---|---|
| What does AIScan check? | `check:M1`: whether `/.well-known/ucp` answers. It is a bonus check, so a miss never lowers your score | [AIScan commerce docs](/docs/checks/commerce) |
| What must the file contain? | A top-level `ucp` object with `version`, `services` and `payment_handlers`. The last two must exist even when empty | [UCP specification](https://ucp.dev/specification/overview/) |
| How must it be served? | HTTPS, no redirects, `Cache-Control` with `public` and `max-age` of at least 60 | Same page, Hosting section |
| Fastest route on WooCommerce | A plugin that serves the profile and the endpoints behind it | Path 1 |
| Fastest route on a custom cart | A static or generated file, honest about what you support | Path 2 |
| Who should skip it? | Stores with a handful of plans and no catalogue. UCP is a catalogue standard | [AIScan check guidance](/docs/checks/commerce) |

**Fastest fix:** run `npx aiscan-cli yourstore.com`, read the M1 row, then pick a path below.

## What the check sees, and what an agent needs

AIScan requests `/.well-known/ucp` and records the status code. On `aiscan.site` it records `HTTP 404`, and the row shows as informational. Our [adoption study](/blog/ucp-profile-adoption-shopify-vs-rest) found that M1 decides on the status code alone, so an empty page or an HTML page that returns 200 passes the check and helps no agent.

An agent needs more. According to the UCP specification, a profile is "a JSON object with a required `ucp` member". The `ucp` member carries `version`, `services` and `payment_handlers`, all required, and "the `services` and `payment_handlers` registries MUST be present even when empty." `capabilities` is optional.

Each entry under `services` names a transport (`rest`, `mcp`, `a2a` or `embedded`) and a `version` and `spec` URL. REST and MCP entries also need a `schema`, and the `endpoint` is where the agent will send calls. That endpoint is the part that costs real work. A profile is a promise that something answers there.

## Decide what you can honestly promise

There are three levels, and only the third needs engineering.

1. **A valid placeholder.** `services` and `payment_handlers` empty. It satisfies the spec and turns M1 green, and it advertises nothing. Treat it as a marker, not as agent readiness.
2. **A profile served by software that also serves the endpoints.** The plugin or platform owns both halves, so the promise is kept.
3. **A profile you write, pointing at endpoints you built.** You implement the UCP REST or MCP surface from the spec, then declare it.

Never copy a Shopify store's profile and change the domain. We read one live on 13 October 2026 (Allbirds). It declares an MCP service at a `myshopify.com` address, a checkout capability, and a fulfillment extension. Declaring capabilities your store does not run sends agents to endpoints that do not exist.

## Path 1: WooCommerce, with a plugin

WooCommerce has no native UCP support in core. The feature-request thread on woocommerce.com was still marked Open when we looked on 13 October 2026. Two plugins on WordPress.org describe themselves as UCP implementations. We read their listings and have not installed either one:

| Plugin | Version | Last updated | Active installs | What the listing says |
|---|---|---|---|---|
| Universal Commerce Protocol (UCP) for WooCommerce | 1.0.4 | 9 Mar 2026 | about 30 | Serves `/.well-known/ucp`, with REST, MCP and embedded bindings |
| WebToffee Universal Commerce Protocol Enabler | 1.0.5 | 1 Oct 2026 | about 10 | Implements UCP `2026-08-25`, serves the profile, REST and MCP endpoints, and an A2A agent card |

Both are tiny installs, so read the code and test on staging first. Steps:

1. Clone the store to staging and back up the database.
2. Install one plugin from Plugins, then Add New, and search for "Universal Commerce Protocol".
3. Activate it and open `https://staging.example.com/.well-known/ucp` in a browser. You should see JSON.
4. Read `ucp.version`. The first plugin was last updated before `2026-08-25` existed, so confirm which version it declares before you rely on it.
5. Run the verifier below against staging, then the AIScan scan after you go live.

If the profile renders as a 404 page, flush permalinks (Settings, Permalinks, Save) and check that no cache or security plugin rewrites `/.well-known/`.

[ThinkRank](https://thinkrank.ai) is the WordPress plugin we recommend for the layer around the profile: Product schema, robots.txt, and llms.txt from one plugin, with migration from Rank Math, Yoast, AIOSEO and SEOPress. We have not tested whether it serves a UCP profile, so treat the profile as its own job. Yoast and Rank Math also handle schema; the UCP profile is a separate task for any of them.

## Path 2: write the file yourself

This fits a custom cart, a headless build, or any platform with no plugin.

1. Create `ucp.json` with the placeholder below. It is valid and claims nothing.

```json
{
  "ucp": {
    "version": "2026-08-25",
    "services": {},
    "payment_handlers": {}
  }
}
```

2. Serve it at exactly `/.well-known/ucp`, with no `.json` extension. The next section covers headers.
3. Run the verifier. Confirm the placeholder passes.
4. Build the surface you want to declare. Read the [UCP specification](https://ucp.dev/specification/overview/) and the REST or MCP schema it links. Start with one transport.
5. Add one `services` entry per working transport. The shape, from the spec, is:

```json
"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"
  }]
}
```

6. Add `capabilities` only for what the endpoint implements. Each needs `version`, `spec` and `schema`. Extensions such as fulfillment use `extends`.
7. Declare `payment_handlers` that match your real processor. Leave it empty if checkout stays on your own site.
8. Redeploy and rerun the verifier after every change.

## Versions: dated, and exact

UCP versions are dates, not semantic numbers. The profile declares `ucp.version`, and every service and capability inside it carries its own `version`. According to the spec, a business that selects version D must declare D on every `dev.ucp.*` entry. Mixing `2026-08-25` at the top with `2026-04-08` inside a service is an inconsistent profile.

Businesses that still support older releases add a `supported_versions` object mapping each older date to its own profile URL. The Allbirds profile does this for `2026-04-08` and `2026-01-23`. If you start on the current date and have no older clients, you need no such object. Copy the date from the [spec page](https://ucp.dev/specification/overview/), not from a plugin readme, and update the profile when you move to a newer release.

## Serve it so an agent will take it

The specification's Hosting section sets three rules. Published artifacts must be served over HTTPS. Profile endpoints "MUST NOT use redirects (3xx)". And they must send `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` header is recommended.

The redirect rule bites people who force `www` to apex or http to https at the app layer. Fetch the profile on the exact host agents will use. Our [canonical host guide](/blog/canonical-host-redirects-for-agents) covers the redirect side.

Extensionless files need a content type. On nginx, a location block does it (we did not run nginx here, so test it):

```nginx
location = /.well-known/ucp {
    default_type application/json;
    add_header Cache-Control "public, max-age=300";
    try_files /ucp.json =404;
}
```

On a Next.js app, a route handler at `app/.well-known/ucp/route.ts` can return the JSON with the same two headers. We did not run that one either. For the same reason, a catch-all route that returns the app shell for unknown paths will give you a soft 200 here, so test with curl.

## Verify

Start with the scan, because it is free and needs no account:

```bash
npx aiscan-cli yourstore.com
```

Or paste the address at [aiscan.site](https://aiscan.site/). Read the M1 row. `HTTP 200` is a pass and `HTTP 404` means nothing is published. The scan covers M1 only for the status code, so M4 (machine-readable pricing) and P2 (MCP server card) are separate rows, and AIScan cannot tell you whether your profile is valid or your endpoints work.

If you would rather check by hand, this script uses only the standard library and applies the spec's hosting and shape rules:

```python
import http.client, json, re, sys
from urllib.parse import urlsplit

def main(base):
    u = urlsplit(base if "//" in base else "https://" + base)
    cls = http.client.HTTPSConnection if u.scheme == "https" else http.client.HTTPConnection
    c = cls(u.netloc, timeout=15)
    c.request("GET", "/.well-known/ucp", headers={"User-Agent": "Mozilla/5.0"})
    r = c.getresponse()
    h = {k.lower(): v for k, v in r.getheaders()}
    body = r.read()
    bad = []
    if r.status != 200:
        bad.append(f"status {r.status}, want 200 with no redirect")
    if "json" not in h.get("content-type", ""):
        bad.append("content-type is not JSON")
    cc = h.get("cache-control", "")
    m = re.search(r"max-age=(\d+)", cc)
    if "public" not in cc or not m or int(m.group(1)) < 60 or re.search(r"no-store|no-cache|private", cc):
        bad.append(f"cache-control {cc!r}")
    try:
        ucp = json.loads(body)["ucp"]
        if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", str(ucp.get("version", ""))):
            bad.append("ucp.version missing")
        for k in ("services", "payment_handlers"):
            if not isinstance(ucp.get(k), dict):
                bad.append(f"ucp.{k} must exist, even when empty")
    except Exception:
        bad.append("body is not JSON with a ucp object")
    print("\n".join(bad) or "PASS")
    return 1 if bad else 0

sys.exit(main(sys.argv[1]))
```

We ran it on 13 October 2026 against four cases. A local server with the placeholder profile passed. A catch-all server that returns an HTML page with status 200 failed on content type, caching and body. A server that redirected `/.well-known/ucp` with a 301 failed on the status. Allbirds passed: JSON, `Cache-Control: public, max-age=60`, version `2026-08-25`. It does not check that your endpoints work. Test those by calling them.

## BigCommerce and other platforms

Commerce, the parent company of BigCommerce, announced its endorsement of UCP and plans to offer buying inside Google's AI surfaces. We found no merchant documentation for publishing a profile, for it, so this guide makes no claim about BigCommerce's tooling. Check your admin and the platform's developer docs. Where nothing exists, Path 2 works on any host that lets you serve a file.

## When something goes wrong

- **404 after a plugin install:** permalinks or a rewrite rule. Flush permalinks and test the URL with curl, not a browser.
- **200 but HTML:** a catch-all route. Serve the file before the app handler.
- **301 or 302 on the profile:** remove the redirect for this path. Agents are told not to follow it.
- **Valid file, M1 still 404:** you tested a different host. Scan the exact hostname customers use.
- **`403` from your own scan:** a bot-protection rule or WAF is blocking the path. Allow `/.well-known/` and rescan.

On Shopify you do not publish the file. Check that it still answers after a theme or proxy change, and use [StoreSEO](https://storeseo.com/) for the llms.txt and schema layer that makes the catalogue legible. For the protocols themselves, read [agentic commerce protocols explained](/blog/agentic-commerce-protocols-explained); for pricing markup, [machine-readable pricing schema](/blog/machine-readable-pricing-schema).

## Next step

Scan your store with `npx aiscan-cli yourstore.com` and look at M1, M4 and P2. Fix M4 first if your pricing is only in prose, since that check affects your score and M1 does not. Then browse the [guide library](/guides) and the [commerce checks page](/docs/checks/commerce). The task works without us: a text editor and curl are enough.

