Skip to content

Commit 49aee52

Browse files
igrigorikACSchil
andauthored
feat: eligibility claims & verification contract (Universal-Commerce-Protocol#250)
* feat: eligibility claims & verification contract Introduce `context.eligibility` — buyer claims about eligible benefits (loyalty membership, payment instrument perks, etc.) that Businesses can act on across the shopping lifecycle. Processing model: - Platform provides claims via context.eligibility on any request - Business MAY act on recognized claims (adjust pricing, product access, provisional discounts); MUST ignore unrecognized claims - At checkout completion, all claims that influenced the checkout MUST be resolved: verified against proof, or rescinded by Platform - Unresolved claims block completion (invalid_eligibility error) - Business MUST NOT mutate checkout on verification failure Layering: - context.json: eligibility array with reverse-domain $ref validation - checkout.md: normative verification contract (core obligation) - discount.json: provisional + eligibility fields on applied_discount for structured attribution when discount extension is active - catalog/index.md: MAY adjust price/list_price for eligible claims - error_code.json: invalid_eligibility standard error Key design decision: eligibility lives on context (not PaymentInstrument) enabling full-funnel coverage from catalog through checkout. Verification is a core checkout concern; the discount extension adds attribution. * align error code to noun_adjective pattern * fix: address PR Universal-Commerce-Protocol#250 review feedback Terminology: - "claims that influenced the checkout" → "accepted claims" throughout, aligning with the accepted/not-accepted partition (maximenajim, ACSchil) - "not applied" → "not accepted" for consistency Verification semantics: - Clarify that verification failure MUST only affect the messages array, not checkout state (line items, totals, discounts, etc.) (maximenajim) - Add path field to eligibility_invalid example for machine-readable partial failure identification (ACSchil) Messages contract: - Add MAY use type: "info" to explain effects of accepted claims, complementing SHOULD warn on rejection (maximenajim) Catalog: - Add non-binding pricing contract to "Relationship to Checkout" section: catalog responses are not transactional commitments, checkout is authoritative, responses SHOULD NOT be reused across sessions without re-validation (maximenajim) * add eligibility message codes for warning and info Standardize codes for the three eligibility message types: eligibility_not_accepted (warning), eligibility_accepted (info), and eligibility_invalid (error at completion). * remove display MUST from provisional Per TC discussion, this is already captured via messages flow. --------- Co-authored-by: Alex Schillinger <alexcschillinger@gmail.com>
1 parent 1b3e722 commit 49aee52

6 files changed

Lines changed: 202 additions & 2 deletions

File tree

docs/specification/catalog/index.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ Catalog operations return product and variant IDs that can be used directly in
4848
checkout `line_items[].item.id`. The variant ID from catalog retrieval should match
4949
the item ID expected by checkout.
5050

51+
Catalog responses (pricing, availability, etc.) reflect the Business's current
52+
terms for the given request but are not transactional commitments — checkout
53+
is authoritative. Responses can be session-specific and **SHOULD NOT** be
54+
reused across sessions without re-validation.
55+
5156
## Shared Entities
5257

5358
### Context
@@ -69,6 +74,11 @@ the presentment currency differs, businesses SHOULD convert before applying
6974
(see [Price Filter](search.md#price-filter)). Response prices include
7075
explicit currency codes confirming the resolution.
7176

77+
When `context.eligibility` claims are present, Businesses that accept them
78+
**MAY** adjust `price` / `list_price` directly for strikethrough display and
79+
**MAY** use `messages` with `code: "eligibility_benefit"` to attribute the
80+
adjustment to a specific claim.
81+
7282
{{ schema_fields('types/context', 'catalog') }}
7383

7484
### Signals

docs/specification/checkout.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ handle with specific, appropriate UX rather than generic error treatment.
240240
| `item_unavailable` | Item cannot be purchased (e.g. delisted) |
241241
| `address_undeliverable` | Cannot deliver to the provided address |
242242
| `payment_failed` | Payment processing failed |
243+
| `eligibility_invalid` | Eligibility claim could not be verified at completion |
243244

244245
Businesses **SHOULD** mark standard errors with `severity: recoverable` to
245246
signal that platforms should provide appropriate UX (out-of-stock messaging,
@@ -249,6 +250,82 @@ messages or deferring to checkout completion.
249250
Example: `out_of_stock` requires specific upfront UX, whereas
250251
`payment_required` can be handled generically at submission.
251252

253+
#### Eligibility Verification at Completion
254+
255+
Platforms provide `context.eligibility` — buyer claims about eligible benefits
256+
such as loyalty membership, payment instrument perks, and similar. These are
257+
claims, not verified facts. Businesses **MAY** act on recognized claims during
258+
the session (adjusting pricing, granting product access, applying provisional
259+
discounts), but all accepted claims **MUST** be resolved before the
260+
transaction can complete.
261+
262+
Unrecognized or inapplicable claims **MUST NOT** block the checkout.
263+
Businesses **SHOULD** notify the buyer via `messages` with `type: "warning"`
264+
when a claim is not accepted, and **MAY** use `type: "info"` to explain
265+
the effects of accepted claims. At completion, accepted claims that remain
266+
unverified **MUST** result in `type: "error"` with
267+
`code: "eligibility_invalid"` (see below).
268+
269+
**Eligibility message codes:**
270+
271+
| Type | Code | When |
272+
| --------- | -------------------------- | -------------------------------------------------- |
273+
| `warning` | `eligibility_not_accepted` | Claim not recognized or not applicable |
274+
| `info` | `eligibility_accepted` | Effect of an accepted claim |
275+
| `error` | `eligibility_invalid` | Accepted claim could not be verified at completion |
276+
277+
A claim is resolved when it is either **verified** or **rescinded**:
278+
279+
* **Verified**: The Business confirms the claim against a proof provided at
280+
completion time. UCP does not prescribe how verification occurs — proof
281+
may come from the payment credential, an identity verification capability,
282+
or any other mechanism negotiated between Platform and Business.
283+
* **Rescinded**: The Platform removes the claim from `context.eligibility`
284+
before completion (e.g., buyer changes payment method, withdraws a
285+
membership claim). Once removed, the Business recalculates without it.
286+
287+
Businesses **MUST NOT** complete a transaction with unresolved eligibility
288+
claims. Unverified claims may result in incorrect pricing or unauthorized
289+
access to restricted products.
290+
291+
**When verification fails:**
292+
293+
Verification failure **MUST** only affect the `messages` array. The
294+
Business **MUST** return an error in `messages` with
295+
`code: "eligibility_invalid"` and `severity: "recoverable"`. Messages
296+
**SHOULD** use the `path` field to identify which specific claim(s) could
297+
not be verified. The Platform **MAY** then provide valid proof and
298+
resubmit, restructure the checkout (e.g., remove ineligible items, update
299+
claims), or abandon the attempt.
300+
301+
For example, the Platform claims a store card benefit via
302+
`context.eligibility`. The Business applies member pricing during the session.
303+
At completion, the payment credential does not match the claimed instrument:
304+
305+
```json
306+
{
307+
"ucp": { "version": "2026-01-11", "status": "success" },
308+
"id": "checkout_abc",
309+
"status": "ready_for_complete",
310+
"line_items": [ "..." ],
311+
"totals": [ "..." ],
312+
"messages": [
313+
{
314+
"type": "error",
315+
"code": "eligibility_invalid",
316+
"severity": "recoverable",
317+
"content": "Payment credential does not match the claimed store card benefit.",
318+
"path": "$.context.eligibility[0]"
319+
}
320+
]
321+
}
322+
```
323+
324+
The Platform can resolve this by having the buyer switch to the qualifying
325+
payment instrument, or by removing the claim from `context.eligibility` to
326+
renegotiate the checkout (obtaining updated pricing, availability, etc.)
327+
and then resubmitting for completion.
328+
252329
## Continue URL
253330

254331
The `continue_url` field enables checkout handoff from platform to business UI,

docs/specification/discount.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,101 @@ segment, or promotional rules:
188188
- Cannot be removed by the platform
189189
- Surfaced for transparency (platform can explain to user why discount was applied)
190190

191+
## Eligibility Claims
192+
193+
Eligibility claims are buyer claims about eligible benefits (see
194+
[Context](checkout.md#context)) such as loyalty membership, payment instrument
195+
perks, and similar. When the discount extension is active, Businesses that
196+
choose to accept eligibility claims **MUST** surface their effect on pricing
197+
as provisional discounts in the `applied` array. Platforms **MUST** display
198+
provisional discounts to the buyer.
199+
200+
### Discount Behavior
201+
202+
Platforms send buyer claims via `context.eligibility` on cart or checkout
203+
requests (see [Context](checkout.md#context)). When a Business recognizes a
204+
claim and it affects pricing, it **MUST** surface a corresponding provisional
205+
discount in the `discounts.applied` array. This gives the Platform structured
206+
attribution to display to the buyer.
207+
208+
Eligibility-triggered discounts use the following fields:
209+
210+
| Field | Value | Purpose |
211+
| ------------- | -------------------------- | --------------------------------------- |
212+
| `automatic` | `true` | No code required |
213+
| `provisional` | `true` | Requires verification at completion |
214+
| `eligibility` | `"com.example.store_card"` | The accepted claim |
215+
| `code` | *(omitted)* | Not code-based |
216+
217+
Standard `priority`, `method`, and `allocations` fields apply for stacking with
218+
other discounts.
219+
220+
### Verification at Checkout
221+
222+
Discounts from accepted but unverified claims carry `provisional: true`.
223+
Provisional discounts remain until the claim is verified, rescinded, or
224+
replaced during the session. At checkout completion, all remaining provisional
225+
claims **MUST** be resolved (see
226+
[Eligibility Verification at Completion](checkout.md#eligibility-verification-at-completion)).
227+
228+
### Example: Provisional Discount with Attribution
229+
230+
Building on the store card example from
231+
[Eligibility Verification at Completion](checkout.md#eligibility-verification-at-completion),
232+
the discount extension provides structured attribution. The Platform claims a
233+
store card benefit; the Business surfaces the provisional discount with full
234+
stacking and allocation details:
235+
236+
=== "Request"
237+
238+
```json
239+
{
240+
"context": {
241+
"eligibility": ["com.example.store_card"]
242+
},
243+
"line_items": [
244+
{
245+
"item": {
246+
"id": "prod_shirt",
247+
"quantity": 2,
248+
"price": 2500
249+
}
250+
}
251+
]
252+
}
253+
```
254+
255+
=== "Response"
256+
257+
```json
258+
{
259+
"discounts": {
260+
"applied": [
261+
{
262+
"title": "Store Card 5% Off",
263+
"amount": 250,
264+
"automatic": true,
265+
"provisional": true,
266+
"eligibility": "com.example.store_card",
267+
"priority": 1,
268+
"method": "each",
269+
"allocations": [
270+
{"path": "$.line_items[0]", "amount": 250}
271+
]
272+
}
273+
]
274+
},
275+
"totals": [
276+
{"type": "subtotal", "display_text": "Subtotal", "amount": 5000},
277+
{"type": "items_discount", "display_text": "Discounts", "amount": 250},
278+
{"type": "total", "display_text": "Total", "amount": 4750}
279+
]
280+
}
281+
```
282+
283+
The Platform can now render: "Store Card 5% Off: -$2.50 *(verified at
284+
purchase)*" with full confidence in the attribution, amount, and allocation.
285+
191286
## Impact on Line Items and Totals
192287

193288
Applied discounts are reflected in the core cart or checkout fields using two

source/schemas/shopping/discount.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"$id": "https://ucp.dev/schemas/shopping/discount.json",
44
"name": "dev.ucp.shopping.discount",
55
"title": "Discount Extension",
6-
"description": "Extends Cart and Checkout with discount code support, enabling agents to apply promotional, loyalty, referral, and other discount codes.",
6+
"description": "Extends Cart and Checkout with discount support, including discount codes, automatic discounts, and eligibility-triggered provisional discounts.",
77
"$defs": {
88
"allocation": {
99
"type": "object",
@@ -61,6 +61,15 @@
6161
"minimum": 1,
6262
"description": "Stacking order for discount calculation. Lower numbers applied first (1 = first)."
6363
},
64+
"provisional": {
65+
"type": "boolean",
66+
"default": false,
67+
"description": "True if this discount requires additional verification."
68+
},
69+
"eligibility": {
70+
"$ref": "types/reverse_domain_name.json",
71+
"description": "The eligibility claim accepted by the Business for this discount. Corresponds to a value from context.eligibility. Omitted for code-based and non-eligibility automatic discounts."
72+
},
6473
"allocations": {
6574
"type": "array",
6675
"items": {

source/schemas/shopping/types/context.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929
"currency": {
3030
"type": "string",
3131
"description": "Preferred currency (ISO 4217, e.g., 'EUR', 'USD'). Businesses determine presentment currency from context and authoritative signals; this hint MAY inform selection in multi-currency markets. Also serves as the denomination for price filter values — platforms SHOULD include this field when sending price filters. Response prices include explicit currency confirming the resolution."
32+
},
33+
"eligibility": {
34+
"type": "array",
35+
"description": "Buyer claims about eligible benefits such as loyalty membership, payment instrument perks, and similar. Recognized claims MAY inform the Business response (e.g., member-only product availability, adjusted pricing in catalog, provisional discounts at cart or checkout). Businesses MUST ignore unrecognized values without error. Values MUST use reverse-domain naming (e.g., 'com.example.loyalty_gold', 'org.school.student') and MUST be non-identifying.",
36+
"uniqueItems": true,
37+
"items": {
38+
"$ref": "reverse_domain_name.json"
39+
}
3240
}
3341
}
3442
}

source/schemas/shopping/types/error_code.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"out_of_stock",
99
"item_unavailable",
1010
"address_undeliverable",
11-
"payment_failed"
11+
"payment_failed",
12+
"eligibility_invalid"
1213
]
1314
}

0 commit comments

Comments
 (0)