# RFC: Agentic Commerce — Delegate Payment API **Status:** Draft **Version:** 2025-09-29 **Scope:** Delegate payment credential tokenization and controlled usage via allowance constraints This RFC defines a **single, MUST-implement** HTTP endpoint that issues a **delegated vault token** for a payment credential. The token may then be used by the merchant’s existing PSP **only** within explicit _Allowance_ constraints. Payments, settlement, and compliance remain on the merchant’s rails. --- ## 1. Scope & Goals - Enable merchants to **safely delegate** a payment credential for use in ChatGPT-initiated checkouts. - Preserve merchant PSP flows, idempotency, auditability, and risk controls. - Provide a **stable, versioned** surface (API-Version = `2025-09-29`). **Out of scope:** PSP-specific authorization/capture, multi-use tokens beyond allowance, refund semantics. ### 1.1 Terminology & Normative Language The key words **MUST**, **MUST NOT**, **SHOULD**, **MAY** are to be interpreted as described in RFC 2119. --- ## 2. Protocol Phases ### 2.1 Initialization (MUST happen before tokenization) - **Version compatibility:** Client **MUST** send `API-Version`. Server **MUST** validate support (e.g., `2025-09-29`). - When rejecting a request due to missing or unsupported `API-Version` header, servers **SHOULD** return HTTP `400 Bad Request` with a `supported_versions` array listing all versions the server accepts. Servers **MAY** use `unsupported_api_version` or `missing_api_version` as well-known `code` values. - **Identity proofing requirements:** Server advertises acceptable signature algorithms (e.g., Ed25519, ES256) out-of-band. - **Implementation details:** Client capabilities (risk signals, wallet types) **SHOULD** be documented or discoverable. - **Client preparation:** Canonical JSON of request; cryptographic key material for signing. ### 2.2 Preparation & Signing - Client **MUST** serialize the request using a canonical JSON scheme. - Client **MUST** compute a detached signature with its private key, **base64url-encode** it, and place it in the `Signature` header. - Client **MUST** include `Timestamp` (RFC 3339) and **MUST** include `Idempotency-Key`. ### 2.3 Tokenize Request - Client **MUST** issue: - **Method:** `POST` - **Path:** `/agentic_commerce/delegate_payment` - **Headers (required unless marked optional):** - `Authorization: Bearer ` (**REQUIRED**) - `Content-Type: application/json` (**REQUIRED**) - `Accept-Language: en-us` (OPTIONAL) - `User-Agent: ` (OPTIONAL) - `Idempotency-Key: ` (**REQUIRED**; opaque string, max 255 characters, UUID v4 recommended) - `Request-Id: ` (RECOMMENDED) - `Signature: ` (RECOMMENDED; identity verification over canonical request) - `Timestamp: ` (RECOMMENDED) - `API-Version: 2025-09-29` (**REQUIRED**) ### 2.4 Token Creation & Response - On success, server **MUST** return `201 Created` with a unique token `id` and `created` timestamp. - Response **MUST** echo correlation data under `metadata` when applicable (e.g., `merchant_id`, `idempotency_key`). ### 2.5 Usage & Expiry - The returned token **MUST ONLY** be usable within the provided **Allowance** (reason, max_amount, currency, expiry). - The token **MUST** become invalid at or after `allowance.expires_at`. ### 2.6 Error Handling - Errors **MUST** be returned as **flat JSON objects** (no envelope) with fields: `type`, `code`, `message`, optional `param`. - Servers **SHOULD** use appropriate HTTP status codes: - `400/422` invalid request/semantic validation - `409` idempotency conflict - `429` rate limit - `500/503` processing or service unavailable --- ## 3. HTTP Interface ### 3.1 Endpoint ``` POST /agentic_commerce/delegate_payment ``` ### 3.2 Request Body (Top-level) Exactly **one** credential type is supported today: **card**. | Field | Type | Req | Description | | ----------------- | -------------------- | :-: | -------------------------------------------------- | | `payment_method` | PaymentMethodCard | ✅ | The credential to tokenize. (type MUST be `card`.) | | `allowance` | Allowance | ✅ | Constraints on how the token may be used. | | `billing_address` | Address | ❌ | Address associated with the payment method. | | `risk_signals` | RiskSignal[] | ✅ | One or more risk signals. | | `metadata` | object (map) | ✅ | Arbitrary key/values for correlation. | ### 3.3 PaymentMethodCard (REQUIRED) - `type`: **MUST** equal `card`. - `card_number_type`: `fpan` | `network_token` (**REQUIRED**) - `virtual`: boolean (**REQUIRED**) - `number`: string (**REQUIRED**) (FPAN/DPAN/network token/virtual PAN) - `exp_month`: string (max 2; `"01"`–`"12"`) - `exp_year`: string (max 4; four-digit year) - `name`: string - `cvc`: string (max 4) - `checks_performed`: array of `avs` | `cvv` | `ani` | `auth0` - `iin`: string (max 8) - `display_card_funding_type`: `credit` | `debit` | `prepaid` (**REQUIRED**) - `display_wallet_type`: string (e.g., wallet indicator for virtual) - `display_brand`: string (e.g., `visa`, `amex`) - `display_last4`: string (max 4) - `metadata`: map (**REQUIRED**) ### 3.4 Address (OPTIONAL) - `name` (≤256), `line_one` (≤60), `line_two` (≤60), `city` (≤60), `state` (ISO-3166-2 where applicable), `country` (ISO-3166-1 alpha-2), `postal_code` (≤20) ### 3.5 Allowance (REQUIRED) - `reason`: **MUST** be `one_time` - `max_amount`: integer, minor units (e.g., $20 → `2000`) - `currency`: string, lowercase ISO-4217 (e.g., `usd`) - `checkout_session_id`: string - `merchant_id`: string (≤256) - `expires_at`: RFC 3339 timestamp ### 3.6 RiskSignal (REQUIRED, one or more) - `type`: **MUST** be `card_testing` - `score`: integer - `action`: `blocked` | `manual_review` | `authorized` ### 3.7 Metadata (REQUIRED) - Free-form key/value pairs (strings). Store correlation fields (e.g., `source`, `campaign`, `merchant_id`). --- ## 4. Responses ### 4.1 Success — HTTP `201 Created` **Headers:** - `Content-Type: application/json` - `Request-Id: ` **Body:** ```json { "id": "vt_01J8Z3WXYZ9ABC", "created": "2025-09-29T11:00:00Z", "metadata": { "source": "agent_checkout", "merchant_id": "acme", "idempotency_key": "idem_abc123" } } ``` ### 4.2 Error — Flat Object (no envelope) **Status:** `4xx` / `5xx` **Body:** ```json { "type": "invalid_request", "code": "invalid_card", "message": "Missing/malformed field", "param": "payment_method.number" } ``` **Type & Code Guidelines** - `type` ∈ `invalid_request`, `rate_limit_exceeded`, `processing_error`, `service_unavailable` - `code` ∈ `invalid_card`, `duplicate_request`, `idempotency_conflict`, `idempotency_key_required`, `idempotency_in_flight` - `param` **SHOULD** be an RFC 9535 JSONPath (when applicable). --- ## 5. Idempotency & Retries > Canonical reference for delegate-payment idempotency. Supersedes prior idempotency rules where they conflict. See SEP #120. ### 5.1 Idempotency-Key Requirement - Clients **MUST** include an `Idempotency-Key` header on the `POST /agentic_commerce/delegate_payment` request. - The key is an **opaque string**, max **255 characters**; UUID v4 is **RECOMMENDED**. - Servers **MUST** scope keys to the **authenticated identity + endpoint path**. - A request **without** an `Idempotency-Key` header **MUST** be rejected with: ```json { "type": "invalid_request", "code": "idempotency_key_required", "message": "Idempotency-Key header is required" } ``` HTTP status: **400 Bad Request**. ### 5.2 Request Equivalence - Equivalence is determined by **semantic JSON equality of the request body only**; headers are excluded. - Servers **MUST** treat the following as equivalent: | Variation | Equivalent? | |---|---| | Different key ordering | Yes | | `null` value vs absent key | **Different** — `null` means "clear this field"; absent means "do not modify" | | Trailing zeros in numbers (`1.0` vs `1`) | Yes | | Array element ordering | **No** — arrays are order-sensitive | - Monetary values **SHOULD** use **string** or **integer-cent** representations to avoid floating-point ambiguity. - _Informative:_ Servers **MAY** implement comparison via RFC 8785 (JCS) canonicalization + SHA-256 fingerprint. ### 5.3 Replay Behavior - Same key + **identical** body → server **MUST** return the **original response** with the **same HTTP status code**. - The replayed response **SHOULD** include the header `Idempotent-Replayed: true`. - Servers **MUST NOT** re-execute side effects (e.g., vault token creation, PSP tokenization calls) on replay. ### 5.4 Error Responses All idempotency errors use `type: "invalid_request"` and the following codes: | Scenario | HTTP | `code` | Retryable? | |---|---|---|---| | Missing `Idempotency-Key` header | 400 | `idempotency_key_required` | Yes (add header) | | Same key, different body | 422 | `idempotency_conflict` | **No** (permanent) | | Same key, original request still in flight | 409 | `idempotency_in_flight` | Yes (honor `Retry-After`) | - **422 — payload mismatch** example: ```json { "type": "invalid_request", "code": "idempotency_conflict", "message": "Idempotency-Key has already been used with a different request body" } ``` - **409 — in-flight collision** example: ```json { "type": "invalid_request", "code": "idempotency_in_flight", "message": "A request with this Idempotency-Key is currently being processed" } ``` Servers **SHOULD** include a `Retry-After` header (in seconds). ### 5.5 Server Error Caching - Responses with HTTP **5xx** status codes **MUST NOT** be cached against the idempotency key. - A retry with the same key after a 5xx **MUST** be processed as a **fresh request**. ### 5.6 Key Retention - Servers **MUST** retain idempotency key → response mappings for **at least 24 hours**. - After expiry, a reused key is treated as a **new request** (no error). ### 5.7 Implementation Guidance (informative) - **Gateway / middleware pattern:** Idempotency logic is **RECOMMENDED** to live in a middleware layer in front of business logic. - **Atomic transaction boundaries:** The idempotency key record and the tokenization operation **SHOULD** be committed in the **same ACID transaction**. - **Recovery points:** When tokenization triggers external PSP calls, servers **SHOULD** implement recovery-point semantics — record intermediate state so that retries can resume rather than restart. - **SDK serialization:** Because `null` and absent are semantically distinct (§5.2), SDK authors **SHOULD** ensure their serializers preserve the distinction. Clients **MUST** only include `null` when they intend to clear a field, and **MUST** omit the key when they intend to leave the field unchanged. --- ## 6. Security Considerations - **Authentication:** `Authorization: Bearer ` **MUST** be required. - **Integrity:** `Signature` over canonical JSON **SHOULD** be verified (algorithm policy advertised out-of-band). - **Freshness:** `Timestamp` **SHOULD** be required and checked within an acceptable clock-skew window. - **PII/PCI:** Card data handling **MUST** follow applicable PCI DSS requirements; logs **MUST NOT** contain full PAN or CVC. - **Transport:** All requests **MUST** use HTTPS/TLS 1.3. --- ## 7. Validation Rules (non-exhaustive) - `payment_method.type` **MUST** be `card`. - `payment_method.card_number_type` ∈ `fpan|network_token`. - `payment_method.virtual` present (boolean). - `payment_method.number` present (string). - When present: - `exp_month` length ≤ 2 and value `"01"`–`"12"`. - `exp_year` length ≤ 4 and four digits. - `cvc` length ≤ 4. - `iin` length ≤ 8. - `display_card_funding_type` ∈ `credit|debit|prepaid`. - `allowance.currency` matches `^[a-z]{3}$` (e.g., `usd`). - `allowance.expires_at` must be RFC 3339. - At least one `risk_signal` item. --- ## 8. Examples ### 8.1 Success (FPAN, one-time allowance) ```json { "payment_method": { "type": "card", "card_number_type": "fpan", "virtual": false, "number": "4242424242424242", "exp_month": "11", "exp_year": "2026", "name": "Jane Doe", "cvc": "223", "checks_performed": ["avs", "cvv"], "iin": "424242", "display_card_funding_type": "credit", "display_brand": "visa", "display_last4": "4242", "metadata": { "issuing_bank": "temp" } }, "allowance": { "reason": "one_time", "max_amount": 2000, "currency": "usd", "checkout_session_id": "csn_01HV3P3...", "merchant_id": "acme", "expires_at": "2025-10-09T07:20:50.52Z" }, "billing_address": { "name": "Ada Lovelace", "line_one": "1234 Chat Road", "line_two": "", "city": "San Francisco", "state": "CA", "country": "US", "postal_code": "94131" }, "risk_signals": [ { "type": "card_testing", "score": 10, "action": "manual_review" } ], "metadata": { "campaign": "q4" } } ``` **201 Created** ```json { "id": "vt_01J8Z3WXYZ9ABC", "created": "2025-09-29T11:00:00Z", "metadata": { "source": "agent_checkout", "merchant_id": "acme", "idempotency_key": "idem_abc123" } } ``` ### 8.2 Error (invalid expiry) **400 Bad Request** ```json { "type": "invalid_request", "code": "invalid_card", "message": "Invalid expiry date (exp_month or exp_year)", "param": "payment_method.exp_month" } ``` ### 8.3 Error (idempotency conflict) **422 Unprocessable Entity** ```json { "type": "invalid_request", "code": "idempotency_conflict", "message": "Idempotency-Key has already been used with a different request body" } ``` --- ## 9. Conformance Checklist - [ ] Accepts `API-Version` and validates `2025-09-29` - [ ] Verifies `Authorization` (Bearer) - [ ] Validates request fields per §3 & §7 - [ ] Enforces **exactly one** credential type (`card`) - [ ] Requires `Idempotency-Key`; returns 400/422/409 per §5 - [ ] Emits **flat** error object with `type`/`code`/`message`/`param?` - [ ] Returns `201` with `id`, `created`, and `metadata` - [ ] Enforces `allowance` constraints and expiry - [ ] Redacts PCI data in logs/telemetry --- ## 10. Change Log - **Unreleased**: Rewrote §5 (Idempotency & Retries) with full normative rules: mandatory `Idempotency-Key` on all POST requests, request equivalence semantics, replay behavior with `Idempotent-Replayed` header, IETF-aligned error codes (`idempotency_key_required`, `idempotency_conflict`, `idempotency_in_flight`), 5xx caching prohibition, and 24-hour key retention. Added `idempotency_key_required` and `idempotency_in_flight` to `Error.code` enum. See SEP #120. - **2025-09-29**: Initial draft. Errors changed to **flat object** (no envelope). Tightened allowance and card display requirements.