Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/specification/shopping/cart/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,8 +482,8 @@ operations unless otherwise noted.
* **Idempotency-Key**: Operations that modify state **SHOULD** support
idempotency. When provided, the server **MUST**:
1. Store the key with the operation result for at least 24 hours.
2. Return the cached result for duplicate keys whose request body matches the original.
3. Return `409 Conflict` if the key is reused with a mismatched body.
2. Return the cached result for duplicate keys whose payload matches the original.
3. Return `409 Conflict` if the key is reused with a mismatched payload.
See [Message Signatures — Idempotency Key Requirements](../../signatures.md#replay-protection)
for the full payload-matching contract.

Expand Down
8 changes: 8 additions & 0 deletions docs/specification/shopping/checkout/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ The `meta["ucp-agent"]` field is **required** on all requests to enable
`meta["idempotency-key"]` for retry safety. Platforms **MAY** include
additional metadata fields.

When the request is signed, the covered `Idempotency-Key` header (see
[Message Signing](#message-signing)) is authoritative for the idempotency
key; `meta["idempotency-key"]` **MUST** equal it, and the Business
**MUST** reject a mismatch without executing. On unsigned requests,
`meta["idempotency-key"]` alone is authoritative. See
[Message Signatures — Replay Protection](../../signatures.md#replay-protection)
for the full payload-matching contract.

## Tools

UCP Capabilities map 1:1 to MCP Tools.
Expand Down
4 changes: 2 additions & 2 deletions docs/specification/shopping/checkout/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -1308,8 +1308,8 @@ operations unless otherwise noted.
* **Idempotency-Key**: Operations that modify state **SHOULD** support
idempotency. When provided, the server **MUST**:
1. Store the key with the operation result for at least 24 hours.
2. Return the cached result for duplicate keys whose request body matches the original.
3. Return `409 Conflict` if the key is reused with a mismatched body.
2. Return the cached result for duplicate keys whose payload matches the original.
3. Return `409 Conflict` if the key is reused with a mismatched payload.
See [Message Signatures — Idempotency Key Requirements](../../signatures.md#replay-protection)
for the full payload-matching contract.

Expand Down
76 changes: 62 additions & 14 deletions docs/specification/signatures.md
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,13 @@ Signature-Input: sig1=("@method" "@authority" "@path" "idempotency-key" ...);key
Signature: sig1=:6G4i8TS6oUkGrx8KnCFUpsSPwd74...:
```

Some bindings also carry the key inside the request payload (over MCP,
`meta["idempotency-key"]` in the tool arguments). When the request is
signed, the covered `Idempotency-Key` header is authoritative for the
idempotency key; a payload copy of the key **MUST** equal it, and
Businesses **MUST** reject a mismatch without executing. On unsigned
requests, the payload copy alone is authoritative.

**Idempotency Key Requirements:**

| Requirement | Value |
Expand All @@ -834,18 +841,57 @@ Signature: sig1=:6G4i8TS6oUkGrx8KnCFUpsSPwd74...:
| **On duplicate (mismatched payload)** | Reject with `409 Conflict` (REST) / `-32000` (MCP); do not execute |
| **On storage failure** | Fail closed (reject request with 503) |

**Payload Matching:** Businesses **MUST** detect whether the payload of
a duplicate-key request matches the payload of the original by
comparing the SHA-256 hash of the raw body bytes — the same digest
RFC 9530 mandates as `Content-Digest`. When signing is in use, this
value is supplied in the `Content-Digest` header and the Intermediary
Warning above guarantees byte fidelity end-to-end; businesses persist
it alongside the idempotency key. For unsigned requests, businesses
compute the same digest from the received body bytes. Platforms
therefore **MUST** generate a fresh idempotency key whenever they
modify the request payload — including retries with modified payment
instruments, updated shipping addresses, swapped line items, or any
other change to the request body.
**Payload Matching:** A stored idempotency key binds the operation it
was first used with and, where one exists, the target resource
identifier (the path parameter for REST, the top-level `id` argument
for MCP). Businesses **MUST** reject, without executing, any request
that reuses a stored key with a different operation or a different
target resource identifier. Within that binding, payload identity is
defined per operation, by whether the operation's arguments carry
anything beyond `meta` and the target resource identifier — a class
decidable from the operation's input schema alone, not from the
transport envelope:

* **Target-only operations.** The operation's arguments carry nothing
beyond `meta` and the target resource identifier (the path parameter
for REST, the top-level `id` argument for MCP) — `cancel_checkout` is
in this class by schema. Payload identity is the key binding itself:
Businesses persist the operation and the target resource identifier
alongside the key, and **MUST** treat a request as a replay, and
return the stored result, when key, operation, and identifier all
match the stored record. No hashing or canonicalization applies to
this class.
* **Payload-carrying operations.** The operation's arguments carry
anything beyond `meta` and the target resource identifier —
`complete_checkout` is in this class by schema. Businesses **MUST**
detect a payload mismatch by comparing the SHA-256 hash of the
operation's arguments with the transport envelope excluded — not the
full JSON-RPC message bytes, whose envelope fields (the JSON-RPC
`id`, `meta`) are guaranteed to vary per retry. The hash is compared
within the key binding above, so a reused key is rejected on an
operation or target mismatch before any hash comparison, and
Businesses persist this hash alongside the key. The hashed input is
transport specific:

* For REST, the raw request body bytes as received, the same digest
RFC 9530 mandates as `Content-Digest`. No canonicalization: the
body is a single byte string on the wire and is hashed as such.
* For MCP, the `params.arguments` object with its top-level
`meta` member removed, serialized with the JSON
Canonicalization Scheme
([RFC 8785](https://datatracker.ietf.org/doc/html/rfc8785)), the
canonicalization this specification already uses for durable
artifacts in
[AP2 Mandates](payment/extensions/ap2-mandates.md#canonicalization).
Removing a member is an operation on a parsed value, not on bytes,
so this class adopts canonicalization explicitly: logically
identical retries hash identically regardless of member order or
serializer, tolerating re-serialization across client stacks.

Platforms **MUST** generate a fresh idempotency key whenever they modify
a payload-carrying operation's arguments — including retries with
modified payment instruments, updated shipping addresses, swapped line
items, or any other change to the request payload.

**Note:** For **default UCP** signatures, the RFC 9421 `created`
parameter is **OPTIONAL** and replay protection is handled at the
Expand Down Expand Up @@ -906,8 +952,10 @@ Signature: sig1=:6G4i8TS6oUkGrx8KnCFUpsSPwd74...:
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"complete_checkout","arguments":{"id":"chk_123","checkout":{...}}}}
```

The JSON-RPC message is the HTTP body. `Content-Digest` binds it to the signature.
No JSON canonicalization is required.
The JSON-RPC message is the HTTP body. `Content-Digest` binds it to the signature;
no JSON canonicalization is required for signing. For payload matching, the
payload-carrying class canonicalizes `params.arguments` with JCS, as defined under
[Replay Protection](#replay-protection).

## Error Handling

Expand Down
Loading