# Spreedly Tokenized Card — UCP payment handler specification

Specification for the `spreedly_tokenized_card_v1` payment handler: how a
business declares that it accepts a Spreedly payment method token, how a
platform obtains one, and how the business processes it. Authored to the UCP
[Payment Handler Guide](https://ucp.dev/2026-04-08/specification/payment-handler-guide/).

## Overview

- Status: draft. Identifiers in this document may change before the first stable
  artifact release.
- Handler name (namespace): `com.spreedly.agentic.specs`
- Handler instance id: `spreedly_tokenized_card_v1`
- Handler version: `2026-09-03` (UCP entity version, `YYYY-MM-DD`)
- UCP protocol version: `2026-04-08`
- Spec URL: `https://specs.agentic.spreedly.com/ucp/handlers/spreedly-card/spec.md`
- Schema URL: `https://specs.agentic.spreedly.com/ucp/handlers/spreedly-card/schema.json`

The contract is the credential. The platform submits an opaque Spreedly payment
method token, minted in the business's Spreedly environment, together with the
token's expiry. The platform never sees the business's gateway, sub-merchant
identifiers, or the raw tokenization response. How the platform obtains the
token is an acquisition route (see Instrument acquisition); the business
advertises the routes it supports, and the platform chooses among them.

## Participants

| Participant | Role |
|---|---|
| Business | Declares the handler and its acquisition routes in its profile; resolves the token and charges through Spreedly. |
| Platform | Discovers the handler, obtains a payment method token by an advertised route, submits it at checkout. |
| Spreedly | The credential provider and processor: vaults the card as a payment method token and settles the charge. |

## Prerequisites

Out-of-band onboarding, not part of the UCP wire exchange:

- The business provisions a Spreedly environment and a gateway. The business's
  public Spreedly environment key is the only Spreedly value that crosses the
  wire, surfaced in the runtime config below. It is a public tokenization key by
  design, not a secret.
- For the Checkout SDK routes, the business registers a certificate with
  Spreedly (`POST /v1/certificates`), enables Enhanced iFrame Security on its
  environment, and exposes an HTTPS endpoint that returns per-session
  AuthDetails. The signature is the SHA-256 signature of `nonce` + `timestamp` +
  `certificate_token`, made with the certificate's private key and Base64
  encoded; a nonce and timestamp are valid for 30 minutes and MUST NOT be
  reused. The private key is server-only; it never reaches the browser or the
  platform. Without Enhanced iFrame Security enabled, the SDK still requires
  the five AuthDetails fields but Spreedly does not verify the signature.
- The platform MUST support at least one route the business advertises.
- The platform authenticates to the business's UCP REST endpoint with a bearer
  token (RFC 6750: `Authorization: Bearer …`, 401 + `WWW-Authenticate` challenge on
  failure). The business issues the token during this same out-of-band
  onboarding. Credentials never appear in the business profile, which advertises
  capability only.

## Handler declaration

The business declares the handler in the `payment_handlers` registry, keyed by
the namespace. UCP defines three declaration variants; this handler uses all
three (schema `$defs` `business_schema`, `platform_schema`, `response_schema`):

| Variant | Where | Config shape |
|---|---|---|
| `business_schema` | Business profile at `/.well-known/ucp` | Acquisition routes the business supports, capability flags. |
| `platform_schema` | Platform profile | Acquisition routes the platform can drive. |
| `response_schema` | Checkout responses | Runtime config: credential contract plus per-route settings (see Instrument acquisition). |

The business declaration advertises the `card` instrument and the acquisition
routes. A business lists the routes it supports; the example below lists every
route this handler defines.

```json
{
  "payment_handlers": {
    "com.spreedly.agentic.specs": [
      {
        "id": "spreedly_tokenized_card_v1",
        "version": "2026-09-03",
        "spec": "https://specs.agentic.spreedly.com/ucp/handlers/spreedly-card/spec.md",
        "schema": "https://specs.agentic.spreedly.com/ucp/handlers/spreedly-card/schema.json",
        "available_instruments": [
          {
            "type": "card",
            "constraints": { "brands": ["visa", "mastercard", "amex", "discover"] }
          }
        ],
        "config": {
          "acquisition_routes": [
            "checkout_sdk_hosted_fields",
            "checkout_sdk_express_checkout",
            "iframe",
            "payment_method_token"
          ],
          "supports_billing_address": true,
          "supports_card_brand_display": true
        }
      }
    ]
  }
}
```

`constraints.brands` is optional. The example declaration advertises `visa`,
`mastercard`, `amex`, and `discover`; the schema's `brand` enum accepts a
superset that also includes `jcb`, `diners`, and `unionpay`.

Spreedly's tokenization response names the card type in its own vocabulary.
The platform maps it to the handler's `brand` when it fills `display.brand`:

| Spreedly `card_type` | Handler `brand` |
|---|---|
| `visa` | `visa` |
| `master` | `mastercard` |
| `american_express` | `amex` |
| `discover` | `discover` |
| `jcb` | `jcb` |
| `diners_club` | `diners` |
| `unionpay` | `unionpay` |

## Instrument acquisition

The platform obtains a Spreedly payment method token by one of the routes the
business advertises, then assembles the instrument below. Every route yields
the same credential; the routes differ only in how the card is collected.

| Route | Mechanism | Route-specific runtime config |
|---|---|---|
| `checkout_sdk_hosted_fields` | The platform renders its own form and mounts the Checkout SDK Hosted Fields for the card number and CVV. Reference route, documented step by step below. | `sdk_auth_url`, `hosted_fields` |
| `checkout_sdk_express_checkout` | The platform mounts the Checkout SDK Express Checkout pre-built form. | `sdk_auth_url` |
| `iframe` | The platform uses the Spreedly iFrame payment form with the business's environment key. | none |
| `payment_method_token` | The platform already holds a Spreedly payment method token minted in the business's environment, obtained earlier by any Spreedly tokenization surface, and submits it directly. No card collection happens inside the checkout. | none |

The checkout response carries the handler's runtime config (the
`response_schema` variant defined in `schema.json`). `environment_key`,
`credential_ttl_seconds`, and `acquisition_routes` are always present.
`sdk_auth_url` is required when either Checkout SDK route is advertised;
`hosted_fields` is required when `checkout_sdk_hosted_fields` is advertised.

```json
{
  "payment": {
    "payment_handlers": {
      "com.spreedly.agentic.specs": [
        {
          "id": "spreedly_tokenized_card_v1",
          "version": "2026-09-03",
          "spec": "https://specs.agentic.spreedly.com/ucp/handlers/spreedly-card/spec.md",
          "schema": "https://specs.agentic.spreedly.com/ucp/handlers/spreedly-card/schema.json",
          "available_instruments": [
            {
              "type": "card",
              "constraints": { "brands": ["visa", "mastercard", "amex", "discover"] }
            }
          ],
          "config": {
            "display_name": "Credit or debit card",
            "environment_key": "EXAMPLE_ENVIRONMENT_KEY_f3n8q",
            "credential_ttl_seconds": 600,
            "acquisition_routes": [
              "checkout_sdk_hosted_fields",
              "checkout_sdk_express_checkout",
              "iframe",
              "payment_method_token"
            ],
            "sdk_auth_url": "https://merchant.example/ucp/sdk-auth",
            "hosted_fields": {
              "number_container_id": "spreedly-number",
              "cvv_container_id": "spreedly-cvv"
            }
          }
        }
      ]
    }
  }
}
```

### Reference route: Checkout SDK Hosted Fields

1. Read the handler's runtime config from the checkout response.
2. Request AuthDetails from `sdk_auth_url`, passing the checkout session id. The
   business returns `environment_key`, `certificate_token`, `nonce`, `timestamp`,
   and `signature`; the signing key stays on the business's server.
3. Initialize the Checkout SDK with the AuthDetails and mount Hosted Fields into
   the DOM containers named by `hosted_fields`.
4. On submit, the SDK tokenizes the card and returns an opaque payment method
   token. The platform sets `credential.expiry` to the issuance time plus
   `credential_ttl_seconds` and assembles the instrument.

### The instrument

The resulting instrument extends the UCP card payment instrument. Its `credential`
is a token credential of type `spreedly_payment_method_token` carrying the opaque
token and its `expiry`. The UCP base token credential leaves `type` open for
handler-specific values; the business also accepts the base type `token`, which
is what generic platform agents send.

```json
{
  "payment": {
    "instruments": [
      {
        "id": "EXAMPLE_INSTRUMENT_ID_a7k3m",
        "handler_id": "spreedly_tokenized_card_v1",
        "type": "card",
        "selected": true,
        "display": {
          "brand": "visa",
          "last_digits": "1111",
          "description": "visa ending in 1111"
        },
        "credential": {
          "type": "spreedly_payment_method_token",
          "token": "EXAMPLE_PAYMENT_METHOD_TOKEN_v8w2k",
          "expiry": "2026-04-15T18:00:00.000Z"
        }
      }
    ]
  }
}
```

### Expiry

`expiry` is the platform's promise: the platform MUST NOT submit the credential
after it, and MUST obtain a fresh token instead. The business MUST reject a
credential whose `expiry` has passed. The business re-prices every charge from
its own catalog, so the platform never sets the amount.

The token is single-use by construction. A payment method token that is not
retained is usable for one transaction; Spreedly marks it used after the first
charge, rejects any further use, and redacts unretained tokens automatically.
The business MUST NOT retain the token (`retain_on_success`) and MUST NOT ask
Spreedly to keep it cached (`continue_caching`), or the token becomes reusable.
Binding a token to a single checkout session is the business's enforcement;
this handler does not require Spreedly to enforce it.

### What the platform does not send

- PAN, CVV, or expiry of the underlying card in plaintext
- Gateway identifier or sub-merchant identifiers
- The raw Spreedly tokenization response
- 3DS or network-token material

## Processing

Processing happens inside the business, off the UCP wire: the business resolves
the opaque credential to a single Spreedly `purchase`
(`POST /v1/gateways/{gateway_token}/purchase.json`), re-pricing the amount
server-side. The projection below uses the purchase call's own parameter names
(`amount` is an integer in minor units, `currency_code` is ISO 4217); it is not
a UCP wire message:

```json
{
  "call": {
    "method": "purchase",
    "args": {
      "payment_method_token": "EXAMPLE_PAYMENT_METHOD_TOKEN_v8w2k",
      "amount": 2500,
      "currency_code": "USD",
      "order_id": "ucp-EXAMPLE_SESSION_ID_8f4dq",
      "description": "Order ucp-EXAMPLE_SESSION_ID_8f4dq",
      "retain_on_success": false
    }
  },
  "result": {
    "outcome": "succeeded",
    "gateway_message": "Approved",
    "transaction_token": "EXAMPLE_TRANSACTION_TOKEN_c4m9x"
  }
}
```

The result discriminates `succeeded`, `declined`, and `errored`.

## Error handling

| Processing outcome | HTTP result | Checkout state after |
|---|---|---|
| `succeeded` | `200`, checkout `completed`, order emitted | `completed` |
| `declined` | `402` with a generic error body | remains `incomplete` (retryable) |
| `errored` | `402`/`503` with a generic error body | remains `incomplete` (retryable) |

A decline SHOULD be generic on the wire, for example `{"detail": "Payment Failed"}`.
Per-decline reason codes are a card-testing enumeration signal, so decline detail
belongs in the business's server logs. A decline leaves the session `incomplete`
and the platform MAY retry with a fresh instrument; the business SHOULD
velocity-limit repeated failures on one session (`429`).

### Checkout status

UCP owns the checkout state machine and its transitions; the business declares
that specification as its checkout capability spec
(`https://ucp.dev/2026-04-08/specification/checkout/`). This handler operates
inside it.

A checkout session is created `incomplete`. A successful charge moves it to
`completed` and a cancellation moves it to `canceled`; both are terminal. A
declined or errored charge leaves the session `incomplete`, so the platform MAY
retry with a fresh instrument.

This handler's credential normally avoids buyer authentication: the card
arrives as an already-vaulted opaque token and the charge requests no
authentication step, so no 3DS material crosses the UCP wire.

When a charge does require buyer authentication, the business MUST return
`requires_escalation` with a `continue_url` for buyer handoff rather than
decline, and the platform SHOULD implement the handoff. A decline in place of
an escalation loses a sale that only needed a challenge.

## Security

- No raw card data (PAN/CVV) ever crosses the UCP wire; only the opaque token does.
- The Checkout SDK routes initialize with server-signed AuthDetails; the RSA signing
  key is server-only and never reaches the browser.
- The token credential carries a required `expiry`, and the business re-prices
  every charge server-side.
- The published environment key is the public tokenization key, safe to expose.
- The business profile's `keys[]` belong to UCP's profile contract, not to this
  handler; this handler does not use them. Two mechanisms apply here: the bearer
  token authenticates the platform to the UCP endpoint, and the AuthDetails
  signature authenticates the business's server to the browser SDK.

## References

- Schema: `https://specs.agentic.spreedly.com/ucp/handlers/spreedly-card/schema.json`
- UCP payment handler base: `https://ucp.dev/2026-04-08/schemas/payment_handler.json`
- UCP token credential: `https://ucp.dev/2026-04-08/schemas/shopping/types/token_credential.json`
- UCP card payment instrument: `https://ucp.dev/2026-04-08/schemas/shopping/types/card_payment_instrument.json`
