diff --git a/docs/specification/payment-terms.md b/docs/specification/payment-terms.md index 4a4dfdbb0..4246e2242 100644 --- a/docs/specification/payment-terms.md +++ b/docs/specification/payment-terms.md @@ -20,138 +20,227 @@ ## Overview -The Payment Terms extension lets a business describe when payment is due for a -checkout. A buyer-facing **payment term** is composed of one or more **payment -schedules**. Each schedule can be due immediately, due at a future date, or due -on a recurring cadence. +The Payment Terms extension lets a Business offer the Buyer a choice of **when** +payment for a checkout is due. A lodging Business can offer one term that +charges the full stay at booking and another that charges the first night now +and the balance at check-in. A Business selling equipment can offer Net-30 +alongside pay-now. -This is intended for payment timing, not for changing what is being purchased. -For example, a lodging checkout can offer one term where the first night is paid -at booking time and the remaining stay is paid at check-in. +A **payment term** is an addressable, renderable choice composed of one or more +**payment schedules**. Each schedule is one payment: an amount, and a complete +buyer-facing statement of when it is due. -This extension adds to `checkout.payment`: +This extension adds two properties to `checkout.payment`: -* `available_terms[]` — checkout-wide payment terms the buyer can choose from. -* `instruments[].term_refs[]` — selection of one term and optional assignment - of instruments to schedules within that term. +* `terms[]` — the payment terms the Business offers for this checkout. + Response-only. +* `selected_term_id` — the selected term. Always present in a response; + a Platform writes it to change the selection. -It also adds `order.payment.terms[]`, containing exactly one complete snapshot -of the accepted `available_terms[]` entry. Payment handlers can use -`available_instruments[].constraints.term_refs[]` to describe which terms or -schedules an instrument supports. +A Buyer picks one of the options in `terms[]`, the same way they pick +one [fulfillment option](fulfillment.md#platform-responsibilities). -## Schema - -### Payment +## Presenting payment terms -When this capability is active, `checkout.payment` is extended with available -terms and term-aware payment instruments. +Presentation is **term-agnostic**: a Platform does not need to model deposits, +installments, or trade credit to present a term meaningfully. -{{ extension_schema_fields('payment_terms.json#/$defs/payment', 'payment_terms') }} - -### Entities - -#### Available Payment Term - -{{ schema_fields('types/available_payment_term', 'payment_terms') }} - -#### Payment Schedule - -{{ schema_fields('types/payment_schedule', 'payment_terms') }} +Given the Checkout's `currency`, a Platform that recognizes no `type` value and +reads no payment-term field other than `title`, `description`, +`schedules[].description`, and `schedules[].totals` **MUST** be able to present +what is owed and when, for every term. A Business **MUST** author terms so this +holds. -#### Schedule Type +That floor covers amounts and timing. It does not by itself discharge a +jurisdictional disclosure duty — a finance charge that must be given a +prescribed prominence, for instance. Those obligations travel through +[Disclosures](#disclosures), and a Platform that cannot honor them escalates +rather than renders. -{{ schema_fields('types/schedule_type', 'payment_terms') }} +Everything above that floor is supplementary. `type` and `due_at` let a Platform +that wants to do more — split the checkout into due-now and due-later, sort +schedules, drive a calendar reminder — without ever being required to. -#### Entity Reference - -{{ schema_fields('types/entity_ref', 'payment_terms') }} - -#### Available Payment Instrument (Payment Terms) - -{{ extension_schema_fields('payment_terms.json#/$defs/available_payment_instrument', 'payment_terms') }} +## Payment schedules -#### Payment Instrument (Payment Terms) +A schedule is **one payment**, not a timetable. A term that charges four times +has four schedules. -{{ extension_schema_fields('payment_terms.json#/$defs/payment_instrument', 'payment_terms') }} +A Business **MUST** make each schedule's `description` a complete buyer-facing +statement of when and how that payment is due, so that a Platform can render it +verbatim. A Platform **MAY** use `type` and `due_at` for a richer presentation +— a calendar view, a countdown, a reminder — but **MUST NOT** present derived +timing that contradicts the `description`. Recognizing a `type` only enables +optional enhancement; the baseline contract is that `description` and `total` +are sufficient. -## Payment schedules +### Timing class -Payment schedules use `type` as an open string vocabulary. Well-known values are: +`type` is an open string vocabulary with exactly one well-known value: | Type | Meaning | | :--- | :------ | -| `immediate` | Payment is due when checkout is completed. | -| `deferred` | Payment is due after checkout completion, as stated in `description`. | - -Businesses MAY use additional values. Platforms MUST tolerate unknown schedule -types and either apply business-specific support or surface clear validation -messages when a selected payment instrument cannot satisfy them. - -Every schedule has a buyer-facing `description` that **MUST** completely state -when and how its payment or payments are due. The `type` field provides only a -broad timing class; platforms do not infer dates or processor instructions from -it. - -Each schedule's `totals[]` is the aggregate amount of every payment represented -by that schedule. A schedule may describe several occurrences in text, but its -`total` covers the complete series. If individual occurrences need different -instrument assignments, the business represents them as separate schedules. -The sum of schedule `total` entries **MUST** equal the checkout `total` exactly -once. - -Schedules settle only the current checkout; they do not create future purchases, -orders, renewals, or fulfillment obligations. They disclose payment timing but -do not themselves define credential storage, future-charge authorization, -processor capture behavior, or mandate requirements. - -## Instrument constraints - -Payment handlers declare term and schedule support with -`available_instruments[].constraints.term_refs`. Each `term_refs[]` entry is an -Entity Reference interpreted in the payment terms context. - -References can target: - -* **Payment term ID** — `{ "id": "pt_deposit_balance" }` applies to all - schedules in that term. -* **Payment schedule ID** — `{ "schedule_id": "balance_at_check_in" }` applies - to matching schedules. -* **Schedule type** — `{ "schedule_type": "immediate" }` applies to all - schedules with that timing class. -* **Compound refs** — `{ "id": "pt_deposit_balance", "schedule_type": - "immediate" }` applies only to schedules that match both selectors. - -For instrument assignment, every reference **MUST** include `id`, and all -assignment references in one checkout **MUST** identify the same available -term. `schedule_id` and `schedule_type` only narrow that selected term. Handler -constraints may omit `id` to express support across terms. Empty references are -invalid in both contexts. - -Businesses SHOULD make payment schedule IDs unique across -`payment.available_terms[]` when platforms are expected to reference schedules -by `schedule_id` alone. If the same schedule ID appears in more than one term, -a `schedule_id`-only reference matches every schedule with that ID; include `id` -to scope the match to a specific payment term. - - -```json -{ - "type": "gift_card", - "constraints": { - "term_refs": [ - { "schedule_type": "immediate" } - ] - } -} -``` - -When the [Split Payments](split-payments.md) extension is also active, -instruments may include an `amount` field to specify a fixed contribution when -multiple instruments share a schedule. A fixed contribution **MUST** resolve to -exactly one schedule. Contributions to each schedule **MUST** equal that -schedule's aggregate total, and every schedule **MUST** be covered by at least -one eligible instrument. +| `immediate` | The payment is due when the checkout is completed. | + +Any other value means the payment is **not** due at completion; the +`description` states when it is due. A Platform **MUST** treat an unrecognized +`type` as not due at completion. Businesses **MAY** use additional values +such as `deferred` or `on_shipment`, but those values carry no protocol meaning +beyond "not `immediate`". + +### Due dates + +`due_at` is an absolute RFC 3339 date-time. A Business **SHOULD** provide it +when it can determine the due date at checkout, and **MUST** omit it when the +date depends on a future event — "due on delivery" has no date yet. `due_at` +never replaces `description`; it restates in machine-readable form what the +description already says. + +The protocol defines no calendar arithmetic. A Business that offers four +biweekly payments emits four schedules with four computed dates, rather than a +recurrence rule a Platform would have to expand. This keeps month-end, daylight +saving, and rounding-residual decisions with the party that already makes them. + +### Amounts + +Each schedule's `totals` states what this one payment costs. A Business **MUST** +include exactly one `type: total` entry with a non-negative amount: the final +amount charged when this payment is taken, inclusive of tax and every other +charge. Unlike checkout totals, no `subtotal` is required, because the purchase +is priced at the checkout rather than once per payment. + +A term's schedule `total` entries define the amount payable under that term. For +the **selected** term, the Business **MUST** ensure that sum equals the checkout +`total`. + +Unselected terms are **indicative**. A term that discounts the purchase for +paying today has a different payable amount than one that defers part of it, so +at most one term can match the checkout total at any moment. Only the selected +term is bound to it. + +## Selecting a payment term + +Selecting a payment term is a **Checkout mutation**. A Platform sets +`selected_term_id`; the Business returns a recomputed Checkout. That response is +**authoritative for all derived state** — including `totals`, line item prices, +discount eligibility, `policies[]`, `messages[]`, and the payment handlers +offered. + +A Platform **MUST NOT** assume that the amounts shown in `terms[]` +survive selection unchanged, and **MUST** re-render from the response. + +A Business **MUST** make `terms[].id` unique within a Checkout, so +that a selection resolves to exactly one term. + +A term is always selected. The Checkout `total` is the selected term's total, so +a response without a selection would show an amount that matches no stated +terms. A Business **MUST** return `selected_term_id` in every response. Where +the Buyer has made no choice, the Business selects a default. + +A Platform changes the selection through Update Checkout, setting +`selected_term_id` to an `id` from the latest `terms[]`. A Platform **MUST** +omit it on create, because term IDs are scoped to a Checkout and no options +exist yet — the create response establishes both the options and the default. A +Platform **MUST** omit it on complete, because the term is settled before a +Checkout can reach `ready_for_complete`, and the instrument is authorized +against the selected term's total. A Business receiving a selection that no +longer resolves — because the options changed — **MUST NOT** silently substitute +a term, and **MUST** report the change as a `payment_term_changed` warning in +`messages[]`. + +Selecting a term can invalidate a selection previously accepted elsewhere in the +Checkout — a deferred term may not be available with a same-day fulfillment +option. The Business resolves the conflict, returns the authoritative state, and +**MUST** report the change as a `payment_term_changed` warning in `messages[]`. +A Platform can therefore detect a changed selection from the code alone, rather +than by comparing responses. + +## Payment instruments and eligibility + +This extension does not change how instruments are supplied. The Buyer's +instruments fund the checkout under the selected term, and the Business +allocates them across that term's schedules. + +Checkout-specific handler and instrument eligibility is a **runtime result**. +Profiles advertise broad support; the Business resolves that support against the +Checkout context and any instruments already supplied, then returns the +authoritative `ucp.payment_handlers` in the response. This covers cases a +discovery-time predicate cannot express — a gift card whose balance is below the +deposit, an issuer that will not support a delayed capture for this Business, a +credential that expires before the balance comes due. + +A Business **MUST NOT** publish payment-term identifiers in a discovery-time +handler profile. Term IDs are scoped to one checkout; a cacheable, +Buyer-independent profile cannot reference them. + +Per-schedule funding — directing one instrument at one schedule and a different +instrument at another — is not defined in this version. A Business that supports +it does so outside the protocol. + +## Disclosures + +Some terms carry display obligations. A subscription that renews, an +installment plan with a finance charge, and a deposit that is forfeited on +cancellation are all subject to consumer-protection rules about what must be +shown, and when. + +This extension does not define a private disclosure channel. It uses the two +that already exist: + +1. A [policy](overview.md#policies) carries the durable terms text, targeted + with `applies_to` at the node the terms concern — the payment term when the + terms are about payment timing, the line item when they are about the goods. +2. A `messages[]` warning with `presentation: "disclosure"` and `code` set to + that policy's `type` compels display of the notice. + +These mechanisms carry durable terms and compel their display. The Business +remains responsible for the required content, its applicability, its timing, and +any affirmative Buyer acknowledgment. In particular, presenting a policy is +optional for a Platform, so any content that **must** reach the Buyer belongs in +the warning `content`, not only in the policy `description`. + +Disclosure display is unconditional. Under [Warning +Presentation](checkout.md#warning-presentation) a Platform **MUST** display +every returned disclosure, **MUST** keep it in proximity to the node named by +`path`, and **MUST NOT** hide, collapse, or auto-dismiss it. A Platform that +cannot honor that contract — for example one that collapses a list of terms and +so cannot preserve proximity for each — **MUST** escalate through `continue_url` +rather than silently dropping the notice. + +An obligation disclosed at checkout does not end at checkout: it records money +still owed, not context. Where a disclosure governed the term the Buyer +accepted, the Business **MUST** return that disclosure on the Order with its +`path` set to `$.payment.accepted_term`, and **MUST** use the same target in +the `applies_to` of any policy paired with it. A Business **MUST NOT** return +disclosures attached to terms the Buyer did not accept. + +`applies_to` and `path` resolve against the response they appear in, so a target +that named the right node on the Checkout does not necessarily name it on the +Order; an Order's line items, for instance, are current-state data whose +positions need not match. A Business **MUST** ensure that every target it emits +resolves to the intended node on the response that carries it. + +## Out of scope + +**Amounts that are not determinable at checkout.** A schedule states a known +amount due at a stated time. Hotel incidentals, usage overages, and +post-purchase true-ups are not payment schedules; they are authorizations and +order adjustments. + +**Multiple currencies within one term.** Every schedule total is denominated in +the Checkout `currency`, so a term cannot express an obligation payable partly +in another currency. + +**Recurring commerce.** Schedules settle the current checkout. They do not +create future purchases, renewals, or fulfillment obligations. A Business +enrolling a Buyer in a subscription charges the first cycle through a payment +term and discloses the ongoing arrangement through a policy and its paired +disclosure. See [Subscription enrollment](#subscription-enrollment). + +**Payment execution.** This extension discloses when money is due. It does not +define credential storage, future-charge authorization, how a Business executes +capture, or mandate requirements, and a Platform **MUST NOT** infer that a +deferred payment can be inspected, modified, or cancelled through UCP. ## Discovery @@ -179,61 +268,90 @@ Businesses advertise payment terms support in their profile: } ``` -## Operations +## Schema + +### Payment + +When this capability is active, `checkout.payment` is extended with available +terms and the selected term. + +{{ extension_schema_fields('payment_terms.json#/$defs/payment', 'payment_terms') }} + +### Order Payment + +On the Order, `payment` carries the accepted term. The terms offered at checkout +are not projected. -### Selecting and assigning payment terms +The accepted term is the agreement, not a running balance. When the Order is +created, a Business **MUST** ensure its schedule `total` entries sum to the +Order `total`. A Business **MUST NOT** modify the accepted term after the Order +is created; post-purchase changes are recorded in `adjustments[]`. The schedules +can therefore sum to more than the Order currently owes after a refund, or less +after an exchange: the term states what was agreed, and the adjustments state +what happened after. + +{{ extension_schema_fields('payment_terms.json#/$defs/order_payment', 'payment_terms') }} + +### Entities -The buyer selects one checkout-wide term through -`payment.instruments[].term_refs[]`. Every referenced `id` **MUST** match one -`payment.available_terms[].id`; referenced schedules **MUST** belong to that -term, and every schedule **MUST** be completely funded by eligible instruments. +#### Payment Term -Before authorization, the business **MUST** return a Checkout response containing -the accepted instruments and their `term_refs`. A change to the selected term or -schedule assignment requires a new Checkout response and renewed authorization. -When AP2 is active, these accepted assignments are part of the Checkout state -bound by the mandate. +{{ schema_fields('types/payment_term', 'payment_terms') }} -### Persisting terms on orders +#### Payment Schedule -When this extension is active, a completed Order **MUST** include exactly one -`payment.terms[]` entry. That entry **MUST** equal the selected -`available_terms[]` object, including its schedules and totals. +{{ schema_fields('types/payment_schedule', 'payment_terms') }} ## Examples -### Lodging deposit and balance at check-in +### Lodging: pay now, or deposit and balance at check-in + +> A $1,200 stay. Pay in full today for $1,150, or pay the first night now and +> the balance at check-in. -> Pay the first night at booking time and the rest of the stay at check-in. +The two terms have **different payable amounts**. Their schedules sum to their +own term's total, and only the selected one is bound to the checkout total. -**Checkout response fragment:** +**Checkout response fragment — the terms on offer.** The Buyer has not chosen +yet, so the Business defaults to paying now, and says so: ```json { - "available_terms": [ + "selected_term_id": "pt_pay_now", + "terms": [ + { + "id": "pt_pay_now", + "title": "Pay now", + "description": { "plain": "Save $50 by paying for your stay today." }, + "schedules": [ + { + "id": "sched_full", + "type": "immediate", + "description": { "plain": "Due today when you book." }, + "totals": [{ "type": "total", "amount": 115000 }] + } + ] + }, { - "id": "pt_first_night_balance", - "name": "First night now, balance at check-in", - "description": "Pay $300 now and $900 when you check in.", + "id": "pt_deposit_balance", + "title": "First night now, balance at check-in", + "description": { "plain": "Hold your room with one night's rate." }, "schedules": [ { "id": "sched_first_night", "type": "immediate", - "description": "Due when checkout is completed.", - "totals": [ - { "type": "subtotal", "amount": 30000 }, - { "type": "total", "amount": 30000 } - ] + "description": { "plain": "Due today when you book." }, + "totals": [{ "type": "total", "amount": 30000 }] }, { - "id": "sched_check_in_balance", + "id": "sched_balance", "type": "deferred", - "description": "Due at check-in on September 1, 2026 at 3:00 PM PDT.", - "totals": [ - { "type": "subtotal", "amount": 90000 }, - { "type": "total", "amount": 90000 } - ] + "description": { + "plain": "Due at check-in on September 1, 2026 at 3:00 PM PDT." + }, + "due_at": "2026-09-01T15:00:00-07:00", + "totals": [{ "type": "total", "amount": 90000 }] } ] } @@ -241,236 +359,343 @@ When this extension is active, a completed Order **MUST** include exactly one } ``` -**Complete request payment fragment:** +**Update request — the Buyer selects the deposit term:** - + ```json { - "instruments": [ - { - "id": "pi_card_1", - "handler_id": "handler_1", - "type": "card", - "credential": { - "type": "token", - "token": "tok_visa_xxxx" - }, - "term_refs": [ - { "id": "pt_first_night_balance" } + "selected_term_id": "pt_deposit_balance" +} +``` + +**Checkout response — recomputed and authoritative:** + + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.shopping.checkout": [{ "version": "{{ ucp_version }}" }], + "dev.ucp.shopping.payment_terms": [{ "version": "{{ ucp_version }}" }] + }, + "payment_handlers": { + "com.example.card_handler": [ + { + "id": "card_handler", + "version": "{{ ucp_version }}", + "available_instruments": [{ "type": "card" }] + } ] } - ] + }, + "id": "checkout_123", + "status": "incomplete", + "currency": "USD", + "line_items": [], + "links": [ + { "type": "terms_of_service", "url": "https://example.com/tos" } + ], + "totals": [ + { "type": "subtotal", "amount": 120000 }, + { "type": "total", "amount": 120000 } + ], + "payment": { + "selected_term_id": "pt_deposit_balance", + "terms": [ + { + "id": "pt_pay_now", + "title": "Pay now", + "description": { "plain": "Save $50 by paying for your stay today." }, + "schedules": [ + { + "id": "sched_full", + "type": "immediate", + "description": { "plain": "Due today when you book." }, + "totals": [{ "type": "total", "amount": 115000 }] + } + ] + }, + { + "id": "pt_deposit_balance", + "title": "First night now, balance at check-in", + "description": { "plain": "Hold your room with one night's rate." }, + "schedules": [ + { + "id": "sched_first_night", + "type": "immediate", + "description": { "plain": "Due today when you book." }, + "totals": [{ "type": "total", "amount": 30000 }] + }, + { + "id": "sched_balance", + "type": "deferred", + "description": { + "plain": "Due at check-in on September 1, 2026 at 3:00 PM PDT." + }, + "due_at": "2026-09-01T15:00:00-07:00", + "totals": [{ "type": "total", "amount": 90000 }] + } + ] + } + ] + } } ``` -The card is used for both the immediate first-night payment and the deferred -check-in balance. +Because the pay-now discount no longer applies, `checkout.totals` reports +$1,200 — the sum of the selected term's two schedules. Had the Buyer selected +`pt_pay_now`, the checkout total would be $1,150. The terms are unchanged; only +the selected term is bound to the checkout total. `ucp.payment_handlers` in this +response is the set resolved for the selected term. -### Different instruments for immediate and deferred schedules +The deposit terms live in a policy that targets the term they apply to: -> Use a gift card for the deposit, and a card for the check-in balance. + +```json +[ + { + "type": "com.example.policy.deposit_forfeiture", + "applies_to": ["$.payment.terms[1]"], + "description": { + "plain": "The $300 deposit is non-refundable within 48 hours of arrival." + } + } +] +``` -**Complete request payment fragment:** +On completion, the accepted term travels to the Order, so the Buyer can still +see that $900 is due at check-in. The other terms do not travel, and the deposit +disclosure moves with the term it governs: - + ```json { - "instruments": [ + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.shopping.order": [{"version": "{{ ucp_version }}"}], + "dev.ucp.shopping.payment_terms": [{"version": "{{ ucp_version }}"}] + } + }, + "id": "order_9f2", + "checkout_id": "checkout_7c1", + "permalink_url": "https://hotel.example.com/orders/9f2", + "currency": "USD", + "line_items": [ { - "id": "pi_gift_card_1", - "handler_id": "handler_1", - "type": "gift_card", - "credential": { - "type": "token", - "token": "gc_abc123" + "id": "li_room", + "item": { + "id": "room_deluxe", + "title": "Deluxe King, 3 nights", + "price": 120000 }, - "term_refs": [ + "quantity": { "original": 1, "total": 1, "fulfilled": 0 }, + "totals": [ + { "type": "subtotal", "amount": 120000 }, + { "type": "total", "amount": 120000 } + ], + "status": "processing" + } + ], + "fulfillment": { "expectations": [] }, + "totals": [ + { "type": "subtotal", "amount": 120000 }, + { "type": "total", "amount": 120000 } + ], + "payment": { + "accepted_term": { + "id": "pt_deposit_balance", + "title": "First night now, balance at check-in", + "description": { "plain": "Hold your room with one night's rate." }, + "schedules": [ { - "id": "pt_first_night_balance", - "schedule_id": "sched_first_night" - } - ] - }, - { - "id": "pi_card_1", - "handler_id": "handler_1", - "type": "card", - "credential": { - "type": "token", - "token": "tok_visa_xxxx" - }, - "term_refs": [ + "id": "sched_first_night", + "type": "immediate", + "description": { "plain": "Due today when you book." }, + "totals": [{ "type": "total", "amount": 30000 }] + }, { - "id": "pt_first_night_balance", - "schedule_id": "sched_check_in_balance" + "id": "sched_balance", + "type": "deferred", + "description": { + "plain": "Due at check-in on September 1, 2026 at 3:00 PM PDT." + }, + "due_at": "2026-09-01T15:00:00-07:00", + "totals": [{ "type": "total", "amount": 90000 }] } ] } + }, + "policies": [ + { + "type": "com.example.policy.deposit_forfeiture", + "applies_to": ["$.payment.accepted_term"], + "description": { + "plain": "The $300 deposit is non-refundable within 48 hours of arrival." + } + } ] } ``` -If both instruments contribute to the same schedule, use the -[Split Payments](split-payments.md) extension to specify fixed contribution -amounts. +The schedules sum to the Order `total`, and the policy that named +`$.payment.terms[1]` on the Checkout names `$.payment.accepted_term` +here. No representation of `pt_pay_now` survives. ### Installments -> Pay 25% today, then 25% every two weeks for three more payments. +> Pay 25% today, then 25% every two weeks. - +Four payments and four schedules: one due at completion, and three with +computed due dates. The Business does the calendar arithmetic; the Platform +reads dates. + + ```json { "id": "pt_pay_in_4", - "name": "Pay in 4", + "title": "Pay in 4", + "description": { "plain": "Four interest-free payments of $25." }, "schedules": [ { - "id": "sched_installment_1", + "id": "sched_1", "type": "immediate", - "description": "One payment of $25 due when checkout is completed.", - "totals": [ - { "type": "subtotal", "amount": 2500 }, - { "type": "total", "amount": 2500 } - ] + "description": { "plain": "Due today." }, + "totals": [{ "type": "total", "amount": 2500 }] }, { - "id": "sched_installments_2_to_4", + "id": "sched_2", "type": "deferred", - "description": "Three payments of $25 due two, four, and six weeks after checkout.", - "totals": [ - { "type": "subtotal", "amount": 7500 }, - { "type": "total", "amount": 7500 } - ] + "description": { "plain": "Due September 15, 2026." }, + "due_at": "2026-09-15T00:00:00Z", + "totals": [{ "type": "total", "amount": 2500 }] + }, + { + "id": "sched_3", + "type": "deferred", + "description": { "plain": "Due September 29, 2026." }, + "due_at": "2026-09-29T00:00:00Z", + "totals": [{ "type": "total", "amount": 2500 }] + }, + { + "id": "sched_4", + "type": "deferred", + "description": { "plain": "Due October 13, 2026." }, + "due_at": "2026-10-13T00:00:00Z", + "totals": [{ "type": "total", "amount": 2500 }] } ] } ``` -The deferred schedule describes three occurrences, while its totals represent -the aggregate $75 obligation assigned to that schedule. +### Subscription enrollment -### Order with accepted payment terms +> $29.99 per month, cancel anytime. -The Order carries the complete accepted term from the originating Checkout. +The checkout charges the first cycle. The ongoing arrangement is **disclosed**, +not scheduled: a later cycle is a future purchase the Buyer can decline by +cancelling, so it is not an amount owed for this checkout. - +**Payment term — one immediate schedule for cycle one:** + + ```json { - "ucp": { - "version": "{{ ucp_version }}", - "capabilities": { - "dev.ucp.shopping.order": [ - { "version": "{{ ucp_version }}" } - ], - "dev.ucp.shopping.payment_terms": [ - { "version": "{{ ucp_version }}" } - ] + "id": "pt_monthly", + "title": "Monthly", + "description": { "plain": "$29.99 today, then monthly until you cancel." }, + "schedules": [ + { + "id": "sched_cycle_1", + "type": "immediate", + "description": { "plain": "Due today. Covers your first month." }, + "totals": [{ "type": "total", "amount": 2999 }] } - }, - "id": "order_123", - "checkout_id": "checkout_123", - "permalink_url": "https://business.example.com/orders/123", - "line_items": [], - "fulfillment": {}, - "currency": "USD", - "totals": [ - { "type": "subtotal", "amount": 10000 }, - { "type": "total", "amount": 10000 } - ], - "payment": { - "instruments": [ - { - "id": "pi_gift_card", - "type": "gift_card", - "amount": 2500, - "display": { - "description": "Gift card", - "last_digits": "9821" - }, - "term_refs": [ - { - "id": "pt_pay_in_4", - "schedule_id": "sched_installment_1" - } - ] - }, - { - "id": "pi_installment_card", - "type": "card", - "billing_address": { - "street_address": "123 Main St", - "address_locality": "Austin", - "address_region": "TX", - "address_country": "US", - "postal_code": "78701" - }, - "display": { - "brand": "visa", - "last_digits": "4242", - "description": "Visa ending in 4242" - }, - "term_refs": [ - { - "id": "pt_pay_in_4", - "schedule_id": "sched_installments_2_to_4" - } - ] - } - ], - "terms": [ - { - "id": "pt_pay_in_4", - "name": "Pay in 4", - "schedules": [ - { - "id": "sched_installment_1", - "type": "immediate", - "description": "One payment of $25 due when checkout is completed.", - "totals": [ - { "type": "subtotal", "amount": 2500 }, - { "type": "total", "amount": 2500 } - ] - }, - { - "id": "sched_installments_2_to_4", - "type": "deferred", - "description": "Three payments of $25 due two, four, and six weeks after checkout.", - "totals": [ - { "type": "subtotal", "amount": 7500 }, - { "type": "total", "amount": 7500 } - ] - } - ] - } - ] - } + ] } ``` +**Policy — the durable terms.** The recurrence concerns the subscribed item +rather than the payment timing, so it targets the line item — a node the Order +also has, which is what makes the snapshot possible: + + +```json +[ + { + "type": "com.example.policy.subscription", + "description": { + "markdown": "Renews at **$29.99/month** on the 14th until cancelled. Cancel anytime at example.com/account." + }, + "applies_to": ["$.line_items[0]"], + "url": "https://example.com/subscription-terms" + } +] +``` + +**Disclosure — compelled display, paired to the policy by `code`:** + + +```json +[ + { + "type": "warning", + "code": "com.example.policy.subscription", + "path": "$.line_items[0]", + "presentation": "disclosure", + "content": "This subscription renews at $29.99/month on the 14th until you cancel. Cancel at https://example.com/account." + } +] +``` + +A free trial works the same way: the schedule's `total` is `0`, and the +disclosure carries the obligation. A checkout whose total is zero while an +ongoing commitment is being authorized is precisely the case where compelled +display matters most. + ## Platform responsibilities -Platforms SHOULD: +Platforms **MUST**: -* Present available payment terms in a clear, buyer-understandable format. -* Show every schedule's aggregate amount and complete timing description. -* Validate term and schedule references plus instrument constraints before submitting payment. -* Tolerate unknown schedule types and surface clear recovery guidance when a - selected instrument cannot satisfy them. +* Present each term's `title`, and each schedule's `description` and `total` + amount, formatted in the Checkout `currency`. +* Re-render from the Business response after selecting a term, rather than + reusing amounts read from `terms[]`. +* Treat an unrecognized schedule `type` as not due at completion, and present + the term regardless. +* Process disclosures attached to terms per + [Warning Presentation](checkout.md#warning-presentation), escalating through + `continue_url` when the rendering contract cannot be honored. -## Business responsibilities +Platforms **MAY** use `type` and `due_at` for enhanced presentation — calendar +views, countdowns, reminders. This is optional; recognizing a `type` never +changes what a Platform is required to render. -Businesses MUST: +Platforms **SHOULD**: -* Provide at least one checkout-wide payment term when the extension is active. -* Ensure aggregate schedule totals cover the checkout total exactly once. -* Validate that all instrument assignments select the same term and completely - fund its schedules. -* Include that complete accepted term on the resulting Order. -* Honor payment instrument constraints declared by payment handlers. +* Present terms so the Buyer can compare them before selecting, unless only one + term is available or the Checkout is being handed off. -Businesses SHOULD: +## Business responsibilities -* Include clear names and descriptions for buyer-facing terms. -* Include at least one immediate pay-now term for simple checkouts when - supported. -* Use stable schedule IDs so platforms can assign instruments precisely. +Businesses **MUST**: + +* Offer at least one payment term when the extension is active, and report the + selected term in `selected_term_id` on every response. +* Ensure every term's schedules state, in `description` alone, when each payment + is due. +* Ensure the selected term's schedule totals sum to the checkout total exactly + once. +* Return the recomputed Checkout after a selection, including any change to + totals, policies, messages, or eligible payment handlers. +* Place any content that must reach the Buyer in the disclosure `content`, not + only in the paired policy `description`. +* Carry the accepted term onto the Order as `payment.accepted_term`, along with + any disclosure that governed it. + +Businesses **SHOULD**: + +* Provide `due_at` whenever the due date is determinable at checkout. +* Keep schedule IDs stable across Checkout responses unless the schedule itself + changes. diff --git a/source/schemas/common/types/warning_code.json b/source/schemas/common/types/warning_code.json index 4cf544038..59071de89 100644 --- a/source/schemas/common/types/warning_code.json +++ b/source/schemas/common/types/warning_code.json @@ -8,6 +8,7 @@ "final_sale", "prop65", "fulfillment_changed", + "payment_term_changed", "age_restricted" ] } diff --git a/source/schemas/shopping/payment_terms.json b/source/schemas/shopping/payment_terms.json index 3f8bf2c18..eb9f3f1b4 100644 --- a/source/schemas/shopping/payment_terms.json +++ b/source/schemas/shopping/payment_terms.json @@ -2,9 +2,8 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/shopping/payment_terms.json", "name": "dev.ucp.shopping.payment_terms", - "version": "2026-01-23", "title": "Payment Terms Extension", - "description": "Extends Checkout with flexible payment terms support, enabling deferred payments, installments, and per-line-item payment schedules.", + "description": "Extends Checkout with selectable payment terms, letting a Business offer alternative schedules for when payment for the checkout is due, and projects the accepted term onto the resulting Order.", "requires": { "protocol": { "min": "2026-04-08" }, "capabilities": { @@ -13,83 +12,54 @@ } }, "$defs": { - "available_payment_term": { - "$ref": "types/available_payment_term.json" - }, "payment_term": { "$ref": "types/payment_term.json" }, - "payment_schedule": { - "$ref": "types/payment_schedule.json" - }, - "schedule_ref": { - "$ref": "types/entity_ref.json" - }, - "available_payment_instrument": { - "title": "Available Payment Instrument (Payment Terms)", - "description": "Extends available_payment_instrument with term reference constraints.", - "allOf": [ - { "$ref": "types/available_payment_instrument.json" }, - { - "type": "object", - "properties": { - "constraints": { - "type": "object", - "properties": { - "term_refs": { - "type": "array", - "items": { "$ref": "#/$defs/schedule_ref" }, - "description": "Payment terms and schedules this instrument supports. References can target a payment term by `id`, a payment schedule by `schedule_id`, a schedule class by `schedule_type`, or a combination. If omitted, the instrument supports all terms and schedules unless constrained by the payment handler." - } - } - } - } - } - ] - }, - "payment_instrument": { - "title": "Payment Instrument (Payment Terms)", - "description": "Payment instrument extended with payment term and payment schedule assignment.", - "allOf": [ - { "$ref": "types/payment_instrument.json" }, - { - "type": "object", - "properties": { - "term_refs": { - "type": "array", - "items": { "$ref": "#/$defs/schedule_ref" }, - "description": "Payment terms or schedules this instrument pays for. References can target a payment term by `id`, a payment schedule by `schedule_id`, a schedule class by `schedule_type`, or a combination. If omitted, the instrument can be applied by the business according to the selected terms and handler constraints." - } - } - } - ] - }, "payment": { "title": "Payment with Terms", - "description": "Payment object extended with payment terms.", + "description": "Payment object extended with selectable payment terms.", "allOf": [ { "$ref": "payment.json" }, { "type": "object", + "required": ["terms"], "properties": { - "available_terms": { + "terms": { "type": "array", - "items": { "$ref": "#/$defs/available_payment_term" }, - "description": "Available payment terms the buyer can choose from. Each term defines schedules for when payments are due.", + "items": { "$ref": "#/$defs/payment_term" }, + "minItems": 1, + "description": "Payment terms the Buyer can choose from. An unselected term's amounts are indicative; the selected term's schedule totals sum to the checkout total.", "ucp_request": "omit" }, - "instruments": { - "type": "array", - "items": { "$ref": "#/$defs/payment_instrument" }, - "description": "Payment instruments, optionally assigned to selected payment terms or schedules." + "selected_term_id": { + "type": "string", + "description": "ID of the selected payment term. MUST match one `terms[].id` from the latest Checkout response. Always present in responses: the Checkout total is the selected term's total, so a response without a selection would show an amount that matches no stated terms. Where the Buyer has made no choice, the Business selects a default. Omitted on create requests because term IDs are checkout-scoped and no terms exist yet, and on complete requests because the term is already settled by then. Selecting a term is an Update Checkout mutation: the Business response is authoritative for all derived state.", + "ucp_request": { + "create": "omit", + "update": "optional", + "complete": "omit" + }, + "ucp_response": "required" } } } ] }, + "order_payment": { + "title": "Order Payment with Accepted Term", + "description": "Order payment details carrying the term the Buyer accepted at checkout.", + "type": "object", + "required": ["accepted_term"], + "properties": { + "accepted_term": { + "$ref": "#/$defs/payment_term", + "description": "The payment term the Buyer accepted at checkout. Businesses MUST carry it forward so the Order states the amounts owed and when, and MUST ensure its schedule totals sum to the Order total. The available terms are checkout state and are not projected." + } + } + }, "dev.ucp.shopping.checkout": { "title": "Checkout with Payment Terms", - "description": "Checkout extended with payment terms and payment schedules.", + "description": "Checkout extended with selectable payment terms.", "allOf": [ { "$ref": "checkout.json" }, { @@ -109,22 +79,17 @@ ] }, "dev.ucp.shopping.order": { - "title": "Order with Payment Terms", - "description": "Order extended with the payment terms accepted during checkout.", + "title": "Order with Accepted Payment Term", + "description": "Order extended with the payment term accepted at checkout.", "allOf": [ { "$ref": "order.json" }, { "type": "object", "properties": { "payment": { - "type": "object", - "properties": { - "terms": { - "type": "array", - "items": { "$ref": "#/$defs/available_payment_term" }, - "description": "Complete payment terms accepted for the originating checkout." - } - } + "$ref": "#/$defs/order_payment", + "description": "Payment details for the Order, including the accepted payment term.", + "ucp_response": "required" } } } diff --git a/source/schemas/shopping/types/available_payment_term.json b/source/schemas/shopping/types/available_payment_term.json deleted file mode 100644 index 5194d5fbb..000000000 --- a/source/schemas/shopping/types/available_payment_term.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/shopping/types/available_payment_term.json", - "title": "Available Payment Term", - "description": "A payment term offered by the business, defining how line items can be paid for over time.", - "type": "object", - "required": ["id", "name", "schedules"], - "properties": { - "id": { - "type": "string", - "description": "Unique identifier for this payment term." - }, - "name": { - "type": "string", - "description": "Human-readable name for this payment term, such as `Pay Now`, `Net-30`, `Pay in 4`, or `Deposit + Balance at Check-in`." - }, - "description": { - "type": "string", - "description": "Optional buyer-facing explanation of the payment term." - }, - "schedules": { - "type": "array", - "items": { - "$ref": "payment_schedule.json" - }, - "minItems": 1, - "description": "Payment schedules within this term defining when payments are due, if used." - } - } -} diff --git a/source/schemas/shopping/types/entity_ref.json b/source/schemas/shopping/types/entity_ref.json deleted file mode 100644 index 58cc69f17..000000000 --- a/source/schemas/shopping/types/entity_ref.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/shopping/types/entity_ref.json", - "title": "Entity Reference", - "description": "Flexible reference by ID, schedule ID, schedule type, or any combination thereof. Enables semantic grouping and specific targeting.", - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Entity ID to reference. Context-dependent: line item ID for line_item_refs, payment term ID for term_refs." - }, - "schedule_id": { - "type": "string", - "description": "Match by entity schedule ID." - }, - "schedule_type": { - "$ref": "schedule_type.json", - "description": "Match by schedule type." - } - }, - "examples": [ - { "id": "li_123" }, - { "id": "pt_pay_in_4" }, - { "schedule_type": "immediate" }, - { "schedule_type": "deferred" }, - { "id": "pt_pay_in_4", "schedule_type": "immediate" }, - { "schedule_id": "deposit-uuid" } - ] -} diff --git a/source/schemas/shopping/types/payment_schedule.json b/source/schemas/shopping/types/payment_schedule.json index 46cdcaac8..f00b52ce7 100644 --- a/source/schemas/shopping/types/payment_schedule.json +++ b/source/schemas/shopping/types/payment_schedule.json @@ -2,25 +2,48 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/shopping/types/payment_schedule.json", "title": "Payment Schedule", - "description": "A named payment obligation within a payment term. Timing is disclosed in buyer-facing text while totals provide a structured allocation amount.", + "description": "A single payment that settles part or all of the checkout under a payment term. Timing is stated in buyer-facing text; `type` and `due_at` are supplementary machine-readable signals derived from it.", "type": "object", - "required": ["id", "type", "description", "totals"], + "required": [ + "id", + "type", + "description", + "totals" + ], + "additionalProperties": true, "properties": { "id": { "type": "string", - "description": "Unique identifier for this payment schedule." + "description": "Identifier for this payment schedule, unique within its payment term. Businesses SHOULD keep it stable across responses while the schedule remains the same payment." }, "type": { - "$ref": "schedule_type.json", - "description": "Broad payment timing class. Well-known values: `immediate` and `deferred`." + "type": "string", + "description": "Timing class, drawn from an open vocabulary. `immediate` is the only value with defined meaning: the payment is due when the checkout is completed. Any other value means the payment is not due at completion, and `description` states when it is due. Whether a due payment is authorized, captured, or settled at that moment is payment-handler behavior and outside this extension. Businesses MAY use additional values (e.g. `deferred`, `on_shipment`); Platforms MUST treat unrecognized values as not due at completion." }, "description": { + "$ref": "../../common/types/description.json", + "description": "Complete buyer-facing statement of when and how this payment is due. Businesses MUST make this field sufficient on its own: a Platform that recognizes no `type` value and reads no other field MUST be able to present this schedule correctly. Platforms MAY use `type` and `due_at` for enhanced presentation, but MUST NOT present derived timing that contradicts this field." + }, + "due_at": { "type": "string", - "description": "Complete buyer-facing explanation of when and how the payment is due." + "format": "date-time", + "description": "Absolute RFC 3339 date-time when this payment is due, when the Business can determine one at checkout. Supplementary to `description`, never a replacement for it. Omitted when the due date depends on a future event (e.g. 'due on delivery'); the timing is then stated in `description` alone." }, "totals": { - "$ref": "totals.json", - "description": "Aggregate pricing breakdown for every payment represented by this schedule. The entry with `type: total` is the complete amount allocated to the schedule in the checkout or order currency." + "type": "array", + "items": { + "allOf": [ + { "$ref": "total.json" }, + { + "if": { "properties": { "type": { "const": "total" } }, "required": ["type"] }, + "then": { "properties": { "amount": { "minimum": 0 } } } + } + ] + }, + "contains": { "properties": { "type": { "const": "total" } }, "required": ["type"] }, + "minContains": 1, + "maxContains": 1, + "description": "What this one payment costs. Businesses MUST include exactly one `total` entry with a non-negative amount: the final amount charged when this payment is taken, inclusive of tax and every other charge, in the checkout currency's minor units (ISO 4217). Unlike checkout totals, no `subtotal` is required, because the purchase is priced at the checkout rather than once per payment." } } } diff --git a/source/schemas/shopping/types/payment_term.json b/source/schemas/shopping/types/payment_term.json index c8bf8ffb4..372883ca1 100644 --- a/source/schemas/shopping/types/payment_term.json +++ b/source/schemas/shopping/types/payment_term.json @@ -2,13 +2,34 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/shopping/types/payment_term.json", "title": "Payment Term", - "description": "A buyer's selection of a payment term for specific line items.", + "description": "A way of paying for the checkout: one or more payment schedules that together cover its total.", "type": "object", - "required": ["id"], + "required": [ + "id", + "title", + "schedules" + ], + "additionalProperties": true, "properties": { "id": { "type": "string", - "description": "ID of the selected payment term from available_terms." + "description": "Unique identifier for this payment term within the checkout. Referenced by `payment.selected_term_id`." + }, + "title": { + "type": "string", + "description": "Short label that distinguishes this term from its siblings (e.g. 'Pay now', 'Pay in 4', 'Deposit + balance at check-in')." + }, + "description": { + "$ref": "../../common/types/description.json", + "description": "Supplementary context for the title (e.g. 'Save 5% by paying today'). Directly renderable; MUST NOT repeat the title." + }, + "schedules": { + "type": "array", + "items": { + "$ref": "payment_schedule.json" + }, + "minItems": 1, + "description": "Payment schedules that settle this checkout under this term, in the order they come due." } } } diff --git a/source/schemas/shopping/types/schedule_type.json b/source/schemas/shopping/types/schedule_type.json deleted file mode 100644 index b296f387c..000000000 --- a/source/schemas/shopping/types/schedule_type.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/shopping/types/schedule_type.json", - "title": "Schedule Type", - "description": "Classifies timing for entities such as orders, payments, and fulfillment. 'immediate' refers to immediate capture/fulfillment, 'deferred' refers to future scheduled events.", - "type": "string", - "examples": ["immediate", "deferred"] -}