Skip to content
Draft
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
7 changes: 7 additions & 0 deletions .cspell/custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,10 @@ punycode
userinfo
examplecorp
zapatillas
Pix
pix
Mercadopago
mercadopago
Pago
boleto
copia
140 changes: 140 additions & 0 deletions docs/specification/payment-display.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<!--
Copyright 2026 UCP Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

# Payment Display Extension

* **Capability Name:** `com.mercadopago.payment.display`
* **Schema:** `https://ucp.dev/schemas/shopping/payment_display.json`

## Overview

The Payment Display extension lets a Business complete an **in-session,
out-of-band payment method** — such as **Pix** in Brazil — without redirecting
the buyer or handing off a `continue_url`. When the buyer chooses such a method,
the Business surfaces an **inert display artifact** (a QR image, a
copy-and-paste code, and/or hosted instructions) as an outstanding Action on the
checkout. The Platform renders it, the buyer pays in their bank app, and the
**same** Complete Checkout operation resolves once payment is confirmed.

This is a concrete, vendor-namespaced Action type stacked on the generic
[Actions](overview.md#actions) primitive. It defines the Action type
`com.mercadopago.payment.display` and the shape of its `config`.

**Key features:**

* Display Pix (and equivalent QR/code methods) natively — no iframe, no redirect
* Works across surfaces: web, native app, and voice (a `code` fallback exists
when no screen can show an image)
* Payment resolves in-session by polling Get Checkout — the Platform never
re-drives Complete
* Strict render/trust contract: display data only, inert fields, single
allowlisted loadable field

**Dependencies:**

* Checkout Capability
* The [Actions](overview.md#actions) primitive on Checkout

## Discovery

Businesses advertise this extension in their profile, extending the Checkout
capability:

<!-- ucp:example schema=profile def=business_schema extract=$.ucp.capabilities target=$.ucp.capabilities -->
```json
{
"ucp": {
"version": "{{ ucp_version }}",
"capabilities": {
"com.mercadopago.payment.display": [
{
"version": "{{ ucp_version }}",
"extends": ["dev.ucp.shopping.checkout"],
"spec": "https://ucp.dev/{{ ucp_version }}/specification/payment-display",
"schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/payment_display.json"
}
]
}
}
}
```

## Schema

When this extension is active, the checkout `actions` map MAY carry outstanding
`com.mercadopago.payment.display` instances. Each instance is a common Action
instance (`id`, optional `config`) whose `config` is the display artifact below.

### Payment Display Config

{{ extension_schema_fields('payment_display.json#/$defs/config', 'payment_display') }}

At least one of `code`, `image`, or `instructions_url` MUST be present so the
Platform always has something renderable.

## Render and Trust Contract

The artifact carries **public display data only** and is designed to be safe to
render on any surface. Implementations MUST honor the following:

* **No secrets transit the artifact.** It never contains a PAN, CVV, OTP,
token, credential, or any other sensitive value — only data already meant to
be shown to the buyer.
* **`image` is inert.** It is rendered as a static image only. The Platform MUST
NOT execute, interpret, or fetch it as anything other than image bytes, and
MUST restrict it to `data:` or `https:` URIs.
* **`code` is display text.** The Platform SHOULD present it with a copy
affordance. It MUST NOT be interpreted as a URL, deep link, or executable
content.
* **`instructions_url` is the only loadable field.** When present, the Platform
MUST constrain it to an `https:` origin allowlist advertised for the handler,
MUST open it as a plain document, and MUST NOT auto-submit forms or forward
buyer data to it.
* **`expires_at` bounds the artifact.** After it passes, the Platform stops
rendering the artifact and re-fetches checkout state rather than continuing to
show a stale QR/code.

## Resolution Flow

```text
1. Platform → Business : Complete Checkout (buyer chose Pix)
2. Business → Platform : status = complete_in_progress
actions {
"com.mercadopago.payment.display": [
{ "id": "...", "config": { "type": "qr_code", ... } }
]
}
3. Platform : renders QR image + copy button for `code`
(works on web / native / voice — no frame)
4. Buyer : pays via bank app (out of band)
5. Platform → Business : polls Get Checkout (MUST NOT re-drive Complete)
6. Mercado Pago : confirms payment → Business resolves
7. Business → Platform : status = completed, order present, Action gone
```

On expiry or failure, the Business returns the checkout with a `recoverable`
error [Message](overview.md#messages) whose `path` points at the exact Action
occurrence, e.g. `$.actions['com.mercadopago.payment.display'][0]`. The Platform
surfaces the recovery path (for example, refreshing the artifact) rather than
re-driving Complete.

## Relationship to Async Payment Methods

This extension covers methods that resolve **within the checkout session**
(seconds), so the display artifact lives on the checkout and the outcome arrives
by polling. Methods that settle **after the session ends** (hours to days) —
such as boleto or cash vouchers — are out of scope here and are addressed
separately at the order level via order lifecycle webhooks.
7 changes: 7 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ nav:
- Discounts Extension: specification/discount.md
- Fulfillment Extension: specification/fulfillment.md
- Loyalty Extension: specification/loyalty.md
- Payment Display Extension: specification/payment-display.md
- Split Payments Extension: specification/split-payments.md
- Cart Capability:
- Overview: specification/cart.md
Expand Down Expand Up @@ -328,6 +329,12 @@ plugins:
memberships, tiers, benefits, and reward earning forecasts while
managing provisional eligibility claims and enforcing
data-minimization privacy rules.
- specification/payment-display.md: >-
Payment Display Extension, defining an in-session, out-of-band
payment Action (e.g., Pix) that surfaces an inert display artifact
(QR image, copy-and-paste code, hosted instructions) for the
Platform to render without redirect or continue_url, resolved by
polling Get Checkout, under a strict render and trust contract.
Cart Capability:
- specification/cart.md: >-
Pre-purchase Cart Capability, detailing item collection, state
Expand Down
87 changes: 87 additions & 0 deletions source/schemas/shopping/payment_display.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ucp.dev/schemas/shopping/payment_display.json",
"name": "com.mercadopago.payment.display",
"title": "Payment Display Extension",
"description": "Extends Checkout with a display Action for in-session, out-of-band payment methods (e.g., Pix). The Business surfaces an inert display artifact (QR image, copy-and-paste code, hosted instructions) that the Platform renders so the buyer can complete payment in a separate app; the same Complete Checkout operation resolves afterward via polling Get Checkout, with no redirect or continue_url handoff. Declares the Action type 'com.mercadopago.payment.display' on top of the common Actions primitive.",
"$defs": {
"config": {
"type": "object",
"title": "Payment Display Config",
"description": "Public, inert display artifact for one outstanding payment method. Contains display data only: no PAN, OTP, credential, or other secret ever transits this artifact. Render fields are non-executable — 'image' is shown as a static image (never as HTML or script), 'code' is display text, and 'instructions_url' is the only loadable field.",
"required": ["type"],
"properties": {
"type": {
"type": "string",
"description": "The display artifact kind, so the Platform knows how to render it. Reverse-domain or short token defined by this extension.",
"examples": ["qr_code"]
},
"code": {
"type": "string",
"minLength": 1,
"description": "Copy-and-paste payment string (Pix 'copia e cola'). Display text only — the Platform SHOULD offer a copy affordance. MUST NOT be interpreted as a URL or executed."
},
"image": {
"type": "string",
"format": "uri",
"description": "QR (or equivalent) image encoding the payment, as a 'data:' or 'https:' URI. The Platform MUST render it as a static image only and MUST NOT execute, parse, or fetch it as anything other than image bytes."
},
"instructions_url": {
"type": "string",
"format": "uri",
"description": "Optional hosted, human-readable payment instructions. The ONLY loadable field in this artifact; the Platform MUST constrain it to an 'https:' origin allowlist advertised for the handler and MUST NOT auto-submit or pass buyer data to it."
},
"reference": {
"type": "string",
"description": "Opaque payment reference (e.g., Pix end-to-end id) for reconciliation. Not buyer-facing; not a credential."
},
"expires_at": {
"type": "string",
"format": "date-time",
"description": "RFC 3339 timestamp after which this artifact is no longer valid. On expiry the Platform stops rendering it and re-fetches checkout state."
}
},
"anyOf": [
{ "required": ["code"] },
{ "required": ["image"] },
{ "required": ["instructions_url"] }
]
},
"instance": {
"title": "Payment Display Action Instance",
"description": "One outstanding payment-display Action: the common Action instance with a payment-display 'config'.",
"allOf": [
{ "$ref": "../common/types/actions.json#/$defs/instance" },
{
"type": "object",
"properties": {
"config": { "$ref": "#/$defs/config" }
}
}
]
},
"dev.ucp.shopping.checkout": {
"title": "Checkout with Payment Display",
"description": "Checkout whose 'actions' map MAY carry outstanding 'com.mercadopago.payment.display' instances while payment is pending out of band.",
"allOf": [
{ "$ref": "checkout.json" },
{
"type": "object",
"properties": {
"actions": {
"type": "object",
"properties": {
"com.mercadopago.payment.display": {
"type": "array",
"minItems": 1,
"description": "Outstanding payment-display Actions. Typically a single instance for the buyer's chosen method.",
"items": { "$ref": "#/$defs/instance" }
}
}
}
}
}
]
}
}
}
Loading