-
Notifications
You must be signed in to change notification settings - Fork 452
feat: add com.mercadopago.payment.render_artifact extension #635
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| <!-- | ||
| 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 Render Artifact Extension | ||
|
|
||
| * **Capability Name:** `com.mercadopago.shopping.render_artifact` | ||
| * **Schema:** `https://ucp.dev/schemas/shopping/payment_render_artifact.json` | ||
|
|
||
| ## Overview | ||
|
|
||
| The Payment Render Artifact 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 vendor-namespaced capability stacked on the generic | ||
| [Actions](overview.md#actions) primitive. The **capability** is | ||
| `com.mercadopago.shopping.render_artifact`; it declares the **Action type** | ||
| `com.mercadopago.payment.render_artifact` and the shape of its `config`. Keeping | ||
| the capability and the Action type distinct lets the capability declare further | ||
| Action types later without overloading a single name. | ||
|
|
||
| **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.shopping.render_artifact": [ | ||
| { | ||
| "version": "{{ ucp_version }}", | ||
| "extends": ["dev.ucp.shopping.checkout"], | ||
| "spec": "https://ucp.dev/{{ ucp_version }}/specification/payment-render-artifact", | ||
| "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/payment_render_artifact.json" | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Schema | ||
|
|
||
| When this extension is active, the checkout `actions` map MAY carry outstanding | ||
| `com.mercadopago.payment.render_artifact` instances. Each instance is a common Action | ||
| instance (`id`, optional `config`) whose `config` is the display artifact below. | ||
|
|
||
| ### Payment Render Artifact Config | ||
|
|
||
| {{ extension_schema_fields('payment_render_artifact.json#/$defs/config', 'payment_render_artifact') }} | ||
|
|
||
| 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` is required and bounds the artifact.** It bounds Platform | ||
| polling (see [Polling](#polling)) and MUST NOT exceed the checkout-level | ||
| `expires_at`; where both are present, this Action-level value takes precedence | ||
| for the Action. 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.render_artifact": [ | ||
| { "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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The action relies on this polling mechanism to know the outcome, but it doesn't define a clear polling contract. According to checkout.md, ideally, the action should define one. Otherwise it will be subjected to different platform policies.
With no contract in the action definition, it falls through to Platform policy, and conforming implementations can diverge wildly. For reference, the payment-authentication action defines a 30-second polling interval with backoff, but that is not suitable here. I think this action should state a normative minimum interval and maximum duration (as a MUST/SHOULD prose).
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 27db19a — added a normative |
||
| 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.render_artifact'][0]`. The Platform | ||
| surfaces the recovery path (for example, refreshing the artifact) rather than | ||
| re-driving Complete. | ||
|
|
||
| ## Polling | ||
|
|
||
| The outcome of a render-artifact Action is observed by the Platform polling Get | ||
| Checkout; the Platform never re-drives Complete. Absent a contract here, backoff | ||
| falls through to Platform policy and conforming implementations diverge, so this | ||
| Action defines it: | ||
|
|
||
| * The Platform MUST NOT poll Get Checkout more than once every **2 seconds**. | ||
| * The Platform SHOULD apply exponential backoff between attempts. | ||
| * The Platform MUST stop polling at the Action-level `expires_at` and re-fetch | ||
| checkout state once, rather than continuing to poll a stale artifact. | ||
|
|
||
| ## Scope: In-Session vs. Out-of-Session Settlement | ||
|
|
||
| This extension covers artifacts that resolve **within the checkout session's | ||
| lifetime**: the Business holds the checkout in `complete_in_progress`, the buyer | ||
| pays out of band, and the Platform observes the outcome by polling Get Checkout | ||
| before the session's viability window closes. | ||
|
|
||
| The dividing line is **not the payment method** but **whether settlement | ||
| completes while the checkout session is still alive**: | ||
|
|
||
| * **In-session (this extension).** The artifact's `expires_at` fits within the | ||
| session's viability window, so the outcome is observable by polling — for | ||
| example, an immediate Pix QR paid within minutes. | ||
| * **Out-of-session (out of scope here).** Settlement may land after the checkout | ||
| session is gone — for example, **Pix with a long expiry or due date (Pix com | ||
| vencimento)**, boleto, or cash vouchers. A Platform cannot hold | ||
| `complete_in_progress` open for hours or days, so these MUST NOT be surfaced | ||
| as a checkout Action resolved by polling. They belong to an order-level flow | ||
| where the same display artifact lives on the `order` and the outcome arrives | ||
| via an order lifecycle webhook (addressed separately). | ||
|
|
||
| Note that the discriminator is temporal, not the method label: a long-expiry | ||
| Pix charge behaves like boleto for this purpose. Accordingly, a Business using | ||
| this extension MUST issue an artifact whose `expires_at` is bounded by the | ||
| checkout session's viability; if the intended expiry exceeds it, use the | ||
| order-level async-payment flow instead. | ||
| 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_render_artifact.json", | ||
| "name": "com.mercadopago.shopping.render_artifact", | ||
| "title": "Payment Render Artifact Extension", | ||
| "description": "Extends Checkout with a render-artifact 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.render_artifact' on top of the common Actions primitive.", | ||
| "$defs": { | ||
| "config": { | ||
| "type": "object", | ||
| "title": "Payment Render Artifact 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", "expires_at"], | ||
| "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": { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two points related to
Therefore, I suggest marking the action-level
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 27db19a — |
||
| "type": "string", | ||
| "format": "date-time", | ||
| "description": "RFC 3339 timestamp after which this artifact is no longer valid. Required; it bounds Platform polling and MUST NOT exceed the checkout-level 'expires_at'. On expiry the Platform stops rendering it and re-fetches checkout state." | ||
| } | ||
| }, | ||
| "anyOf": [ | ||
| { "required": ["code"] }, | ||
| { "required": ["image"] }, | ||
| { "required": ["instructions_url"] } | ||
| ] | ||
| }, | ||
| "instance": { | ||
| "title": "Payment Render Artifact Action Instance", | ||
| "description": "One outstanding payment render-artifact Action: the common Action instance with a render-artifact 'config'.", | ||
| "allOf": [ | ||
| { "$ref": "../common/types/actions.json#/$defs/instance" }, | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "config": { "$ref": "#/$defs/config" } | ||
| } | ||
| } | ||
| ] | ||
| }, | ||
| "dev.ucp.shopping.checkout": { | ||
| "title": "Checkout with Payment Render Artifact", | ||
| "description": "Checkout whose 'actions' map MAY carry outstanding 'com.mercadopago.payment.render_artifact' instances while payment is pending out of band.", | ||
| "allOf": [ | ||
| { "$ref": "checkout.json" }, | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "actions": { | ||
| "type": "object", | ||
| "properties": { | ||
| "com.mercadopago.payment.render_artifact": { | ||
| "type": "array", | ||
| "minItems": 1, | ||
| "description": "Outstanding payment render-artifact Actions. Typically a single instance for the buyer's chosen method.", | ||
| "items": { "$ref": "#/$defs/instance" } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this. The render artifact design is clean and the render/trust contract is well specified.
One structural concern worth discussing: source/schemas/shopping/payment_render_artifact.json currently sits in the same flat directory as first-party core schemas like checkout.json, order.json, and payment.json. Since this is explicitly a vendor-namespaced capability (com.mercadopago.*), it would help consumers immediately distinguish stable core contracts from optional vendor extensions if these were kept in a separate directory.
Something like:
source/schemas/shopping/extensions/com.mercadopago/payment_render_artifact.json docs/specification/extensions/com.mercadopago/payment-render-artifact.mdThis also matters for the pattern going forward to accommodate similar use cases for other handlers like PPRO, establishing a clear extensions/ home now avoids multiple vendor handlers landing alongside core schemas.
Worth aligning on the directory structure before the pattern solidifies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strong +1 — worth settling before it solidifies. This is the first external-vendor namespace in the repo (
com.mercadopago.*), so there's noextensions/precedent yet, and I'd rather we agree the layout once than have each handler improvise.Proposed starting point, mirroring your suggestion:
Mechanically this shifts the schema
$id, the relative$refs (order.json→../../order.json,../common/...), the doc macro path, and the mkdocs nav/llmstxt entries — all fine, just wants doing consistently.Since this sets a repo-wide convention for every vendor handler (yours included), @igrigorik / @raginpirate — could you say where these should live? Once it's confirmed I'll migrate both this PR and #719 (
com.mercadopago.shopping.digital_delivery, same flat-dir situation) to it in one go.