7 min read

UCP v2026-08-25 Breaks Quietly: 4 Changes That Still Pass Validation

The new Universal Commerce Protocol release renamed fields your profile is still allowed to contain. Your JSON stays valid, your validator stays green, and agents stop reading your keys. Here is what actually changed and how to migrate.

On August 25, 2026, the Universal Commerce Protocol shipped v2026-08-25. It is the first new spec version in four and a half months, and it is a big one: a multi-vertical refactor laying groundwork for Food and Lodging, grocery and location capabilities, 3D Secure via a new Actions primitive, split payments and installment terms, a loyalty extension, and Web Bot Auth interop.

The release notes carry a clearly labelled "Breaking Changes" section. Good.

The problem is that the most dangerous changes in it are not the ones that break loudly. They are the ones where your profile still parses, still validates against a naive checker, and agents quietly stop working.

I know, because our own validator got this exactly wrong. More on that below.

Why this release breaks quietly

Look at source/schemas/profile.json at tag v2026-08-25. The base definition ends like this:

{
  "type": "object",
  "required": ["ucp"],
  "properties": {
    "ucp": { "$ref": "ucp.json#/$defs/base" },
    "keys": {
      "type": "array",
      "items": { "$ref": "#/$defs/jwk_public_key" }
    }
  },
  "additionalProperties": true
}

Two things matter here.

required is ["ucp"] and nothing else. And additionalProperties is true.

That combination means a profile carrying removed fields is not invalid. It parses. It validates. It just contains members that nothing reads any more. When a spec renames a field under additionalProperties: true, the old name does not become an error. It becomes decoration.

That is the failure mode to hunt for in this release. Not "my profile is rejected" but "my profile is accepted and ignored".

Silent breaker 1: signing_keys became keys

This is the big one, and it is the cleanest example of the pattern.

Before v2026-08-25, a profile published its signing keys in a root-level signing_keys array. As of this release, signing_keys is gone from profile.json entirely and keys is the sole canonical field. The schema is blunt about it:

When a profile publishes signing keys, they MUST appear here; this is where every UCP verifier reads them.

So:

// Before - v2026-04-08
{
  "ucp": { "version": "2026-04-08", "...": "..." },
  "signing_keys": [
    { "kty": "EC", "kid": "key-1", "crv": "P-256", "x": "...", "y": "..." }
  ]
}

// After - v2026-08-25
{
  "ucp": { "version": "2026-08-25", "...": "..." },
  "keys": [
    { "kty": "EC", "kid": "key-1", "crv": "P-256", "x": "...", "y": "..." }
  ]
}

The array contents do not change. It is the same JWK Set under a different name.

Now put that next to additionalProperties: true. If you bump your version to 2026-08-25 and leave signing_keys where it is, your profile is still schema-valid. Nothing rejects it. But no verifier looks at signing_keys any more, so signature verification fails against a profile in which nothing looks wrong. You will be debugging your signing code, not your field names.

The fix is a rename. The hard part is knowing you need it.

We got this wrong in the opposite direction

Worth being straight about this, because it is the same trap from the other side.

Our validator hard-required signing_keys and had no concept of keys at all. So when the spec moved, we started emitting a hard error on profiles that were correctly migrated. A merchant who did the right thing got told their profile was broken.

It got worse. Our pipeline skips rules and network validation whenever structural validation reports an error. So the bogus error did not just add noise, it suppressed every real check behind it. One false error, and no actual analysis.

We shipped the fix on August 29. Both spellings are now accepted, keys is treated as canonical from 2026-08-25, and a leftover signing_keys on a new-version profile raises a warning that tells you it will be ignored rather than an error claiming it is invalid.

The lesson generalises past our bug: a validator that hard-codes a field name is a validator that will eventually fail correct input. If you have written any internal UCP checking, go look at how it resolves keys today.

Silent breaker 2: fulfillment config lost its allows_ prefix and changed shape

The file was renamed from merchant_fulfillment_config.json to business_fulfillment_config.json, and two properties were renamed. But this is not just a rename, and that is what makes it dangerous. multi_destination also changed type.

// Before - v2026-04-08: a map of method -> boolean
"allows_multi_destination": {
  "shipping": true,
  "pickup": false
}

// After - v2026-08-25: an array of objects, listing = permitting
"multi_destination": [
  { "method": "shipping" }
]

Now trace what happens if you skip this migration. The old schema set additionalProperties: false, so a stray key there used to be a hard error. The new business_fulfillment_config does not, so allows_multi_destination is now simply an unrecognised member that nothing reads. And the new field's own rule is explicit:

Listing a method permits it; an omitted method does not.

So your untouched config does not fail. It resolves to an empty permission set. You silently stop allowing split shipping across addresses, and nothing anywhere says so. That is a capability you believe you advertise quietly turning off.

method_combinations kept its array-of-arrays shape, but its enum restriction to ["shipping", "pickup"] was removed in favour of an open vocabulary with those as well-known values. That one is a loosening, so it will not break you.

While you are in these files: fulfillment_option.description was upgraded from a flat string to a structured object (it now $refs common/types/description.json). That one does change type on a required-ish field, so it is more likely to fail loudly, which honestly makes it the safer of the two.

Silent breaker 3: buyer consent went from booleans to a reverse-DNS map

This one changes shape rather than just names.

// Before - v2026-04-08: four fixed booleans
"consent": {
  "analytics": true,
  "marketing": false,
  "preferences": true,
  "sale_of_data": false
}

In v2026-08-25, consent is a dynamic map keyed by reverse-DNS identifiers, where each purpose is an object requiring granted, source, and description:

// After - v2026-08-25
"consent": {
  "dev.ucp.consent.marketing": {
    "granted": false,
    "source": "platform",
    "description": "Promotional communications across all channels",
    "segments": {
      "dev.ucp.consent.marketing.email": {
        "granted": true,
        "source": "platform",
        "description": "Email marketing only"
      }
    }
  }
}

The source field is the interesting addition: it distinguishes a business default ("business") from an explicit buyer decision captured by the platform ("platform"). If you are handling consent for compliance reasons, that distinction is probably one you have been reconstructing by hand. Now it is in the protocol. Segments let you scope a decision to a channel without inventing your own convention, and UCP predefines dev.ucp.consent.marketing.email and .sms.

Silent breaker 4: payment extensions moved namespace

Payment extensions migrated out of the shopping vertical:

  • dev.ucp.shopping.split_payments becomes dev.ucp.common.payment.split_payments
  • the same move applies to payment_terms and ap2_mandates

Related, and easy to miss: shared primitive schemas (amount.json, price.json, line_item.json) moved under common/types/, which changes their $id URLs. If you pin, cache, or fetch those schemas by URL anywhere, those references are now stale.

This is the change most likely to bite tooling rather than merchants. Anything that walks capability extension chains by prefix will read migrated extensions as orphaned.

The migration checklist

  1. Rename signing_keys to keys at the profile root. Delete the old key. Do not keep both - two sources of truth for signing keys drifting apart is precisely the failure this change was made to eliminate.
  2. Move to business_fulfillment_config.json, drop the allows_ prefix, and convert multi_destination from a method-keyed boolean map to an array of { "method": "..." } objects. Do not just rename it - an unconverted map reads as "nothing permitted".
  3. Convert fulfillment_option.description from a flat string to the structured common/types/description.json object.
  4. Restructure buyer consent into the reverse-DNS map with granted / source / description.
  5. Repoint payment extensions to dev.ucp.common.payment.*.
  6. Update any pinned $id URLs for amount, price, and line_item to common/types/.
  7. Bump version to 2026-08-25 last, after the above. The version field is the part agents read to decide how to interpret everything else.

Then re-validate. And validate with something that actually knows about 2026-08-25 - a checker still targeting an older version will happily tell you a half-migrated profile is fine.

The broader point

Date-based protocol versions make it easy to think of a spec bump as a number you increment. This release is a good argument against that. The changes that will cost you time are not in the "Breaking Changes" heading you read once. They are in the gap between "my JSON is valid" and "an agent can actually transact with me", and additionalProperties: true is exactly where that gap lives.

If you maintain UCP tooling, the practical takeaway is narrower: stop hard-coding field names, and make your validator report which layout it detected rather than silently assuming one.


UCP is an open standard co-developed by Google and Shopify. UCPtools is an independent community tool and is not affiliated with either company. Every schema detail above was read from the published specification at tag v2026-08-25 in the Universal-Commerce-Protocol/ucp repository, not from the release notes.

You can check a profile against v2026-08-25 with the free validator at ucptools.dev, or run it in CI with the UCP validate GitHub Action.

← Back to Blog

UCP v2026-08-25 Breaks Quietly: 4 Changes That Still Pass Validation | UCP.tools