Agentic Guides · Jul 26, 2026 · 6 min read

How to validate a UCP integration

A checklist for confirming your UCP profile actually works: transport, discovery, document structure and the mistakes that only show up when an agent tries it rather than a browser.

Max Kruger

Publishing a UCP profile is easy. Publishing one that works is a slightly different exercise, because the ways it fails do not produce errors you would notice.

This is the checklist we encoded into our checker, in the order that matters. You can run all of it by hand with curl.

Accurate as of July 2026, against version 2026-04-08.

The golden rule: test like an agent, not like a browser

Almost every false conclusion about a UCP integration comes from checking it in a browser.

Your browser follows redirects silently, accepts any content type, sends browser-shaped headers, and renders whatever it gets. An agent does none of those things. Two requests to the same URL from these two clients can produce genuinely different results, and we have watched it happen: Allbirds' apex domain returned a redirect to a browser-style request and served the profile directly when asked with Accept: application/json.

So the baseline request is:

curl -sI \
  --no-location \
  -H "Accept: application/json" \
  https://yourstore.com/.well-known/ucp

--no-location is the important flag. It is the difference between testing what an agent sees and testing what you see.

1. Transport

HTTPS. Non-negotiable. A profile over plain HTTP should be treated as absent.

Status code 200. Not 301, not 302, not a challenge page returned with a 200.

Content type. Should be application/json. A profile served as text/html or text/plain may still parse, but you are relying on the caller being lenient, and a strict one is within its rights not to be.

Cache-Control present. The specification requires it, with public and a sensible max age:

Cache-Control: public, max-age=3600

This is the most commonly missed item by a wide margin. Every Shopify-served profile we tested was missing it. Without it, every discovery hits your origin.

2. Discovery

No redirects, on any host a customer might use. The specification says implementations must not follow redirects on the profile endpoint. Agents comply.

The classic failure: your apex redirects to www, or the reverse, and the profile only answers on one. Check both:

for host in yourstore.com www.yourstore.com; do
  printf "%-24s %s\n" "$host" \
    "$(curl -sI --no-location -H 'Accept: application/json' \
       https://$host/.well-known/ucp | head -1)"
done

Both should be 200 OK. Anything in the 3xx range on either is a genuine gap, not a technicality, because half your callers will try the host you did not think about.

Every customer-facing host. Regional domains, vanity domains, a headless storefront on a different host. Each is a host an agent may try. If your profile is not on it, you have no integration there.

3. Document structure

Once you have the body, check what it claims.

It parses as JSON. Obvious, but a truncated file or an HTML error page returned with a 200 fails here and nowhere else.

A version is declared, and it is current. 2026-04-08 at the time of writing. UCP versions are dates, so an old one is not automatically broken, but it tells you when you last kept up.

Required top-level fields are present. version, services, and for a shop, payment_handlers.

Capabilities are namespaced properly. dev.ucp.shopping.checkout, not checkout. Vendor extensions like dev.shopify.catalog are permitted, but your own extensions should use a namespace you control.

Size is sane. Real profiles run around 4 KB. If yours is very much larger, something is being inlined that should be referenced.

curl -s -H "Accept: application/json" \
  https://yourstore.com/.well-known/ucp \
  | python3 -m json.tool > /dev/null && echo "valid JSON"

4. Capability references point at you

If a capability references a schema or endpoint on a domain you do not control, that is worth a second look. It may be legitimate for a platform-served profile referencing the platform's own schemas. It is a problem if it happened because someone copied an example and left the example's URLs in.

The failure mode here is a supply chain one: you are telling agents to trust a definition hosted by a third party.

5. Nothing private is in there

The profile is public, unauthenticated, and now machine-readable by anything that wants it. That combination makes it an unusually bad place for a mistake.

Check for anything resembling key material, tokens, internal hostnames or staging endpoints. This sounds paranoid until you consider how these files get produced: often by templating from a config that also holds credentials.

Our checker treats published private key material as a hard failure for this reason.

6. It survives your own edge

A profile your infrastructure blocks is not a profile.

If you run bot protection, rate limiting or a WAF, confirm the well-known path is reachable by an unfamiliar client. Two specific things to look for:

  • A challenge page returned with 200. This is worse than a clean failure because it parses as a successful response containing no profile.
  • Rate limiting on bursts. An agent may fetch the profile and several pages in quick succession.

A useful diagnostic: if your own check comes back rate limited or challenged, that is a finding rather than a non-result. A real agent hits the same wall. It is why we report that case as indeterminate rather than as "no integration".

A script you can run today

#!/usr/bin/env bash
# Minimal UCP smoke test. Checks both hosts the way an agent would.
DOMAIN="${1:?usage: ucp-check.sh example.com}"

for host in "$DOMAIN" "www.$DOMAIN"; do
  url="https://$host/.well-known/ucp"
  echo "== $url"

  headers=$(curl -sI --no-location -m 15 -H "Accept: application/json" "$url")
  status=$(printf '%s' "$headers" | head -1 | awk '{print $2}')
  cache=$(printf '%s' "$headers" | grep -i '^cache-control' | tr -d '\r')

  echo "   status:        ${status:-no response}"
  echo "   cache-control: ${cache:-MISSING (spec requires it)}"

  if [ "$status" = "200" ]; then
    curl -s --no-location -H "Accept: application/json" "$url" \
      | python3 -c 'import sys,json; d=json.load(sys.stdin)["ucp"]; print("   version:      ", d.get("version")); print("   capabilities: ", len(d.get("capabilities",{})))' \
      2>/dev/null || echo "   body:          did not parse as UCP JSON"
  fi
done

What this does not cover

Validating the profile confirms the declaration. It does not confirm the store behind it works.

If you declare checkout and your checkout needs a modal dismissed, or you declare catalog.search and your search results render only after a script runs, the profile is accurate and the experience still fails. That part is covered in preparing your checkout for agentic commerce.

Do it more than once

The reason to automate any of this is that all of it can be true today and false next month, without anyone touching your code. Your platform changes how it serves the profile. Someone adds a redirect for a campaign. The specification moves, as it did in a breaking release twelve days after UCP's first version.

Run it in CI, or run a check periodically. The point is not the first result. It is noticing the day the result changes.

Share this article:

Ready to monitor your brand?

Track your brand mentions across ChatGPT, Claude, Perplexity, Grok, and Gemini with Orbilo.

Start Free Trial