Skip to content

feat: add com.mercadopago.shopping.digital_delivery extension - #719

Open
gforgab wants to merge 2 commits into
Universal-Commerce-Protocol:mainfrom
gforgab:gforgab/digital-delivery-extension
Open

feat: add com.mercadopago.shopping.digital_delivery extension#719
gforgab wants to merge 2 commits into
Universal-Commerce-Protocol:mainfrom
gforgab:gforgab/digital-delivery-extension

Conversation

@gforgab

@gforgab gforgab commented Aug 12, 2026

Copy link
Copy Markdown

What

Adds com.mercadopago.shopping.digital_delivery, a vendor-namespaced Order
extension
for digital / intangible goods delivery — gift-card codes,
vouchers, top-up PINs, license keys, and access entitlements. Prototypes the
shape discussed in #648.

Why this shape

  • Attaches to the durable Order. A delivered artifact outlives the checkout
    session (a code can be redeemed weeks later), so the record lives on the Order,
    not the checkout.
  • kind discriminatorredeemable_artifact (bearer value drawn down at an
    issuer; "how much is left?") vs entitlement (a granted, revocable right;
    "still valid, until when?").
  • Pointer, not balance. The Order stores only a durable reference + a
    lookup to query the issuer. No mutable state (balance, validity, redemption
    status) is cached on the Order — there is no stale copy to trust. Partial
    redemption lives at the issuer as an append-only ledger, obtained via lookup.
  • Delivery ≠ settlement ≠ redemption. delivered is not terminal; a
    settlement rail must not release funds on delivery.
  • Inert display data (code / QR image / hosted instructions) under the same
    render/trust rules as feat: add com.mercadopago.payment.render_artifact extension #635.

Scope

Vendor extension, no core change. Reconciling the digital method_type
contradiction (expectation.json / fulfillment.md) and adding a non-postal
expectation.destination are core and tracked as separate Enhancement
Proposals.

Changes

  • source/schemas/shopping/digital_delivery.json — extension schema
  • docs/specification/digital-delivery.md — spec (discovery, schema, state/trust
    contract, lifecycle, scope)
  • mkdocs.yml — nav under Order Capability + llmstxt entry
  • .cspell/custom-words.txt — domain terms

Related

@damaz91 damaz91 added status:needs-triage Signal that the PR is ready for human triage status:under-review area:payments and removed status:needs-triage Signal that the PR is ready for human triage labels Aug 12, 2026
@westonale-facet

Copy link
Copy Markdown

Read it. The four points came through intact, and the two you flagged are the two
worth arguing about, so let me take them in turn and then raise one thing the
schema does not yet cover.

Pointer-only: right call, and the failure mode is real.

The State and Trust Contract section makes the correct argument. Worth adding the
operational version: a cached mirror of issuer state does not fail loudly. It
drifts, silently, and you find out at dispute time, which is the single worst
moment to discover that your copy and the issuer's copy disagree. Same caveat as
before, that this comes from escrow and redemption on physical fulfilment rather
than bearer artifacts, but the mechanism transfers exactly: whoever holds the
stale copy becomes the de facto arbiter, and they are arbitrating with the wrong
data. "No cached state on the Order, not even as an advisory copy" is stronger
than most specs manage, and the "not even advisory" clause is doing real work.
Keep it.

Two gaps in the schema undercut it as written.

  1. lookup is optional but the MUSTs assume it. The artifact requires id,
    kind, reference and delivered_at. lookup is not required. The spec says
    current state is obtained only by querying the issuer via lookup.url, and that
    a Platform MUST NOT release funds, grant access, or deny a redemption without a
    fresh issuer answer. If lookup is absent those two requirements are jointly
    unsatisfiable: there is no way to obtain a fresh answer, so a conforming Platform
    can never act on that artifact at all, yet it is schema-valid. Either make
    lookup required, or state what an artifact without one means. My suggestion for
    the second: it is display-only and cannot back any funds or access decision, said
    in the spec rather than left for integrators to infer.

  2. The lookup answer has no shape. lookup carries url and nothing else. The
    extension is careful to split kind into two different standing questions, how
    much is left versus is this still valid and until when, and then types neither
    answer, so two issuers will return incompatible payloads for the same question.
    UCP already has the house pattern: catalog_lookup.json defines lookup_request
    and lookup_response pairs in the schema. A minimal normative response would be
    a status, a kind-conditional field (remaining value plus currency for
    redeemable_artifact, validity window for entitlement), and an as_of
    timestamp. That last one matters more than it looks: without a freshness field a
    Platform cannot distinguish a live issuer answer from a cached CDN response,
    which quietly reintroduces the exact staleness the pointer-only rule exists to
    prevent. There is no as_of convention in the repo today, so treat that as a
    proposal rather than a consistency fix.

  3. Say what happens when the issuer is unreachable. The MUST NOT already implies
    fail closed: no fresh answer, no release. That is the right default, but leaving
    it implicit invites integrators to invent a fallback, and the fallback they will
    reach for is a cached copy, which is the one thing this extension forbids. One
    sentence naming it costs nothing.

One security note on reference. Describing it as opaque and not a credential to
spend is right, but if lookup.url is a plain GET keyed by reference with no
stated auth requirement, then reference becomes a bearer read-credential for
artifact state. Opaque is not unguessable, and unguessable is not authorized.
Worth a line on whether the issuer MUST authenticate the caller, or an explicit
statement that disclosing state to any holder of the reference is acceptable.

Delivered versus settlement: the wording is right, and it stops one step short.

The sentence saying a settlement rail must not wire delivered to release funds is
the one that matters, and I would not soften it. The gap is that it tells a rail
what not to do without saying what the release signal is instead. For physical
goods the industry has a default and everyone reaches for it. For a redeemable
artifact, delivery is early and full redemption may never happen at all, so a
rail integrator reading this paragraph is left with a prohibition and no anchor.
Two honest options: name the release trigger explicitly out of scope, or say that
it is agreed by the rail out of band (a redemption event, a dispute window
expiring, an issuer attestation) and that this extension's contribution is the
lookup that makes such a trigger checkable. Either is fine. Silence is worse than
both, because the shortcut people take in the absence of guidance is exactly the
one the paragraph just banned.

Smaller wording point in the same section. The doc reads that delivered means
the buyer received the artifact, which parses as a normative Order state named
delivered. The schema carries delivered_at, a timestamp, and no state
machine. Since order states are core rather than extension territory, phrasing it
as the presence of delivered_at avoids appearing to define a core state from
inside a vendor extension.

Last one, on partial redemption. Requiring that the issuer model redemption as an
append-only ledger puts a requirement on a party the extension cannot bind, and
issuer internals are not observable anyway. The enforceable version of the same
requirement lives at the interface: the lookup response MUST be able to express
partial state, not merely redeemed or not redeemed. That gets you the dispute
history you are after, and unlike the ledger requirement it is checkable by a
conformance test.

Overall: the invariant is the right one, the prose defends it better than most,
and the remaining work is making the schema enforce what the prose promises.

gforgab added a commit to gforgab/ucp that referenced this pull request Aug 17, 2026
Addresses @westonale-facet's review on Universal-Commerce-Protocol#719 — make the schema enforce what the
prose promises:

- Make `lookup` required (a schema-valid artifact could otherwise carry no way
  to obtain the fresh issuer answer the MUSTs demand).
- Add a typed `lookup_response` (status incl. partially_redeemed, required
  as_of freshness, kind-conditional remaining+currency / valid_until), following
  the catalog_lookup response house pattern; couple currency to remaining and
  constrain it with the ISO-4217 pattern.
- State fail-closed when the issuer is unreachable (no cached fallback).
- Require the issuer to authenticate the lookup caller; `reference` is not an
  authorization credential.
- Express partial redemption at the interface (lookup_response) instead of
  requiring an append-only ledger on the unbindable issuer.
- Phrase delivery as the presence of `delivered_at`, not a core Order state.
- Name the settlement release trigger as out-of-band, with `lookup` making it
  checkable.
@gforgab

gforgab commented Aug 17, 2026

Copy link
Copy Markdown
Author

Thanks @westonale-facet — this is exactly the "make the schema enforce what the prose promises" pass the PR needed. Applied all of it:

  1. lookup is now required. Closes the gap where a schema-valid artifact could carry no way to obtain a fresh answer while the MUSTs demand one.
  2. Typed the lookup answer. Added a lookup_response shape, following the catalog_lookup.json request/response house pattern: status (with partially_redeemed first-class), a required as_of freshness timestamp, and the kind-conditional fields (remaining + currency for redeemable_artifact, valid_until for entitlement). Agreed as_of is the load-bearing one — without it a live answer is indistinguishable from a cached CDN response, reopening the exact staleness the pointer-only rule closes. Flagging that there's no repo-wide as_of convention yet, so treating this as a proposal.
  3. Fail-closed made explicit. If no fresh answer can be obtained, the Platform MUST NOT act and MUST NOT fall back to a cached copy — the absence of an answer is not a state.
  4. Auth on reference. "Opaque is not unguessable, and unguessable is not authorized" is the right frame. The lookup contract now requires the issuer to authenticate the caller; reference is explicitly not an authorization credential, so an unauthenticated GET keyed by it must not disclose state.
  5. Partial redemption moved to the interface. Agreed — requiring the issuer to model an append-only ledger binds a party the extension can't bind and isn't observable. It now lives at the interface: lookup_response can report partial state (status: partially_redeemed with a remaining balance) instead of collapsing to redeemed/not — expressible and checkable at the interface rather than an unobservable requirement on issuer internals. (remaining is coupled to currency; I left the partially_redeemedremaining pairing as prose rather than a schema if/then for now — open to tightening it if you'd prefer it enforced.)
  6. delivered reworded. Now phrased as the presence of delivered_at (a timestamp), explicitly not a core Order state defined from inside a vendor extension.
  7. Settlement trigger named. Took your option (b): the release trigger is agreed out of band by the rail (a redemption event, a dispute window expiring, an issuer attestation), and the extension's contribution is the lookup that makes it checkable.

The same hardening will carry to #635 once the extensions/ directory layout is settled. Thanks again — this materially tightened it.

gforgab added a commit to gforgab/ucp that referenced this pull request Aug 17, 2026
Addresses @westonale-facet's review on Universal-Commerce-Protocol#719 — make the schema enforce what the
prose promises:

- Make `lookup` required (a schema-valid artifact could otherwise carry no way
  to obtain the fresh issuer answer the MUSTs demand).
- Add a typed `lookup_response` (status incl. partially_redeemed, required
  as_of freshness, kind-conditional remaining+currency / valid_until), following
  the catalog_lookup response house pattern; couple currency to remaining and
  constrain it with the ISO-4217 pattern.
- State fail-closed when the issuer is unreachable (no cached fallback).
- Require the issuer to authenticate the lookup caller; `reference` is not an
  authorization credential.
- Express partial redemption at the interface (lookup_response) instead of
  requiring an append-only ledger on the unbindable issuer.
- Phrase delivery as the presence of `delivered_at`, not a core Order state.
- Name the settlement release trigger as out-of-band, with `lookup` making it
  checkable.
@gforgab
gforgab force-pushed the gforgab/digital-delivery-extension branch 2 times, most recently from a50183d to c847b95 Compare August 17, 2026 20:41
gforgab added a commit to gforgab/ucp that referenced this pull request Aug 17, 2026
Addresses @westonale-facet's review on Universal-Commerce-Protocol#719 — make the schema enforce what the
prose promises:

- Make `lookup` required (a schema-valid artifact could otherwise carry no way
  to obtain the fresh issuer answer the MUSTs demand).
- Add a typed `lookup_response` (status incl. partially_redeemed, required
  as_of freshness, kind-conditional remaining+currency / valid_until), following
  the catalog_lookup response house pattern; couple currency to remaining and
  constrain it with the ISO-4217 pattern.
- State fail-closed when the issuer is unreachable (no cached fallback).
- Require the issuer to authenticate the lookup caller; `reference` is not an
  authorization credential.
- Express partial redemption at the interface (lookup_response) instead of
  requiring an append-only ledger on the unbindable issuer.
- Phrase delivery as the presence of `delivered_at`, not a core Order state.
- Name the settlement release trigger as out-of-band, with `lookup` making it
  checkable.
@westonale-facet

Copy link
Copy Markdown

Thanks for working through all of it, and for typing the lookup answer rather than only making lookup required. Requiring the field closes the schema-valid-but-unactionable case; typing the response is what stops two implementations from disagreeing about what a fresh answer looks like, which was the failure the pointer-only rule existed to prevent in the first place. Following catalog_lookup.json for the request and response pair is the right precedent to borrow.

Nothing further from me on this one. Happy to look again if the shape changes after review.

Vendor-namespaced Order extension for digital / intangible goods delivery
(gift-card codes, vouchers, top-up PINs, license keys, access entitlements),
prototyping the shape discussed in Universal-Commerce-Protocol#648.

- Attaches to the durable Order (the artifact outlives the checkout session).
- kind discriminator: redeemable_artifact (bearer, drawn down at the issuer)
  vs entitlement (granted, revocable right).
- Stores only a durable reference + a lookup to query the issuer; no mutable
  state (balance/validity/redemption) is cached on the Order, so there is no
  stale copy to trust. Partial redemption lives at the issuer as an append-only
  ledger, obtained via lookup.
- Delivery is not terminal and not settlement (a rail must not release funds on
  delivered).

Core changes (reconciling the digital method_type contradiction and a
non-postal destination) are out of scope here and tracked as separate EPs.
Addresses @westonale-facet's review on Universal-Commerce-Protocol#719 — make the schema enforce what the
prose promises:

- Make `lookup` required (a schema-valid artifact could otherwise carry no way
  to obtain the fresh issuer answer the MUSTs demand).
- Add a typed `lookup_response` (status incl. partially_redeemed, required
  as_of freshness, kind-conditional remaining+currency / valid_until), following
  the catalog_lookup response house pattern; couple currency to remaining and
  constrain it with the ISO-4217 pattern.
- State fail-closed when the issuer is unreachable (no cached fallback).
- Require the issuer to authenticate the lookup caller; `reference` is not an
  authorization credential.
- Express partial redemption at the interface (lookup_response) instead of
  requiring an append-only ledger on the unbindable issuer.
- Phrase delivery as the presence of `delivered_at`, not a core Order state.
- Name the settlement release trigger as out-of-band, with `lookup` making it
  checkable.
@gforgab
gforgab force-pushed the gforgab/digital-delivery-extension branch from c847b95 to 2146b11 Compare August 24, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants