6 min

Validator 3.0.0: payment_handlers is required, and we had it wrong

UCPtools 3.0.0 now reports an error when a business profile omits ucp.payment_handlers. The spec has required it since 2026-04-08 - we were the ones getting it wrong, including on our own profile.

If you validate a UCP profile with UCPtools and it passed yesterday, it may fail today. That is deliberate. Version 3.0.0 enforces a rule the spec has carried since 2026-04-08 and we were not checking.

Profiles missing ucp.payment_handlers now report an error:

[error] UCP_MISSING_PAYMENT_HANDLERS  $.ucp.payment_handlers
Missing required "payment_handlers" field in ucp object

This post explains why that is correct, why we got it wrong, and exactly what to change.


The rule

A profile served at /.well-known/ucp is a business profile. It is validated against profile.json#/$defs/business_schema, which composes ucp.json#/$defs/business_schema - and that schema says:

"required": ["services", "payment_handlers"]

Two fields, both mandatory. This is identical at tag v2026-04-08 and v2026-08-25, so it is not something the August release introduced. It has been true for five months.

Why we were reporting it as fine

Because we were reading the wrong layer of the schema, and we had a good reason to be reading it.

UCP defines several profile variants. The base envelope really does require only one field:

// ucp.json#/$defs/base
"required": ["version"]

Earlier this year we found that our validator was demanding three fields the spec never required at all - among them config.supported_mechanisms on Identity Linking, which appears in no published tag and which we were erroring on hard enough to suppress every check behind it. It fired on the specification's own example profile.

Fixing that meant establishing the principle that we validate against the published schema rather than against our assumptions. The base layer, required: ["version"], was the evidence.

The correction was right. The generalisation drawn from it was too broad. ucp.json#/$defs/base governs platform profiles and API response envelopes. It does not govern /.well-known/ucp, which composes the stricter business_schema. In removing three invented requirements we removed a real one standing next to them, and then spent five months returning ok: true for profiles the specification rejects.

We were shipping one of those profiles

ucptools.dev publishes its own UCP profile. It had no payment_handlers field.

Our validator did not catch it, because our validator had the same blind spot as our generator - both were built from the same wrong reading. A tool cannot find a bug it is also making.

That is fixed in the same release. It is also the reason this is a major version rather than a quiet patch: we would rather break your build today than keep telling you a profile is valid when an agent will disagree.

What a correct registry looks like

payment_handlers is not an array of strings, and it does not live inside a payment capability. It is a registry that sits directly on the ucp object, keyed by reverse-domain name, with array values:

{
  "ucp": {
    "version": "2026-08-25",
    "services": {
      "dev.ucp.shopping": [
        {
          "version": "2026-08-25",
          "spec": "https://ucp.dev/2026-08-25/specification/overview/",
          "transport": "rest",
          "schema": "https://ucp.dev/2026-08-25/services/shopping/rest.openapi.json",
          "endpoint": "https://yourstore.example/ucp/v1"
        }
      ]
    },
    "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"
        }
      ]
    },
    "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"
        }
      ]
    }
  },
  "keys": [
    { "kid": "key_2026", "kty": "EC", "crv": "P-256", "x": "...", "y": "...", "alg": "ES256" }
  ]
}

Note also that the spec URLs are version-scoped. Unversioned https://ucp.dev/schemas/... URLs return 404, and the 2026-08-25 tree is vertical-scoped - checkout moved from specification/checkout to specification/shopping/checkout.

If you do not take payments

Declare the registry and leave it empty:

"payment_handlers": {}

That is valid. The schema sets no minProperties, so an empty registry satisfies the requirement. It is also the honest declaration for a site that is not a store - it says "I have a payment registry and there is nothing in it" rather than omitting the field or, worse, inventing handlers you do not operate.

This is what ucptools.dev itself now publishes. We are a developer tool, not a shop.

An empty registry becomes an error in one case only: when your profile also advertises checkout. At that point an agent can walk all the way to paying and find no way to pay, which is a real defect rather than a paperwork one.

Everything else in 3.0.0

Three of these remove checks that were ours rather than the specification's - the same class of mistake, found by the same audit.

UCP_SIGNING_KEY_MISSING_USE no longer fires when use is absent. use is optional - jwk_public_key.required is ["kid", "kty"] - and the specification's own canonical profile omits it. We were warning on a perfect profile, in a message that conceded the field was optional. A key that declares use as something other than "sig" is still flagged, because a verifier selecting it by kid will not use it to check a signature.

AP2 mandates are graded draft again. Our stability map treats any capability it does not recognise as stable. AP2 was missing from it under both its old name (dev.ucp.shopping.ap2_mandate) and the new one 2026-08-25 moved it to (dev.ucp.common.payment.ap2_mandate), so AP2 schema drift raised an error where a draft capability should only warn.

Two capabilities that do not exist have been removed. dev.ucp.shopping.payment and dev.ucp.shopping.payment_data were listed as known, stable capabilities. Neither appears in any published tag - we checked v2026-01-11, v2026-04-08 and v2026-08-25. They were names we made up and then taught the validator to trust. If you import KNOWN_CAPABILITIES.PAYMENT or .PAYMENT_DATA from the npm package, those exports are gone; that is the other breaking change in this release.

2026-08-25 capabilities are now listed explicitly - Permalink, Loyalty, Payment Terms, Split Payments and Payment Authentication (3DS2). They were already being graded correctly by the unknown-name default; listing them makes it deliberate rather than accidental.

Upgrading

npm install @ucptools/validator@3.0.0

The hosted validator, the API and the GitHub Action are already on it - they track the deployment, so there is nothing to do there.

To check a live profile without installing anything:

npx -p @ucptools/validator@3.0.0 ucp-validate validate --remote yourstore.example

Pass a bare domain, not a URL - the CLI appends /.well-known/ucp itself. Add --quick to skip network checks, or --json for CI. A local file works too:

npx -p @ucptools/validator@3.0.0 ucp-validate validate --file ./ucp.json

Or paste it into ucptools.dev.

The part worth taking away

We spent five months confidently reporting ok: true on profiles the specification rejects, because we corrected an error and over-corrected past the point where the evidence supported it. The base schema really does require only version. That fact was true, verifiable, and the wrong fact to be reasoning from.

The rule we now apply to our own checks is narrower than "is it in required?" It is: does this check fire on a profile the specification itself publishes as valid? If it does, the check is invented and it goes. If it does not, it can stay, but the message must not claim the schema demands something the schema does not.


UCPtools is an independent community tool. UCP is an open standard developed by Google, Shopify and partners - we are not affiliated with them or with the UCP consortium. Verified against the published schemas at tag v2026-08-25, 2 September 2026.

← Back to Blog