From 26ebe0f6eb53bd7722869ac504fad43c1f43f833 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 8 Apr 2026 15:03:46 -0700 Subject: [PATCH 01/16] add subscription intent and tempo subscription drafts --- .../draft-payment-intent-subscription-00.md | 410 ++++++++++++++ .../tempo/draft-tempo-subscription-00.md | 511 ++++++++++++++++++ 2 files changed, 921 insertions(+) create mode 100644 specs/intents/draft-payment-intent-subscription-00.md create mode 100644 specs/methods/tempo/draft-tempo-subscription-00.md diff --git a/specs/intents/draft-payment-intent-subscription-00.md b/specs/intents/draft-payment-intent-subscription-00.md new file mode 100644 index 00000000..1a9afd19 --- /dev/null +++ b/specs/intents/draft-payment-intent-subscription-00.md @@ -0,0 +1,410 @@ +--- +title: Subscription Intent for HTTP Payment Authentication +abbrev: Payment Intent Subscription +docname: draft-payment-intent-subscription-00 +version: 00 +category: info +ipr: noModificationTrust200902 +submissiontype: IETF +consensus: true + +author: + - name: Jake Moxey + ins: J. Moxey + email: jake@tempo.xyz + org: Tempo Labs + - name: Brendan Ryan + ins: B. Ryan + email: brendan@tempo.xyz + org: Tempo Labs + - name: Tom Meagher + ins: T. Meagher + email: thomas@tempo.xyz + org: Tempo Labs + +normative: + RFC2119: + RFC3339: + RFC4648: + RFC8174: + RFC8259: + RFC8785: + I-D.httpauth-payment: + title: "The 'Payment' HTTP Authentication Scheme" + target: https://datatracker.ietf.org/doc/draft-httpauth-payment/ + author: + - name: Jake Moxey + date: 2026-01 + I-D.ietf-httpapi-idempotency-key-header: + title: "The Idempotency-Key HTTP Header Field" + target: https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/ + author: + - name: Jayadeba Jena + date: 2024-06 +--- + +--- abstract + +This document defines the "subscription" payment intent for use with the +Payment HTTP Authentication Scheme. The "subscription" intent +represents a recurring fixed-amount payment where the payer grants the +server permission to charge the same amount once per billing period +until a specified expiry time. + +--- middle + +# Introduction + +The "subscription" intent enables recurring fixed-amount payments. A +successful subscription activation creates an authorization for the +server to collect the same payment amount once per billing period until +the subscription expires or is cancelled. + +This intent is useful for recurring API plans, content subscriptions, +and other services with a stable price per billing period. + +## Relationship to Payment Methods + +Payment methods implement "subscription" using method-specific recurring +authorization mechanisms. This document defines the abstract semantics +and shared request fields. Payment method specifications define how +those semantics are enforced. + +# Requirements Language + +{::boilerplate bcp14-tagged} + +# Terminology + +Subscription +: A recurring payment authorization for a fixed amount charged once per + billing period. + +Billing Period +: A fixed-duration window during which at most one subscription charge + may be collected. + +Activation +: The successful initial registration of a subscription, which also + collects the first billing-period charge. + +Renewal +: A later charge that collects the subscription amount for a subsequent + billing period. + +Cancellation +: The act of ending a subscription before `subscriptionExpires`, + preventing future renewals. + +# Intent Semantics + +## Definition + +The "subscription" intent represents a request for a recurring +fixed-amount payment of `amount`, charged once per billing period until +`subscriptionExpires` or cancellation. + +## Properties + +| Property | Value | +|----------|-------| +| **Intent Identifier** | `subscription` | +| **Payment Timing** | Recurring (initial charge at activation, then once per period) | +| **Idempotency** | Credential single-use; subscription grant reusable across billing periods | +| **Reversibility** | Cancellable before expiry | + +## Flow + +~~~ + Client Server Payment Network + │ │ │ + │ (1) GET /resource │ │ + ├───────────────────────────────>│ │ + │ │ │ + │ (2) 402 Payment Required │ │ + │ intent="subscription" │ │ + │<───────────────────────────────┤ │ + │ │ │ + │ (3) Sign subscription grant │ │ + │ │ │ + │ (4) Authorization: Payment │ │ + ├───────────────────────────────>│ │ + │ │ │ + │ │ (5) Activate subscription │ + │ │ + collect first charge │ + │ ├─────────────────────────────>│ + │ │ │ + │ (6) 200 OK + Receipt │ │ + │<───────────────────────────────┤ │ + │ │ │ + │ ... later period ... │ │ + │ │ │ + │ │ (7) Collect renewal │ + │ ├─────────────────────────────>│ + │ │ │ + │ (8) 200 OK + Receipt │ │ + │<───────────────────────────────┤ │ + │ │ │ +~~~ + +# Request Schema + +The `request` parameter for a "subscription" intent is a JSON object +with shared fields defined by this specification and optional +method-specific extensions in the `methodDetails` field. The `request` +JSON MUST be serialized using JSON Canonicalization Scheme (JCS) +{{RFC8785}} and base64url-encoded without padding per +{{I-D.httpauth-payment}}. + +## Shared Fields + +All payment methods implementing the "subscription" intent MUST support +these shared fields. Payment methods MAY elevate OPTIONAL fields to +REQUIRED in their method specification. + +### Required Fields + +| Field | Type | Description | +|-------|------|-------------| +| `amount` | string | Fixed payment amount per billing period in base units | +| `currency` | string | Currency or asset identifier (see {{currency-formats}}) | +| `periodSeconds` | string | Billing period duration in seconds | +| `subscriptionExpires` | string | Subscription expiry timestamp in {{RFC3339}} format | + +The `amount` value MUST be a string representation of a positive +integer in base 10 with no sign, decimal point, exponent, or +surrounding whitespace. Leading zeros MUST NOT be used. + +The `periodSeconds` value MUST be a string representation of a positive +integer in base 10 with no sign, decimal point, exponent, or +surrounding whitespace. Leading zeros MUST NOT be used. + +### Optional Fields + +| Field | Type | Description | +|-------|------|-------------| +| `recipient` | string | Payment recipient in method-native format | +| `description` | string | Human-readable subscription description | +| `externalId` | string | Merchant's reference for the subscription | +| `methodDetails` | object | Method-specific extension data | + +Challenge expiry is conveyed by the `expires` auth-param in +`WWW-Authenticate` per {{I-D.httpauth-payment}}, using {{RFC3339}} +format. Request objects MUST NOT duplicate the challenge expiry value. +The `subscriptionExpires` field instead defines when the subscription +itself expires. + +The `subscriptionExpires` value MUST be strictly later than the +challenge `expires` timestamp. Servers MUST reject credentials where +`subscriptionExpires` is at or before the challenge `expires`. + +The first billing period begins when the subscription is activated. +Payment methods MAY define additional activation controls in +`methodDetails`, but MUST define exact activation semantics if they do +so. + +## Currency Formats {#currency-formats} + +The `currency` field supports multiple formats to accommodate different +payment networks: + +| Format | Example | Description | +|--------|---------|-------------| +| ISO 4217 | `"usd"`, `"eur"` | Fiat currencies (lowercase) | +| Token address | `"0x20c0..."` | On-chain token contract address | +| Method-defined | (varies) | Payment method-specific currency identifiers | + +Payment method specifications MUST document which currency formats they +support and how to interpret amounts for each format. + +## Method Extensions + +Payment methods MAY define additional fields in the `methodDetails` +object. These fields are method-specific and MUST be documented in the +payment method specification. + +## Examples + +### Traditional Payment Processor + +~~~ json +{ + "amount": "9900", + "currency": "usd", + "periodSeconds": "2592000", + "subscriptionExpires": "2026-01-01T00:00:00Z", + "description": "Pro plan" +} +~~~ + +### Blockchain Payment (Tempo) + +~~~ json +{ + "amount": "10000000", + "currency": "0x20c0000000000000000000000000000000000001", + "periodSeconds": "2592000", + "subscriptionExpires": "2026-01-01T00:00:00Z", + "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00", + "methodDetails": { + "chainId": 42431 + } +} +~~~ + +# Credential Requirements + +## Payload + +The credential `payload` for a "subscription" intent contains the +subscription authorization grant. The format is method-specific: + +| Authorization Type | Description | Example Methods | +|-------------------|-------------|-----------------| +| Periodic key auth | Delegated key with per-period limits | Tempo | +| Subscription setup | Processor-managed recurring payment setup | Stripe | +| Signed mandate | Recurring debit mandate | ACH, SEPA | + +## Single-Use + +Each "subscription" credential MUST be usable only once per challenge. +Servers MUST reject replayed credentials. + +A successfully activated subscription may be reused for later billing +periods until: + +- The `subscriptionExpires` timestamp is reached +- The payer explicitly cancels it +- The payment method revokes or invalidates the authorization + +# Subscription Lifecycle + +## Activation + +When the server receives a "subscription" credential, it MUST: + +1. Verify the subscription authorization proof +2. Activate the subscription +3. Collect the first billing-period charge +4. Initialize durable subscription state for later renewals +5. Return success (200) with a `Payment-Receipt` for the first charge + +## Renewal + +For each later billing period, the server MAY collect one renewal +charge for `amount` using the method-specific recurring authorization. + +If the server grants access for a later billing period, it MUST ensure +that the renewal charge for that period has been collected before, or +atomically with, delivering the corresponding service. + +Servers MUST NOT collect more than one renewal charge for the same +billing period. + +## Server Accounting and Idempotency + +Servers MUST maintain durable subscription state sufficient to enforce +per-period charging rules across retries and concurrent requests. + +At minimum, servers MUST track: + +- Subscription identifier +- Current billing period start time +- Whether the current billing period has been charged +- Subscription expiry +- Cancellation or revocation status + +For non-idempotent requests, clients SHOULD send an `Idempotency-Key` +header per {{I-D.ietf-httpapi-idempotency-key-header}}. Servers MUST NOT +collect the same activation or renewal charge more than once for a +duplicate idempotent request. + +## Cancellation + +Payers SHOULD be able to cancel subscriptions before expiry. +Cancellation mechanisms are method-specific. + +Servers MUST NOT collect renewal charges after cancellation takes +effect. + +## Error Responses + +When a subscription cannot be used to fulfill a request, the server +MUST return an appropriate HTTP status code: + +| Condition | Status Code | Behavior | +|-----------|-------------|----------| +| Subscription expired | 402 Payment Required | Issue new challenge | +| Subscription cancelled or revoked | 402 Payment Required | Issue new challenge | +| Current billing period unpaid or renewal failed | 402 Payment Required | Issue new challenge | +| Invalid credential | 402 Payment Required | Issue new challenge | + +For all 402 responses, the server MUST include a `WWW-Authenticate` +header with a fresh challenge. Clients receiving a 402 after a +previously valid subscription SHOULD treat the subscription as no longer +usable and initiate a new subscription flow. + +# Security Considerations + +## Recurring Charge Awareness + +Clients MUST clearly communicate that a subscription authorizes future +recurring charges without requiring a new user action for each billing +period. + +## Amount and Period Verification + +Clients MUST verify before activating a subscription: + +1. `amount` is acceptable for the service +2. `currency` is expected +3. `periodSeconds` matches the expected billing interval +4. `subscriptionExpires` is acceptable + +Clients MUST NOT rely on the `description` field for payment +verification. + +## Duplicate Charge Prevention + +Servers MUST prevent duplicate activation and renewal charges caused by +retries, parallel requests, or races between charging and service +delivery. + +## Server Accountability + +Servers operating subscriptions are responsible for: + +- Secure storage of subscription authorization data +- Not charging more than once per billing period +- Honoring cancellation and revocation +- Providing transaction or billing records to payers + +## Caching + +Responses to subscription challenges (402 Payment Required) MUST include +`Cache-Control: no-store` to prevent sensitive payment data from being +cached by intermediaries. + +Responses containing `Payment-Receipt` headers MUST include +`Cache-Control: private` to prevent shared caches from storing payment +receipts. + +# IANA Considerations + +## Payment Intent Registration + +This document registers the "subscription" intent in the "HTTP Payment +Intents" registry established by {{I-D.httpauth-payment}}: + +| Intent | Description | Reference | +|--------|-------------|-----------| +| `subscription` | Recurring fixed-amount payment | This document | + +Contact: Tempo Labs () + +--- back + +# Acknowledgements + +The authors thank the MPP community for their feedback on this +specification. diff --git a/specs/methods/tempo/draft-tempo-subscription-00.md b/specs/methods/tempo/draft-tempo-subscription-00.md new file mode 100644 index 00000000..cf370e22 --- /dev/null +++ b/specs/methods/tempo/draft-tempo-subscription-00.md @@ -0,0 +1,511 @@ +--- +title: Tempo Subscription Intent for HTTP Payment Authentication +abbrev: Tempo Subscription +docname: draft-tempo-subscription-00 +version: 00 +category: info +ipr: noModificationTrust200902 +submissiontype: IETF +consensus: true + +author: + - name: Jake Moxey + ins: J. Moxey + email: jake@tempo.xyz + organization: Tempo Labs + - name: Brendan Ryan + ins: B. Ryan + email: brendan@tempo.xyz + organization: Tempo Labs + - name: Tom Meagher + ins: T. Meagher + email: thomas@tempo.xyz + organization: Tempo Labs + +normative: + RFC2119: + RFC3339: + RFC4648: + RFC8174: + RFC8259: + RFC8785: + I-D.httpauth-payment: + title: "The 'Payment' HTTP Authentication Scheme" + target: https://datatracker.ietf.org/doc/draft-ietf-httpauth-payment/ + author: + - name: Jake Moxey + date: 2026-01 + I-D.payment-intent-subscription: + title: "Subscription Intent for HTTP Payment Authentication" + target: https://datatracker.ietf.org/doc/draft-payment-intent-subscription/ + author: + - name: Jake Moxey + date: 2026-04 + +informative: + EIP-55: + title: "Mixed-case checksum address encoding" + target: https://eips.ethereum.org/EIPS/eip-55 + author: + - name: Vitalik Buterin + date: 2016-01 + TEMPO-TX-SPEC: + title: "Tempo Transaction Specification" + target: https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction + author: + - org: Tempo Labs + TIP-1020: + title: "TIP-1020: Signature Verification Precompile" + target: https://docs.tempo.xyz/protocol/tips/tip-1020 + author: + - org: Tempo Labs +--- + +--- abstract + +This document defines the "subscription" intent for the "tempo" +payment method in the Payment HTTP Authentication Scheme. It specifies +how clients grant servers permission to collect a fixed TIP-20 token +payment once per billing period using recipient-scoped access keys on +the Tempo blockchain. + +--- middle + +# Introduction + +The `subscription` intent on Tempo represents a recurring fixed-amount +TIP-20 payment. The client grants the server a recipient-scoped access +key with a per-period spending limit. Activation registers the key and +collects the first billing-period charge in the same transaction. + +This specification inherits the shared `subscription` intent semantics +from {{I-D.payment-intent-subscription}} and defines Tempo-specific +request fields, payloads, and settlement behavior. + +Tempo subscriptions support only key-authorization fulfillment. +Tempo transactions containing standalone `approve` calls and push-mode +hash credentials do not provide the per-period enforcement required for +this intent. + +## Subscription Flow + +The following diagram illustrates the Tempo subscription flow: + +~~~ + Client Server Tempo Network + │ │ │ + │ (1) GET /api/resource │ │ + │--------------------------> │ │ + │ │ │ + │ (2) 402 Payment Required │ │ + │ intent="subscription" │ │ + │<-------------------------- │ │ + │ │ │ + │ (3) Sign keyAuthorization │ │ + │ with period limit │ │ + │ │ │ + │ (4) Authorization: Payment │ │ + │--------------------------> │ │ + │ │ │ + │ │ (5) Register key + │ + │ │ transfer first period │ + │ │--------------------------> │ + │ │ │ + │ (6) 200 OK + Receipt │ │ + │<-------------------------- │ │ + │ │ │ + │ ... later period ... │ │ + │ │ │ + │ │ (7) transfer next period │ + │ │--------------------------> │ + │ │ │ + │ (8) 200 OK + Receipt │ │ + │<-------------------------- │ │ + │ │ │ +~~~ + +# Requirements Language + +{::boilerplate bcp14-tagged} + +# Terminology + +TIP-20 +: Tempo's enshrined token standard, implemented as precompiles rather + than smart contracts. TIP-20 tokens use 6 decimal places and provide + `transfer`, `transferFrom`, and `approve` operations. + +Access Key +: A delegated signing key. For Tempo subscriptions, the access key is + configured with an expiry timestamp, a per-period token spending + limit, and a destination restriction. + +AccountKeychain Precompile +: The Tempo precompile that manages access-key registration, spending + limits, and periodic-limit enforcement. + +# Request Schema + +The `request` parameter in the `WWW-Authenticate` challenge contains a +base64url-encoded JSON object. The `request` JSON MUST be serialized +using JSON Canonicalization Scheme (JCS) {{RFC8785}} and +base64url-encoded without padding per {{I-D.httpauth-payment}}. + +## Shared Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `amount` | string | REQUIRED | Fixed payment amount per billing period in base units | +| `currency` | string | REQUIRED | TIP-20 token address | +| `periodSeconds` | string | REQUIRED | Billing period duration in seconds | +| `subscriptionExpires` | string | REQUIRED | Subscription expiry timestamp in {{RFC3339}} format | +| `recipient` | string | REQUIRED | Recipient address authorized for subscription charges | +| `description` | string | OPTIONAL | Human-readable subscription description | +| `externalId` | string | OPTIONAL | Merchant's reference for the subscription | + +The `amount` value MUST be a string representation of a positive +integer in base 10 with no sign, decimal point, exponent, or +surrounding whitespace. Leading zeros MUST NOT be used. + +The `periodSeconds` value MUST be a string representation of a positive +integer in base 10 with no sign, decimal point, exponent, or +surrounding whitespace. Leading zeros MUST NOT be used. + +## Method Details + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `methodDetails.chainId` | number | OPTIONAL | Tempo chain ID. If omitted, the default value is 42431 (Tempo mainnet). | + +Challenge expiry is conveyed by the `expires` auth-param in +`WWW-Authenticate` per {{I-D.httpauth-payment}}, using {{RFC3339}} +format. Request objects MUST NOT duplicate the challenge expiry value. +The `subscriptionExpires` field instead defines when the subscription +itself expires. + +The `subscriptionExpires` value MUST be strictly later than the +challenge `expires` timestamp. Servers MUST reject credentials where +`subscriptionExpires` is at or before the challenge `expires`. + +**Example:** + +~~~json +{ + "amount": "10000000", + "currency": "0x20c0000000000000000000000000000000000001", + "periodSeconds": "2592000", + "subscriptionExpires": "2026-01-01T00:00:00Z", + "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00", + "methodDetails": { + "chainId": 42431 + } +} +~~~ + +The client fulfills this by signing a key authorization with: + +- Expiry = `subscriptionExpires` +- Per-period spending limit = `amount` +- Billing period = `periodSeconds` +- Destination restriction = `recipient` + +# Credential Schema + +The credential in the `Authorization` header contains a +base64url-encoded JSON object per {{I-D.httpauth-payment}}. + +## Credential Structure + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `challenge` | object | REQUIRED | Echo of the challenge from the server | +| `payload` | object | REQUIRED | Tempo-specific payload object | +| `source` | string | OPTIONAL | Payer identifier as a DID (e.g., `did:pkh:eip155:42431:0x...`) | + +The `source` field, if present, SHOULD use the `did:pkh` method with +the chain ID applicable to the challenge and the payer's Ethereum +address. + +## Key Authorization Payload (type="keyAuthorization") + +Subscriptions on Tempo MUST use `type="keyAuthorization"`. The +`signature` field contains the complete signed key authorization +serialized as RLP and hex-encoded with `0x` prefix. + +The encoded value MUST be a signed key authorization containing at +least: + +- the Tempo chain ID +- the access-key identifier +- the authorization expiry +- the TIP-20 token spending limit +- the billing-period limit configuration +- the recipient restriction + +The embedded signature MUST use a primitive signature type supported by +{{TIP-1020}}. Keychain wrapper signatures MUST NOT be used for this +field. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `signature` | string | REQUIRED | Hex-encoded RLP-serialized signed key authorization | +| `type` | string | REQUIRED | `"keyAuthorization"` | + +**Example:** + +~~~json +{ + "challenge": { + "id": "qT8wErYuI3oPlKjH6gFdSa", + "realm": "api.example.com", + "method": "tempo", + "intent": "subscription", + "request": "eyJ...", + "expires": "2025-02-05T12:05:00Z" + }, + "payload": { + "signature": "0xf8c1...signed authorization bytes...", + "type": "keyAuthorization" + }, + "source": "did:pkh:eip155:42431:0x1234567890abcdef1234567890abcdef12345678" +} +~~~ + +## Unsupported Payload Types + +Tempo subscriptions do not support `type="transaction"` or +`type="hash"` payloads. Servers MUST reject such credentials with +`402 Payment Required`, include a fresh `WWW-Authenticate` challenge, +and describe the failure using Problem Details. + +# Settlement Procedure + +## Activation and First-Period Charge + +For `intent="subscription"`, activation and the first billing-period +charge are a single atomic operation: + +~~~ + Client Server Tempo Network + | | | + | (1) Authorization: | | + | Payment | | + | (signed keyAuth) | | + |--------------------------> | | + | | | + | | (2) Construct tx with: | + | | - keyAuthorization | + | | - transfer(recipient, | + | | amount) | + | | | + | | (3) eth_sendRawTxSync | + | |--------------------------> | + | | | + | | (4) Key registered + | + | | transfer executed | + | |<-------------------------- | + | | | + | (5) 200 OK | | + | Payment-Receipt: ... | | + |<-------------------------- | | + | | | +~~~ + +Servers MUST treat the subscription as active only after the activation +transaction succeeds. + +## Renewal + +For each later billing period, the server MAY submit one transaction +using the registered access key to transfer `amount` to `recipient`. + +Servers MUST NOT submit more than one successful renewal charge for the +same billing period. + +## Billing Anchor and Subscription State + +The billing anchor for a Tempo subscription is the settlement time of +the successful activation transaction. + +Billing periods are defined as: + +- Period 0: `[anchor, anchor + periodSeconds)` +- Period 1: `[anchor + periodSeconds, anchor + 2*periodSeconds)` +- Period N: `[anchor + N*periodSeconds, anchor + (N+1)*periodSeconds)` + +Servers MUST maintain durable local state for each subscription, +including at least: + +- subscription identifier +- billing anchor +- last charged billing-period index +- subscription expiry +- revocation status + +When granting access in a later billing period, servers MUST: + +- Verify the subscription has not expired or been revoked +- Determine the current billing-period index from the anchor and + `periodSeconds` +- Verify that the current billing period has not already been charged +- Atomically record the current billing period as charged before, or + atomically with, delivering the corresponding service + +For duplicate idempotent requests, servers MUST NOT charge the same +billing period more than once. + +## Source Verification + +If a credential includes the optional `source` field, servers MUST NOT +trust this value without verification. + +Servers MUST verify the payer identity by recovering the root signer +address from the signed key authorization using +{{TIP-1020}}-compatible verification semantics over the encoded key +authorization payload. + +## Receipt Generation + +Upon successful activation or renewal, servers MUST return a +`Payment-Receipt` header per {{I-D.httpauth-payment}}. Servers MUST NOT +include a `Payment-Receipt` header on error responses. + +The receipt payload for Tempo subscription: + +| Field | Type | Description | +|-------|------|-------------| +| `method` | string | `"tempo"` | +| `reference` | string | Transaction hash of the settlement transaction | +| `status` | string | `"success"` | +| `timestamp` | string | {{RFC3339}} settlement time | +| `externalId` | string | OPTIONAL. Echoed from the challenge request | + +# Security Considerations + +## Destination Scoping + +Tempo subscription access keys MUST be restricted to the `recipient` +address in the request. Servers MUST reject credentials that do not +enforce this restriction. + +## Amount and Period Verification + +Clients MUST parse and verify the `request` payload before signing: + +1. Verify `amount` is reasonable for the service +2. Verify `currency` is the expected TIP-20 token address +3. Verify `periodSeconds` matches expectations +4. Verify `recipient` is controlled by the expected party +5. Verify `subscriptionExpires` is acceptable + +## Revocation + +Users can revoke subscription access keys at any time via the +AccountKeychain precompile. Servers SHOULD handle revocation gracefully +by returning a fresh subscription challenge. + +## Duplicate Charge Prevention + +On-chain periodic limits prevent overspending within a billing period, +but they do not by themselves make HTTP service delivery idempotent. +Servers MUST implement durable local state to prevent duplicate renewal +charges caused by retries or concurrent requests. + +## Caching + +Responses to subscription challenges (402 Payment Required) MUST include +`Cache-Control: no-store` to prevent sensitive payment data from being +cached by intermediaries. + +Responses containing `Payment-Receipt` headers MUST include +`Cache-Control: private` to prevent shared caches from storing payment +receipts. + +# IANA Considerations + +## Payment Intent Registration + +This document registers the following payment intent in the "HTTP +Payment Intents" registry established by {{I-D.httpauth-payment}}: + +| Intent | Applicable Methods | Description | Reference | +|--------|-------------------|-------------|-----------| +| `subscription` | `tempo` | Recurring fixed-amount TIP-20 payment | This document | + +Contact: Tempo Labs () + +--- back + +# Example + +**Challenge:** + +~~~http +HTTP/1.1 402 Payment Required +Cache-Control: no-store +WWW-Authenticate: Payment id="qT8wErYuI3oPlKjH6gFdSa", + realm="api.example.com", + method="tempo", + intent="subscription", + expires="2025-02-05T12:05:00Z", + request="" +~~~ + +The `request` decodes to: + +~~~json +{ + "amount": "10000000", + "currency": "0x20c0000000000000000000000000000000000001", + "periodSeconds": "2592000", + "subscriptionExpires": "2026-01-01T00:00:00Z", + "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00", + "methodDetails": { + "chainId": 42431 + } +} +~~~ + +This requests a recurring payment of 10.00 alphaUSD every 2,592,000 +seconds until 2026-01-01T00:00:00Z. + +**Credential:** + +~~~json +{ + "challenge": { + "id": "qT8wErYuI3oPlKjH6gFdSa", + "realm": "api.example.com", + "method": "tempo", + "intent": "subscription", + "request": "eyJ...", + "expires": "2025-02-05T12:05:00Z" + }, + "payload": { + "signature": "0xf8c1...signed authorization bytes...", + "type": "keyAuthorization" + }, + "source": "did:pkh:eip155:42431:0x1234567890abcdef1234567890abcdef12345678" +} +~~~ + +# ABNF Collected + +~~~ abnf +tempo-subscription-challenge = "Payment" 1*SP + "id=" quoted-string "," + "realm=" quoted-string "," + "method=" DQUOTE "tempo" DQUOTE "," + "intent=" DQUOTE "subscription" DQUOTE "," + "request=" base64url-nopad + +tempo-subscription-credential = "Payment" 1*SP base64url-nopad + +; Base64url encoding without padding per RFC 4648 Section 5 +base64url-nopad = 1*( ALPHA / DIGIT / "-" / "_" ) +~~~ + +# Acknowledgements + +The authors thank the MPP community for their feedback on this +specification. From f89f6bef51272313f47b7e87838ed3bcf6266268 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 8 Apr 2026 15:46:44 -0700 Subject: [PATCH 02/16] docs: address subscription PR comments --- .../tempo/draft-tempo-subscription-00.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/specs/methods/tempo/draft-tempo-subscription-00.md b/specs/methods/tempo/draft-tempo-subscription-00.md index cf370e22..729c2981 100644 --- a/specs/methods/tempo/draft-tempo-subscription-00.md +++ b/specs/methods/tempo/draft-tempo-subscription-00.md @@ -49,6 +49,11 @@ informative: author: - name: Vitalik Buterin date: 2016-01 + TEMPO-ACCOUNT-KEYCHAIN: + title: "Account Keychain Precompile" + target: https://docs.tempo.xyz/protocol/precompiles/account-keychain + author: + - org: Tempo Labs TEMPO-TX-SPEC: title: "Tempo Transaction Specification" target: https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction @@ -142,7 +147,7 @@ Access Key AccountKeychain Precompile : The Tempo precompile that manages access-key registration, spending - limits, and periodic-limit enforcement. + limits, and periodic-limit enforcement {{TEMPO-ACCOUNT-KEYCHAIN}}. # Request Schema @@ -271,13 +276,6 @@ field. } ~~~ -## Unsupported Payload Types - -Tempo subscriptions do not support `type="transaction"` or -`type="hash"` payloads. Servers MUST reject such credentials with -`402 Payment Required`, include a fresh `WWW-Authenticate` challenge, -and describe the failure using Problem Details. - # Settlement Procedure ## Activation and First-Period Charge @@ -401,15 +399,17 @@ Clients MUST parse and verify the `request` payload before signing: ## Revocation Users can revoke subscription access keys at any time via the -AccountKeychain precompile. Servers SHOULD handle revocation gracefully -by returning a fresh subscription challenge. +AccountKeychain precompile {{TEMPO-ACCOUNT-KEYCHAIN}}. Servers SHOULD +handle revocation gracefully by returning a fresh subscription +challenge. ## Duplicate Charge Prevention On-chain periodic limits prevent overspending within a billing period, -but they do not by themselves make HTTP service delivery idempotent. -Servers MUST implement durable local state to prevent duplicate renewal -charges caused by retries or concurrent requests. +but they do not by themselves make HTTP service delivery idempotent +{{TEMPO-ACCOUNT-KEYCHAIN}}. Servers MUST implement durable local state +to prevent duplicate renewal charges caused by retries or concurrent +requests. ## Caching From 0bfc0d40637c6f6c796cdbcdadd29cb385b6b16a Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 8 Apr 2026 16:17:11 -0700 Subject: [PATCH 03/16] docs: tighten subscription auth semantics --- .../draft-payment-intent-subscription-00.md | 61 ++++++++++++--- .../tempo/draft-tempo-subscription-00.md | 77 ++++++------------- 2 files changed, 74 insertions(+), 64 deletions(-) diff --git a/specs/intents/draft-payment-intent-subscription-00.md b/specs/intents/draft-payment-intent-subscription-00.md index 1a9afd19..54a34c18 100644 --- a/specs/intents/draft-payment-intent-subscription-00.md +++ b/specs/intents/draft-payment-intent-subscription-00.md @@ -31,7 +31,7 @@ normative: RFC8785: I-D.httpauth-payment: title: "The 'Payment' HTTP Authentication Scheme" - target: https://datatracker.ietf.org/doc/draft-httpauth-payment/ + target: https://datatracker.ietf.org/doc/draft-ietf-httpauth-payment/ author: - name: Jake Moxey date: 2026-01 @@ -96,6 +96,11 @@ Cancellation : The act of ending a subscription before `subscriptionExpires`, preventing future renewals. +Subscription Identifier +: A server-issued opaque identifier for an activated subscription, + used by clients to re-authenticate into that subscription on later + requests. + # Intent Semantics ## Definition @@ -186,17 +191,23 @@ surrounding whitespace. Leading zeros MUST NOT be used. | `recipient` | string | Payment recipient in method-native format | | `description` | string | Human-readable subscription description | | `externalId` | string | Merchant's reference for the subscription | +| `subscriptionId` | string | Server-issued opaque identifier for an existing subscription | | `methodDetails` | object | Method-specific extension data | -Challenge expiry is conveyed by the `expires` auth-param in -`WWW-Authenticate` per {{I-D.httpauth-payment}}, using {{RFC3339}} -format. Request objects MUST NOT duplicate the challenge expiry value. -The `subscriptionExpires` field instead defines when the subscription -itself expires. +The `subscriptionId` field is absent during initial activation. Servers +MAY include it when issuing a challenge tied to an existing +subscription. + +Servers issuing `intent="subscription"` challenges SHOULD include the +`expires` auth-param in `WWW-Authenticate` per {{I-D.httpauth-payment}}, +using {{RFC3339}} format. Request objects MUST NOT duplicate the +challenge expiry value. The `subscriptionExpires` field instead defines +when the subscription itself expires. -The `subscriptionExpires` value MUST be strictly later than the -challenge `expires` timestamp. Servers MUST reject credentials where -`subscriptionExpires` is at or before the challenge `expires`. +If the challenge includes `expires`, the `subscriptionExpires` value +MUST be strictly later than the challenge `expires` timestamp. Servers +MUST reject credentials where `subscriptionExpires` is at or before the +challenge `expires`. The first billing period begins when the subscription is activated. Payment methods MAY define additional activation controls in @@ -247,7 +258,7 @@ payment method specification. "subscriptionExpires": "2026-01-01T00:00:00Z", "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00", "methodDetails": { - "chainId": 42431 + "chainId": 4217 } } ~~~ @@ -287,7 +298,8 @@ When the server receives a "subscription" credential, it MUST: 2. Activate the subscription 3. Collect the first billing-period charge 4. Initialize durable subscription state for later renewals -5. Return success (200) with a `Payment-Receipt` for the first charge +5. Return success (200) with a `Payment-Receipt` for the first charge, + including a `subscriptionId` ## Renewal @@ -301,6 +313,25 @@ atomically with, delivering the corresponding service. Servers MUST NOT collect more than one renewal charge for the same billing period. +## Reauthentication + +After successful activation, the server MUST return a `subscriptionId` +in the `Payment-Receipt`. The value MUST be a base64url {{RFC4648}} +string without padding and MUST be unique within the server's +subscription namespace. + +Clients SHOULD retain the `subscriptionId` and, when intending to use an +existing subscription on a later request, SHOULD send it in the +`Subscription-Id` request header. + +If a request is associated with an existing subscription, the server MAY +echo that identifier in the challenge `request.subscriptionId` field to +bind the challenge to the intended subscription. + +Servers MUST authenticate or otherwise authorize the client's use of the +identified subscription before granting access or collecting a renewal +charge. + ## Server Accounting and Idempotency Servers MUST maintain durable subscription state sufficient to enforce @@ -391,6 +422,14 @@ receipts. # IANA Considerations +## Header Field Registration + +This document registers the following header fields: + +| Field Name | Status | Reference | +|------------|--------|-----------| +| `Subscription-Id` | permanent | This document | + ## Payment Intent Registration This document registers the "subscription" intent in the "HTTP Payment diff --git a/specs/methods/tempo/draft-tempo-subscription-00.md b/specs/methods/tempo/draft-tempo-subscription-00.md index 729c2981..8e67e3f0 100644 --- a/specs/methods/tempo/draft-tempo-subscription-00.md +++ b/specs/methods/tempo/draft-tempo-subscription-00.md @@ -41,24 +41,11 @@ normative: author: - name: Jake Moxey date: 2026-04 - -informative: - EIP-55: - title: "Mixed-case checksum address encoding" - target: https://eips.ethereum.org/EIPS/eip-55 - author: - - name: Vitalik Buterin - date: 2016-01 TEMPO-ACCOUNT-KEYCHAIN: title: "Account Keychain Precompile" target: https://docs.tempo.xyz/protocol/precompiles/account-keychain author: - org: Tempo Labs - TEMPO-TX-SPEC: - title: "Tempo Transaction Specification" - target: https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction - author: - - org: Tempo Labs TIP-1020: title: "TIP-1020: Signature Verification Precompile" target: https://docs.tempo.xyz/protocol/tips/tip-1020 @@ -167,6 +154,7 @@ base64url-encoded without padding per {{I-D.httpauth-payment}}. | `recipient` | string | REQUIRED | Recipient address authorized for subscription charges | | `description` | string | OPTIONAL | Human-readable subscription description | | `externalId` | string | OPTIONAL | Merchant's reference for the subscription | +| `subscriptionId` | string | OPTIONAL | Server-issued opaque identifier for an existing subscription | The `amount` value MUST be a string representation of a positive integer in base 10 with no sign, decimal point, exponent, or @@ -180,17 +168,18 @@ surrounding whitespace. Leading zeros MUST NOT be used. | Field | Type | Required | Description | |-------|------|----------|-------------| -| `methodDetails.chainId` | number | OPTIONAL | Tempo chain ID. If omitted, the default value is 42431 (Tempo mainnet). | +| `methodDetails.chainId` | number | OPTIONAL | Tempo chain ID. If omitted, the default value is 4217 (Tempo mainnet). | -Challenge expiry is conveyed by the `expires` auth-param in -`WWW-Authenticate` per {{I-D.httpauth-payment}}, using {{RFC3339}} -format. Request objects MUST NOT duplicate the challenge expiry value. -The `subscriptionExpires` field instead defines when the subscription -itself expires. +Servers issuing `intent="subscription"` challenges SHOULD include the +`expires` auth-param in `WWW-Authenticate` per {{I-D.httpauth-payment}}, +using {{RFC3339}} format. Request objects MUST NOT duplicate the +challenge expiry value. The `subscriptionExpires` field instead defines +when the subscription itself expires. -The `subscriptionExpires` value MUST be strictly later than the -challenge `expires` timestamp. Servers MUST reject credentials where -`subscriptionExpires` is at or before the challenge `expires`. +If the challenge includes `expires`, the `subscriptionExpires` value +MUST be strictly later than the challenge `expires` timestamp. Servers +MUST reject credentials where `subscriptionExpires` is at or before the +challenge `expires`. **Example:** @@ -202,7 +191,7 @@ challenge `expires` timestamp. Servers MUST reject credentials where "subscriptionExpires": "2026-01-01T00:00:00Z", "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00", "methodDetails": { - "chainId": 42431 + "chainId": 4217 } } ~~~ @@ -225,7 +214,7 @@ base64url-encoded JSON object per {{I-D.httpauth-payment}}. |-------|------|----------|-------------| | `challenge` | object | REQUIRED | Echo of the challenge from the server | | `payload` | object | REQUIRED | Tempo-specific payload object | -| `source` | string | OPTIONAL | Payer identifier as a DID (e.g., `did:pkh:eip155:42431:0x...`) | +| `source` | string | OPTIONAL | Payer identifier as a DID (e.g., `did:pkh:eip155:4217:0x...`) | The `source` field, if present, SHOULD use the `did:pkh` method with the chain ID applicable to the challenge and the payer's Ethereum @@ -272,7 +261,7 @@ field. "signature": "0xf8c1...signed authorization bytes...", "type": "keyAuthorization" }, - "source": "did:pkh:eip155:42431:0x1234567890abcdef1234567890abcdef12345678" + "source": "did:pkh:eip155:4217:0x1234567890abcdef1234567890abcdef12345678" } ~~~ @@ -368,6 +357,10 @@ Upon successful activation or renewal, servers MUST return a `Payment-Receipt` header per {{I-D.httpauth-payment}}. Servers MUST NOT include a `Payment-Receipt` header on error responses. +On activation, servers MUST include the `subscriptionId` defined by +{{I-D.payment-intent-subscription}} in the receipt. On renewal, servers +SHOULD return the same `subscriptionId` for the active subscription. + The receipt payload for Tempo subscription: | Field | Type | Description | @@ -375,6 +368,7 @@ The receipt payload for Tempo subscription: | `method` | string | `"tempo"` | | `reference` | string | Transaction hash of the settlement transaction | | `status` | string | `"success"` | +| `subscriptionId` | string | Server-issued opaque identifier for the subscription | | `timestamp` | string | {{RFC3339}} settlement time | | `externalId` | string | OPTIONAL. Echoed from the challenge request | @@ -423,16 +417,9 @@ receipts. # IANA Considerations -## Payment Intent Registration - -This document registers the following payment intent in the "HTTP -Payment Intents" registry established by {{I-D.httpauth-payment}}: - -| Intent | Applicable Methods | Description | Reference | -|--------|-------------------|-------------|-----------| -| `subscription` | `tempo` | Recurring fixed-amount TIP-20 payment | This document | - -Contact: Tempo Labs () +The `subscription` payment intent is registered by +{{I-D.payment-intent-subscription}}. This document does not register it +again. --- back @@ -461,7 +448,7 @@ The `request` decodes to: "subscriptionExpires": "2026-01-01T00:00:00Z", "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00", "methodDetails": { - "chainId": 42431 + "chainId": 4217 } } ~~~ @@ -485,26 +472,10 @@ seconds until 2026-01-01T00:00:00Z. "signature": "0xf8c1...signed authorization bytes...", "type": "keyAuthorization" }, - "source": "did:pkh:eip155:42431:0x1234567890abcdef1234567890abcdef12345678" + "source": "did:pkh:eip155:4217:0x1234567890abcdef1234567890abcdef12345678" } ~~~ -# ABNF Collected - -~~~ abnf -tempo-subscription-challenge = "Payment" 1*SP - "id=" quoted-string "," - "realm=" quoted-string "," - "method=" DQUOTE "tempo" DQUOTE "," - "intent=" DQUOTE "subscription" DQUOTE "," - "request=" base64url-nopad - -tempo-subscription-credential = "Payment" 1*SP base64url-nopad - -; Base64url encoding without padding per RFC 4648 Section 5 -base64url-nopad = 1*( ALPHA / DIGIT / "-" / "_" ) -~~~ - # Acknowledgements The authors thank the MPP community for their feedback on this From 62334f6e06c017607042a7cfd10efc17183c2f3e Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 8 Apr 2026 17:21:13 -0700 Subject: [PATCH 04/16] docs: align tempo subscription with TIP-1011 --- .../tempo/draft-tempo-subscription-00.md | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/specs/methods/tempo/draft-tempo-subscription-00.md b/specs/methods/tempo/draft-tempo-subscription-00.md index 8e67e3f0..6dfadf79 100644 --- a/specs/methods/tempo/draft-tempo-subscription-00.md +++ b/specs/methods/tempo/draft-tempo-subscription-00.md @@ -46,6 +46,11 @@ normative: target: https://docs.tempo.xyz/protocol/precompiles/account-keychain author: - org: Tempo Labs + TIP-1011: + title: "TIP-1011: Enhanced Access Key Permissions" + target: https://docs.tempo.xyz/protocol/tips/tip-1011 + author: + - name: Tanishk Goyal TIP-1020: title: "TIP-1020: Signature Verification Precompile" target: https://docs.tempo.xyz/protocol/tips/tip-1020 @@ -203,6 +208,23 @@ The client fulfills this by signing a key authorization with: - Billing period = `periodSeconds` - Destination restriction = `recipient` +When {{TIP-1011}} is available on the chain identified by the +challenge, the signed key authorization MUST additionally configure: + +- a `TokenLimit` for `currency` whose `amount` equals the challenge + `amount` and whose `period` equals `periodSeconds` +- exactly one `allowed_calls` target scope whose `target` equals + `currency` +- explicit selector rules for `transfer(address,uint256)` + (`0xa9059cbb`) and optionally + `transferWithMemo(address,uint256,bytes32)` (`0x95777d59`) +- a recipient allowlist for each permitted selector containing only the + challenge `recipient` + +The signed key authorization MUST NOT use unrestricted target mode for +the subscription token, and it MUST NOT authorize `approve` or any +other non-transfer selector. + # Credential Schema The credential in the `Authorization` header contains a @@ -235,6 +257,7 @@ least: - the TIP-20 token spending limit - the billing-period limit configuration - the recipient restriction +- the `allowed_calls` scope described above when {{TIP-1011}} is used The embedded signature MUST use a primitive signature type supported by {{TIP-1020}}. Keychain wrapper signatures MUST NOT be used for this @@ -351,6 +374,29 @@ address from the signed key authorization using {{TIP-1020}}-compatible verification semantics over the encoded key authorization payload. +## Authorization Scope Verification + +When validating a Tempo subscription credential, servers MUST verify +that the signed key authorization expiry equals `subscriptionExpires`. +Servers MUST also verify that the authorization contains a spending +limit for `currency` whose amount equals `amount` and whose billing +period equals `periodSeconds`. + +When {{TIP-1011}} is active on the target chain, servers MUST verify +that the signed key authorization's `allowed_calls` scope: + +- contains exactly one target scope, and that scope is for `currency` +- uses explicit selector rules rather than unrestricted target mode +- allows `transfer(address,uint256)` and MAY additionally allow + `transferWithMemo(address,uint256,bytes32)` +- does not allow `approve(address,uint256)` or any other non-transfer + selector +- restricts the first ABI `address` argument for each permitted + selector to the challenge `recipient` + +Servers MUST reject authorizations that permit spending the subscription +token through broader call scopes than those required above. + ## Receipt Generation Upon successful activation or renewal, servers MUST return a @@ -377,8 +423,9 @@ The receipt payload for Tempo subscription: ## Destination Scoping Tempo subscription access keys MUST be restricted to the `recipient` -address in the request. Servers MUST reject credentials that do not -enforce this restriction. +address in the request. Where {{TIP-1011}} recipient-bound selector +rules are available, servers MUST reject credentials that do not +enforce this restriction through `allowed_calls`. ## Amount and Period Verification @@ -405,6 +452,13 @@ but they do not by themselves make HTTP service delivery idempotent to prevent duplicate renewal charges caused by retries or concurrent requests. +## Key Scope Minimization + +Subscription access keys SHOULD use the narrowest {{TIP-1011}} scope +needed to support recurring charges. Implementations SHOULD avoid +unrestricted target scopes and SHOULD limit the key to the subscription +token, the permitted transfer selectors, and the configured recipient. + ## Caching Responses to subscription challenges (402 Payment Required) MUST include From 13345c9aca434735c763fa63234bc964b651b7c0 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 8 Apr 2026 18:33:31 -0700 Subject: [PATCH 05/16] docs: clarify tempo subscription time bounds --- specs/methods/tempo/draft-tempo-subscription-00.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/specs/methods/tempo/draft-tempo-subscription-00.md b/specs/methods/tempo/draft-tempo-subscription-00.md index 6dfadf79..3f9c3afc 100644 --- a/specs/methods/tempo/draft-tempo-subscription-00.md +++ b/specs/methods/tempo/draft-tempo-subscription-00.md @@ -186,6 +186,13 @@ MUST be strictly later than the challenge `expires` timestamp. Servers MUST reject credentials where `subscriptionExpires` is at or before the challenge `expires`. +Tempo subscriptions map `periodSeconds` to the {{TIP-1011}} `TokenLimit` +`period` field and map `subscriptionExpires` to the Tempo key +authorization expiry field. Servers MUST reject request objects where +`periodSeconds` cannot be represented as an unsigned 64-bit integer. +Servers MUST reject request objects where `subscriptionExpires` cannot +be represented in the Tempo key authorization expiry field. + **Example:** ~~~json @@ -364,6 +371,13 @@ When granting access in a later billing period, servers MUST: For duplicate idempotent requests, servers MUST NOT charge the same billing period more than once. +{{TIP-1011}} periodic spending limits reset to one billing period of +capacity and do not accumulate across elapsed periods. If one or more +billing periods elapse without a successful renewal charge, a later +transaction authorizes at most one charge in the then-current billing +period. Servers MUST NOT treat missed billing periods as additional +on-chain spending capacity. + ## Source Verification If a credential includes the optional `source` field, servers MUST NOT From f7822515de2bac120b07b4bea8f4a4cb12d2571d Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 8 Apr 2026 18:47:05 -0700 Subject: [PATCH 06/16] docs: clarify subscription intent lifecycle semantics --- .../draft-payment-intent-subscription-00.md | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/specs/intents/draft-payment-intent-subscription-00.md b/specs/intents/draft-payment-intent-subscription-00.md index 54a34c18..fc1195cc 100644 --- a/specs/intents/draft-payment-intent-subscription-00.md +++ b/specs/intents/draft-payment-intent-subscription-00.md @@ -184,6 +184,10 @@ The `periodSeconds` value MUST be a string representation of a positive integer in base 10 with no sign, decimal point, exponent, or surrounding whitespace. Leading zeros MUST NOT be used. +`periodSeconds` defines fixed-duration billing periods measured in +elapsed seconds. It does not, by itself, encode calendar-month or +calendar-year alignment. + ### Optional Fields | Field | Type | Description | @@ -214,6 +218,10 @@ Payment methods MAY define additional activation controls in `methodDetails`, but MUST define exact activation semantics if they do so. +The billing anchor for a subscription is the time activation succeeds. +Billing periods are contiguous fixed-duration windows derived by adding +`periodSeconds` to that anchor. + ## Currency Formats {#currency-formats} The `currency` field supports multiple formats to accommodate different @@ -313,6 +321,11 @@ atomically with, delivering the corresponding service. Servers MUST NOT collect more than one renewal charge for the same billing period. +If one or more billing periods elapse without a successful renewal +charge, the subscription intent authorizes at most one charge for the +then-current billing period. Servers MUST NOT treat missed billing +periods as automatically accumulated authority for additional charges. + ## Reauthentication After successful activation, the server MUST return a `subscriptionId` @@ -340,8 +353,9 @@ per-period charging rules across retries and concurrent requests. At minimum, servers MUST track: - Subscription identifier -- Current billing period start time -- Whether the current billing period has been charged +- Billing anchor or equivalent current billing-period start time +- Last successfully charged billing-period index, or whether the + current billing period has been charged - Subscription expiry - Cancellation or revocation status @@ -355,8 +369,15 @@ duplicate idempotent request. Payers SHOULD be able to cancel subscriptions before expiry. Cancellation mechanisms are method-specific. -Servers MUST NOT collect renewal charges after cancellation takes -effect. +For an active subscription, cancellation takes effect at the end of the +current paid billing period. Servers MUST continue honoring access +already paid for through the end of that billing period. + +If there is no current paid billing period, cancellation takes effect +immediately. + +Servers MUST NOT collect renewal charges for billing periods after +cancellation takes effect. ## Error Responses @@ -366,7 +387,7 @@ MUST return an appropriate HTTP status code: | Condition | Status Code | Behavior | |-----------|-------------|----------| | Subscription expired | 402 Payment Required | Issue new challenge | -| Subscription cancelled or revoked | 402 Payment Required | Issue new challenge | +| Cancellation effective or authorization revoked | 402 Payment Required | Issue new challenge | | Current billing period unpaid or renewal failed | 402 Payment Required | Issue new challenge | | Invalid credential | 402 Payment Required | Issue new challenge | From 4934bdd5b3c3c18702c468351f141a41d4231079 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 8 Apr 2026 18:52:08 -0700 Subject: [PATCH 07/16] docs: add subscription lifecycle examples --- .../draft-payment-intent-subscription-00.md | 59 ++++++++ .../tempo/draft-tempo-subscription-00.md | 138 +++++++++++++++++- 2 files changed, 196 insertions(+), 1 deletion(-) diff --git a/specs/intents/draft-payment-intent-subscription-00.md b/specs/intents/draft-payment-intent-subscription-00.md index fc1195cc..75d38a7f 100644 --- a/specs/intents/draft-payment-intent-subscription-00.md +++ b/specs/intents/draft-payment-intent-subscription-00.md @@ -396,6 +396,65 @@ header with a fresh challenge. Clients receiving a 402 after a previously valid subscription SHOULD treat the subscription as no longer usable and initiate a new subscription flow. +# Illustrative Lifecycle Examples + +This section is non-normative. + +## Monthly Billing Example + +Suppose a server offers a plan with these request fields: + +- `amount = "9900"` +- `currency = "usd"` +- `periodSeconds = "2592000"` +- `subscriptionExpires = "2026-07-14T12:00:00Z"` + +If activation succeeds at `2026-01-15T12:03:10Z`, that time becomes the +billing anchor. The resulting billing periods are: + +- Period 0: `[2026-01-15T12:03:10Z, 2026-02-14T12:03:10Z)` +- Period 1: `[2026-02-14T12:03:10Z, 2026-03-16T12:03:10Z)` +- Period 2: `[2026-03-16T12:03:10Z, 2026-04-15T12:03:10Z)` + +Activation collects the Period 0 charge. Requests during Period 0 do +not require another renewal charge. When Period 1 begins, the server +may collect one renewal charge for Period 1 before, or atomically with, +granting access for that period. After that renewal succeeds, additional +requests during Period 1 do not permit another charge for Period 1. + +## Cancellation Example + +Suppose the subscription above has already been charged through Period 2 +and the payer cancels on `2026-03-20T09:00:00Z`. + +Cancellation takes effect at the end of the current paid billing period, +which is `2026-04-15T12:03:10Z` in this example. The server continues +honoring access through that time. The server does not collect a +renewal charge for Period 3. A request after +`2026-04-15T12:03:10Z` receives `402 Payment Required` with a fresh +challenge. + +## Failed Renewal Example + +Suppose Period 3 begins and the server attempts the renewal charge for +that period, but the method-specific payment step fails. + +The server does not grant access for the unpaid period and returns +`402 Payment Required` with a fresh challenge. If a later retry during +Period 3 succeeds, the server may then grant access for Period 3. + +If Period 4 begins before any successful charge occurs, the subscription +intent authorizes at most one charge for Period 4. The missed Period 3 +charge does not automatically accumulate into authority to collect both +Period 3 and Period 4. + +## Natural Expiry Example + +Suppose `subscriptionExpires` is `2026-07-14T12:00:00Z`. Once that time +is reached, the server stops treating the subscription as reusable for +future billing periods. Requests after that time receive +`402 Payment Required` with a fresh challenge. + # Security Considerations ## Recurring Charge Awareness diff --git a/specs/methods/tempo/draft-tempo-subscription-00.md b/specs/methods/tempo/draft-tempo-subscription-00.md index 3f9c3afc..b090b268 100644 --- a/specs/methods/tempo/draft-tempo-subscription-00.md +++ b/specs/methods/tempo/draft-tempo-subscription-00.md @@ -491,7 +491,11 @@ again. --- back -# Example +# Examples + +This section is non-normative. + +## Activation **Challenge:** @@ -544,6 +548,138 @@ seconds until 2026-01-01T00:00:00Z. } ~~~ +The activation transaction submitted by the server contains: + +- the signed `keyAuthorization` +- one TIP-20 `transfer(recipient, amount)` call, or + `transferWithMemo(recipient, amount, memo)` if the implementation uses + a memo + +If activation settles at `2026-01-15T12:03:10Z`, the `Payment-Receipt` +payload decodes to: + +~~~json +{ + "method": "tempo", + "reference": "0x8d7c6c0d94d8488cb4cf6ab7b8a2f9c3f8e0eac7e5b6d1e8c3d86f733c2b7c01", + "status": "success", + "subscriptionId": "c3ViXzAxMjM0NTY", + "timestamp": "2026-01-15T12:03:10Z" +} +~~~ + +The server records at least: + +- `subscriptionId = "c3ViXzAxMjM0NTY"` +- `billing anchor = 2026-01-15T12:03:10Z` +- `periodSeconds = 2592000` +- `last charged billing-period index = 0` + +## Renewal Across Multiple Periods + +Using the activation timestamp above, the Tempo subscription billing +periods are: + +- Period 0: `[2026-01-15T12:03:10Z, 2026-02-14T12:03:10Z)` +- Period 1: `[2026-02-14T12:03:10Z, 2026-03-16T12:03:10Z)` +- Period 2: `[2026-03-16T12:03:10Z, 2026-04-15T12:03:10Z)` + +Requests during Period 0 can use the active subscription without a new +authorization: + +~~~http +GET /api/resource HTTP/1.1 +Host: api.example.com +Subscription-Id: c3ViXzAxMjM0NTY +~~~ + +When Period 1 begins, the server determines that billing-period index 1 +has not yet been charged. The server submits one Tempo transaction using +the registered access key to call the TIP-20 token at `currency` with: + +- `transfer(recipient, amount)`, or +- `transferWithMemo(recipient, amount, memo)` + +If that transaction settles successfully, the renewal `Payment-Receipt` +payload decodes to: + +~~~json +{ + "method": "tempo", + "reference": "0xb4bf2b4f8e3f0e6f3b6af3a5f6d3c8e32e1c32a19fa56bd9f9b3fd33af88e912", + "status": "success", + "subscriptionId": "c3ViXzAxMjM0NTY", + "timestamp": "2026-02-14T12:05:42Z" +} +~~~ + +The server updates `last charged billing-period index = 1`. Additional +requests during Period 1 do not permit another successful renewal +charge for Period 1. + +## Cancellation At Period End + +Suppose the server has already successfully charged Period 2 and the +payer cancels on `2026-03-20T09:00:00Z`. + +The server records cancellation with an effective time of +`2026-04-15T12:03:10Z`, which is the end of Period 2. Requests before +that time continue to succeed without another renewal charge: + +~~~http +GET /api/resource HTTP/1.1 +Host: api.example.com +Subscription-Id: c3ViXzAxMjM0NTY +~~~ + +Once `2026-04-15T12:03:10Z` is reached, the server stops submitting +renewal transactions for this subscription. A later request receives a +fresh challenge: + +~~~http +HTTP/1.1 402 Payment Required +Cache-Control: no-store +WWW-Authenticate: Payment id="n3xtP3ri0d", + realm="api.example.com", + method="tempo", + intent="subscription", + request="" +~~~ + +## Failed Renewal And Lapse + +Suppose Period 3 begins and the server attempts a renewal transaction, +but the Tempo transaction fails because the payer no longer has enough +TIP-20 balance or fee-paying balance. + +The server does not grant access for Period 3 and returns: + +~~~http +HTTP/1.1 402 Payment Required +Cache-Control: no-store +WWW-Authenticate: Payment id="r3tryP3ri0d", + realm="api.example.com", + method="tempo", + intent="subscription", + request="" +~~~ + +If a later retry during Period 3 succeeds, the server may grant access +for Period 3 and update `last charged billing-period index = 3`. + +If Period 4 begins before any successful renewal occurs, the next +successful Tempo transaction authorizes at most one charge for Period 4. +The elapsed unpaid Period 3 does not become extra on-chain spending +capacity, because {{TIP-1011}} periodic limits reset rather than +accumulate. + +## Natural Expiry + +Suppose `subscriptionExpires` is `2026-07-14T12:00:00Z`. Once that time +is reached, the signed key authorization no longer authorizes future +renewals. Requests after that time receive a fresh challenge rather than +another renewal attempt. + # Acknowledgements The authors thank the MPP community for their feedback on this From 4a029f75f6fc770877851a5d4c7eed137fc917f8 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Fri, 10 Apr 2026 13:24:29 -0700 Subject: [PATCH 08/16] docs: refine subscription intent and add Stripe profile --- .../draft-payment-intent-subscription-00.md | 80 ++- .../stripe/draft-stripe-subscription-00.md | 495 ++++++++++++++++++ .../tempo/draft-tempo-subscription-00.md | 20 +- 3 files changed, 575 insertions(+), 20 deletions(-) create mode 100644 specs/methods/stripe/draft-stripe-subscription-00.md diff --git a/specs/intents/draft-payment-intent-subscription-00.md b/specs/intents/draft-payment-intent-subscription-00.md index 75d38a7f..42ac54b9 100644 --- a/specs/intents/draft-payment-intent-subscription-00.md +++ b/specs/intents/draft-payment-intent-subscription-00.md @@ -68,7 +68,14 @@ and other services with a stable price per billing period. Payment methods implement "subscription" using method-specific recurring authorization mechanisms. This document defines the abstract semantics and shared request fields. Payment method specifications define how -those semantics are enforced. +those semantics are enforced, which request shapes they support, and +which requests they reject because they cannot be represented exactly on +the underlying payment network. + +Payment method specifications MAY intentionally define a constrained +subset of a richer underlying subscription system. A method MUST either +preserve the semantics in this document exactly or reject the request; +it MUST NOT approximate them. # Requirements Language @@ -165,7 +172,8 @@ JSON MUST be serialized using JSON Canonicalization Scheme (JCS) All payment methods implementing the "subscription" intent MUST support these shared fields. Payment methods MAY elevate OPTIONAL fields to -REQUIRED in their method specification. +REQUIRED in their method specification, and MUST document any narrower +supported subset. ### Required Fields @@ -188,6 +196,11 @@ surrounding whitespace. Leading zeros MUST NOT be used. elapsed seconds. It does not, by itself, encode calendar-month or calendar-year alignment. +Payment methods MUST reject request objects whose `periodSeconds` value +they cannot represent exactly. They MUST NOT approximate the requested +period by rounding, truncating, or substituting a nearby network-native +cadence. + ### Optional Fields | Field | Type | Description | @@ -222,6 +235,13 @@ The billing anchor for a subscription is the time activation succeeds. Billing periods are contiguous fixed-duration windows derived by adding `periodSeconds` to that anchor. +The shared fields in this section are the canonical subscription +contract. Payment method specifications MUST document how they map +`amount`, `periodSeconds`, `subscriptionExpires`, and activation to the +underlying payment system. If a payment method cannot represent those +fields or semantics exactly, it MUST reject the request rather than +approximate it. + ## Currency Formats {#currency-formats} The `currency` field supports multiple formats to accommodate different @@ -242,6 +262,37 @@ Payment methods MAY define additional fields in the `methodDetails` object. These fields are method-specific and MUST be documented in the payment method specification. +## Implementor Guidance + +This section is non-normative. + +Payment method authors should treat the shared `subscription` intent as +the canonical interoperable contract between clients and servers. A +method specification may intentionally define a narrower profile of its +underlying payment system, but it should do so explicitly and fail +closed. + +In particular: + +- Methods should support only request shapes they can represent exactly. +- Methods should document the supported and rejected ranges or values of + `periodSeconds`, how `subscriptionExpires` is enforced, and what + conditions make activation succeed. +- Activation should not be reported as successful until both + subscription setup and the first billing-period charge have + succeeded. +- Methods should preserve the shared invariants of one successful charge + per billing period, no automatic accumulation of missed periods, and + no renewals after expiry. +- Richer network-native features such as trials, prorations, + discounts, metered billing, pause or resume controls, quantity + changes, plan changes, or open-ended renewals should be disabled or + rejected unless the method specification defines an exact mapping that + preserves the shared semantics. +- Implementations should maintain durable server state sufficient to + prevent duplicate charges across retries, concurrent requests, and + out-of-band network events. + ## Examples ### Traditional Payment Processor @@ -312,7 +363,8 @@ When the server receives a "subscription" credential, it MUST: ## Renewal For each later billing period, the server MAY collect one renewal -charge for `amount` using the method-specific recurring authorization. +charge for `amount` using the method-specific recurring authorization +flow. If the server grants access for a later billing period, it MUST ensure that the renewal charge for that period has been collected before, or @@ -326,6 +378,10 @@ charge, the subscription intent authorizes at most one charge for the then-current billing period. Servers MUST NOT treat missed billing periods as automatically accumulated authority for additional charges. +Payment method specifications define the concrete renewal, retry, +recovery, and cancellation mechanisms, but they MUST preserve the +invariants in this section. + ## Reauthentication After successful activation, the server MUST return a `subscriptionId` @@ -334,16 +390,19 @@ string without padding and MUST be unique within the server's subscription namespace. Clients SHOULD retain the `subscriptionId` and, when intending to use an -existing subscription on a later request, SHOULD send it in the +existing subscription on a later request, MAY send it in the `Subscription-Id` request header. +The `Subscription-Id` header is only a subscription-selection hint. It +does not, by itself, prove authority to use the subscription. + If a request is associated with an existing subscription, the server MAY echo that identifier in the challenge `request.subscriptionId` field to bind the challenge to the intended subscription. Servers MUST authenticate or otherwise authorize the client's use of the identified subscription before granting access or collecting a renewal -charge. +charge. A matching `Subscription-Id` alone is insufficient. ## Server Accounting and Idempotency @@ -367,14 +426,9 @@ duplicate idempotent request. ## Cancellation Payers SHOULD be able to cancel subscriptions before expiry. -Cancellation mechanisms are method-specific. - -For an active subscription, cancellation takes effect at the end of the -current paid billing period. Servers MUST continue honoring access -already paid for through the end of that billing period. - -If there is no current paid billing period, cancellation takes effect -immediately. +Cancellation mechanisms, effective-time rules, and any continued access +for already-paid service are method-specific and MUST be documented by +the payment method or application profile. Servers MUST NOT collect renewal charges for billing periods after cancellation takes effect. diff --git a/specs/methods/stripe/draft-stripe-subscription-00.md b/specs/methods/stripe/draft-stripe-subscription-00.md new file mode 100644 index 00000000..7166b6dc --- /dev/null +++ b/specs/methods/stripe/draft-stripe-subscription-00.md @@ -0,0 +1,495 @@ +--- +title: Stripe subscription Intent for HTTP Payment Authentication +abbrev: Stripe Subscription +docname: draft-stripe-subscription-00 +version: 00 +category: info +ipr: noModificationTrust200902 +submissiontype: IETF +consensus: true + +author: + - name: Brendan Ryan + ins: B. Ryan + email: brendan@tempo.xyz + organization: Tempo Labs + - name: Steve Kaliski + ins: S. Kaliski + email: stevekaliski@stripe.com + organization: Stripe + +normative: + RFC2119: + RFC3339: + RFC8174: + RFC8785: + I-D.httpauth-payment: + title: "The 'Payment' HTTP Authentication Scheme" + target: https://datatracker.ietf.org/doc/draft-ietf-httpauth-payment/ + author: + - name: Jake Moxey + date: 2026-01 + I-D.payment-intent-subscription: + title: "Subscription Intent for HTTP Payment Authentication" + target: https://datatracker.ietf.org/doc/draft-payment-intent-subscription/ + author: + - name: Jake Moxey + date: 2026-04 + +informative: + STRIPE-BILLING-OVERVIEW: + target: https://docs.stripe.com/billing/subscriptions/overview + title: How subscriptions work + author: + - org: Stripe, Inc. + STRIPE-BILLING-CANCEL: + target: https://docs.stripe.com/billing/subscriptions/cancel + title: Cancel subscriptions + author: + - org: Stripe, Inc. + STRIPE-BILLING-WEBHOOKS: + target: https://docs.stripe.com/billing/subscriptions/webhooks + title: Using webhooks with subscriptions + author: + - org: Stripe, Inc. +--- + +--- abstract + +This document defines the `subscription` intent for the `stripe` +payment method within the Payment HTTP Authentication Scheme +{{I-D.httpauth-payment}}. It specifies a constrained Stripe Billing +profile for fixed-price, bounded recurring subscriptions whose +activation succeeds only after the first invoice is paid. + +--- middle + +# Introduction + +This specification defines the `subscription` intent for use with the +`stripe` payment method in the Payment HTTP Authentication Scheme +{{I-D.httpauth-payment}}. It profiles Stripe Billing as a narrow, +canonical mapping of the shared `subscription` intent defined in +{{I-D.payment-intent-subscription}}. + +This document is intentionally not a specification for all Stripe +subscription features. Stripe Billing supports richer behaviors such as +trials, prorations, discounts, usage-based billing, and flexible +schedule changes. This method supports only the subset that preserves +the shared subscription semantics exactly. Servers MUST reject request +objects or Stripe configurations that would broaden those semantics. + +## Stripe Subscription Flow + +The following diagram illustrates the Stripe subscription flow: + +~~~ + Client Server Stripe + | | | + | (1) GET /resource | | + |----------------------------> | | + | | | + | (2) 402 Payment Required | | + | intent="subscription" | | + |<----------------------------- | | + | | | + | (3) Collect payment method | | + | and create credential | | + | | | + | (4) Authorization: Payment | | + |----------------------------> | | + | | | + | | (5) Create or reuse | + | | customer, price, and | + | | subscription | + | |----------------------------> | + | | | + | | (6) First invoice paid | + | |<---------------------------- | + | | | + | (7) 200 OK + Receipt | | + |<---------------------------- | | + | | | + | ... later period ... | | + | | | + | | (8) Renewal invoice paid | + | | and recorded | + | |<---------------------------- | + | | | + | (9) 200 OK + Receipt | | + |<---------------------------- | | + | | | +~~~ + +# Requirements Language + +{::boilerplate bcp14-tagged} + +# Terminology + +Stripe Customer +: A Stripe object representing the payer for a subscription. + +Stripe Price +: A Stripe object that defines the fixed recurring amount, currency, + and cadence for a subscription item. + +Stripe Subscription +: A Stripe Billing object representing the recurring commercial + relationship. In this profile it MUST contain exactly one fixed-price + recurring item. + +First Invoice +: The initial Stripe invoice created for the subscription at activation + time. Activation succeeds only after this invoice is paid. + +# Request Schema + +The `request` parameter in the `WWW-Authenticate` challenge contains a +base64url-encoded JSON object. The `request` JSON MUST be serialized +using JSON Canonicalization Scheme (JCS) {{RFC8785}} and +base64url-encoded without padding per {{I-D.httpauth-payment}}. + +## Shared Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `amount` | string | REQUIRED | Fixed payment amount per billing period in the currency's smallest unit | +| `currency` | string | REQUIRED | Lowercase ISO 4217 currency code | +| `periodSeconds` | string | REQUIRED | Billing period duration in seconds | +| `subscriptionExpires` | string | REQUIRED | Subscription expiry timestamp in {{RFC3339}} format | +| `description` | string | OPTIONAL | Human-readable subscription description | +| `externalId` | string | OPTIONAL | Merchant's reference for the subscription | +| `subscriptionId` | string | OPTIONAL | Server-issued opaque identifier for an existing subscription | +| `recipient` | string | MUST NOT | This profile identifies the merchant by the challenged Stripe account and `methodDetails.networkId`, not by a request-native recipient field | + +The `amount` value MUST be a string representation of a positive +integer in base 10 with no sign, decimal point, exponent, or +surrounding whitespace. Leading zeros MUST NOT be used. + +The `periodSeconds` value MUST be a string representation of a positive +integer in base 10 with no sign, decimal point, exponent, or +surrounding whitespace. Leading zeros MUST NOT be used. + +Servers MUST reject request objects that include `recipient`. + +## Method Details + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `methodDetails.networkId` | string | REQUIRED | Stripe Business Network Profile ID for the challenged merchant | +| `methodDetails.paymentMethodTypes` | []string | REQUIRED | Stripe payment method types accepted for the first invoice | +| `methodDetails.metadata` | object | OPTIONAL | Merchant-defined metadata to attach to Stripe objects | + +**Example:** + +~~~json +{ + "amount": "5000", + "currency": "usd", + "periodSeconds": "604800", + "subscriptionExpires": "2026-03-12T12:03:10Z", + "description": "Weekly Pro plan", + "externalId": "sub_12345", + "methodDetails": { + "networkId": "profile_1MqDcVKA5fEO2tZvKQm9g8Yj", + "paymentMethodTypes": ["card", "link"] + } +} +~~~ + +## Constrained Stripe Billing Profile + +This method defines a constrained profile of Stripe Billing. Servers +MUST either implement this profile exactly or reject the request. + +Servers MUST create or reuse exactly one Stripe Customer and exactly one +Stripe Subscription containing exactly one recurring Stripe Price. The +Price MUST have a fixed `unit_amount`, fixed `currency`, and fixed +recurring cadence for the full life of the subscription. + +The `periodSeconds` field MUST map exactly to a Stripe recurring cadence +using one of the following forms: + +- `week`, where `periodSeconds = interval_count * 604800` +- `day`, where `periodSeconds = interval_count * 86400` + +If `periodSeconds` is divisible by both values, servers SHOULD prefer +the `week` representation. Servers MUST reject any `periodSeconds` +value that would require approximation, calendar-month interpretation, +calendar-year interpretation, or an unsupported Stripe interval count. + +`subscriptionExpires` MUST define a bounded subscription lifetime. +Servers MUST configure Stripe so that no renewal may occur after +`subscriptionExpires`, typically by setting Stripe's `cancel_at` field +{{STRIPE-BILLING-CANCEL}}. In this profile, `subscriptionExpires` MUST +fall on a canonical billing-period boundary derived from the activation +anchor and `periodSeconds`. Servers MUST reject any request whose +expiry would require a prorated invoice, a partial final billing +period, or any other amount or timing change relative to the shared +subscription intent. + +This profile supports only a fixed quantity of 1 for the single +subscription item. Servers MUST reject any request or server-side +configuration that would vary quantity during the active lifetime of the +subscription. + +## Unsupported Stripe Billing Features + +Servers implementing this profile MUST disable or reject the following +features: + +- free trials +- paid trials +- prorations +- discounts or coupons +- automatic tax +- usage-based billing +- metered add-ons +- mid-cycle plan changes +- quantity changes during an active subscription +- pause or resume controls +- open-ended subscriptions + +# Credential Schema + +The Payment credential is a base64url-encoded JSON object containing +`challenge` and `payload` fields per {{I-D.httpauth-payment}}. For +Stripe subscription, the `payload` object contains the following fields: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `paymentMethod` | string | REQUIRED | Stripe PaymentMethod ID to use for the first invoice and future recurring charges | +| `customer` | string | OPTIONAL | Existing Stripe Customer ID if the merchant already has one for the payer | +| `externalId` | string | OPTIONAL | Client's reference ID | + +The `paymentMethod` MUST reference a Stripe PaymentMethod whose type is +included in `methodDetails.paymentMethodTypes` and which is suitable for +future off-session recurring charges under the challenged Stripe +account. + +**Example:** + +~~~json +{ + "paymentMethod": "pm_1Qabc32eZvKYlo2C7b8H1234", + "customer": "cus_S7x1Pq5R9n2Lm4", + "externalId": "client_sub_789" +} +~~~ + +# Verification Procedure + +Servers MUST verify Payment credentials for Stripe subscription intent: + +1. Verify the challenge ID matches the one issued +2. Verify the challenge has not expired +3. Decode the request object and verify it matches this constrained + profile, including exact `periodSeconds` and `subscriptionExpires` + support +4. Extract the `paymentMethod` and optional `customer` from the + credential payload +5. Verify the Stripe PaymentMethod exists, is reusable by the + challenged merchant, and has a type allowed by the challenge +6. Verify the credential has not been replayed for the same challenge + +Servers MUST complete challenge validation before creating or mutating +Stripe objects. + +# Settlement Procedure + +## Activation and First-Period Charge + +For `intent="subscription"`, the server MUST: + +1. Create or reuse a Stripe Customer for the payer +2. Attach or select the challenged `paymentMethod` for that Customer +3. Create or reuse a Stripe Price whose amount, currency, and recurring + cadence exactly match the request +4. Create a Stripe Subscription with exactly one recurring item, + quantity 1, no unsupported features, and bounded expiry at + `subscriptionExpires` +5. Treat activation as successful only after the first invoice for that + subscription is paid +6. Initialize durable local subscription state for later renewals +7. Return success (200) with a `Payment-Receipt` for the first invoice, + including a `subscriptionId` + +Servers MUST NOT treat the subscription as active, grant access, or +return a success receipt while the first invoice is unpaid, requires +additional customer action, or remains incomplete. + +If the first invoice requires an immediate customer confirmation step, +the implementation MAY complete that step using Stripe-native flows, but +the HTTP subscription activation remains incomplete until the first +invoice is paid. + +The canonical billing anchor for this profile is the start timestamp of +the first paid Stripe invoice period. Servers MUST use that anchor when +mapping later Stripe invoices to the shared `periodSeconds` billing +periods. + +## Renewal + +Later billing periods are fulfilled by Stripe renewal invoices. Servers +MUST use durable local state to map Stripe invoices and webhook events +onto canonical billing periods derived from the activation anchor and +`periodSeconds`. + +Servers MUST treat a later billing period as paid only after they +observe a successful paid Stripe invoice for that subscription and +record that canonical billing period durably. + +Servers MUST NOT grant more than one newly paid billing period because +of duplicate webhooks, retries, concurrent requests, or later +collection of older unpaid invoices. If a Stripe recovery or retry flow +cannot be mapped exactly to the shared one-charge-per-period invariant, +servers MUST disable that flow or reject the request. + +Once `subscriptionExpires` is reached, servers MUST stop treating the +Stripe subscription as authority for additional renewals, even if Stripe +later reports a paid invoice. + +## Receipt Generation + +Upon successful activation or renewal, servers MUST return a +`Payment-Receipt` header per {{I-D.httpauth-payment}}. Servers MUST NOT +include a `Payment-Receipt` header on error responses. + +The receipt payload for Stripe subscription: + +| Field | Type | Description | +|-------|------|-------------| +| `method` | string | `"stripe"` | +| `reference` | string | Stripe invoice ID whose successful payment activated or renewed the subscription | +| `status` | string | `"success"` | +| `subscriptionId` | string | Server-issued opaque identifier for the subscription | +| `stripeSubscription` | string | Stripe subscription ID | +| `timestamp` | string | {{RFC3339}} time the invoice was recorded as paid | +| `externalId` | string | OPTIONAL. Echoed from the challenge request | + +# Security Considerations + +## Reject Unsupported Features + +Stripe Billing supports features whose semantics are broader than the +shared `subscription` intent. Servers MUST reject or disable those +features rather than silently approximating the requested subscription. + +## Invoice Status Versus Access + +Servers MUST NOT grant access based only on a Stripe subscription's +high-level status. Stripe can report an `active` subscription while +other invoices remain open or while retry logic is still in progress +{{STRIPE-BILLING-OVERVIEW}}. Access decisions MUST use the canonical +per-period accounting required by +{{I-D.payment-intent-subscription}} together with successfully paid +invoices. + +## Webhook Authenticity and Ordering + +Implementations using Stripe webhooks MUST verify webhook authenticity, +handle duplicate deliveries safely, and tolerate out-of-order event +arrival {{STRIPE-BILLING-WEBHOOKS}}. + +## Duplicate Charge Prevention + +Stripe invoices and webhooks do not by themselves guarantee that the +same HTTP billing period will be applied only once. Servers MUST keep +durable local state sufficient to prevent duplicate activation or +renewal accounting across retries, concurrent requests, and webhook +replays. + +# IANA Considerations + +The `subscription` payment intent is registered by +{{I-D.payment-intent-subscription}}. This document does not register it +again. + +--- back + +# Examples + +This section is non-normative. + +## Activation + +**Challenge:** + +~~~http +HTTP/1.1 402 Payment Required +Cache-Control: no-store +WWW-Authenticate: Payment id="qT8wErYuI3oPlKjH6gFdSa", + realm="api.example.com", + method="stripe", + intent="subscription", + expires="2026-01-15T12:05:00Z", + request="" +~~~ + +The `request` decodes to: + +~~~json +{ + "amount": "5000", + "currency": "usd", + "periodSeconds": "604800", + "subscriptionExpires": "2026-03-12T12:03:10Z", + "description": "Weekly Pro plan", + "methodDetails": { + "networkId": "profile_1MqDcVKA5fEO2tZvKQm9g8Yj", + "paymentMethodTypes": ["card", "link"] + } +} +~~~ + +**Credential payload:** + +~~~json +{ + "paymentMethod": "pm_1Qabc32eZvKYlo2C7b8H1234", + "customer": "cus_S7x1Pq5R9n2Lm4" +} +~~~ + +The server creates or reuses a Stripe Customer, creates or reuses a +weekly fixed-price Stripe Price, creates a bounded Stripe Subscription, +and waits for the first invoice to be paid. Once Stripe reports the +first invoice as paid, the `Payment-Receipt` payload decodes to: + +~~~json +{ + "method": "stripe", + "reference": "in_1QabdK2eZvKYlo2C0L9n4321", + "status": "success", + "subscriptionId": "c3ViX3N0cmlwZV8wMQ", + "stripeSubscription": "sub_1Qabd52eZvKYlo2CgP0Lm789", + "timestamp": "2026-01-15T12:03:10Z" +} +~~~ + +## Rejected Unsupported Cadence + +If a request uses a `periodSeconds` value that cannot be represented as +an exact whole number of Stripe `day` or `week` intervals, the server +rejects it rather than approximating. For example, the following request +is invalid for this profile because `90000` seconds is not an exact +whole number of days or weeks: + +~~~json +{ + "amount": "5000", + "currency": "usd", + "periodSeconds": "90000", + "subscriptionExpires": "2026-03-12T12:03:10Z", + "methodDetails": { + "networkId": "profile_1MqDcVKA5fEO2tZvKQm9g8Yj", + "paymentMethodTypes": ["card"] + } +} +~~~ + +# Acknowledgements + +The authors thank the MPP community for their feedback on this +specification. diff --git a/specs/methods/tempo/draft-tempo-subscription-00.md b/specs/methods/tempo/draft-tempo-subscription-00.md index b090b268..e3d7707f 100644 --- a/specs/methods/tempo/draft-tempo-subscription-00.md +++ b/specs/methods/tempo/draft-tempo-subscription-00.md @@ -84,6 +84,11 @@ Tempo transactions containing standalone `approve` calls and push-mode hash credentials do not provide the per-period enforcement required for this intent. +Tempo subscriptions also require the {{TIP-1011}} periodic token-limit +and `allowed_calls` restrictions described in this document. Servers +MUST reject request objects on chains or deployments that cannot enforce +those restrictions. + ## Subscription Flow The following diagram illustrates the Tempo subscription flow: @@ -215,8 +220,7 @@ The client fulfills this by signing a key authorization with: - Billing period = `periodSeconds` - Destination restriction = `recipient` -When {{TIP-1011}} is available on the chain identified by the -challenge, the signed key authorization MUST additionally configure: +The signed key authorization MUST additionally configure: - a `TokenLimit` for `currency` whose `amount` equals the challenge `amount` and whose `period` equals `periodSeconds` @@ -264,7 +268,7 @@ least: - the TIP-20 token spending limit - the billing-period limit configuration - the recipient restriction -- the `allowed_calls` scope described above when {{TIP-1011}} is used +- the `allowed_calls` scope described above The embedded signature MUST use a primitive signature type supported by {{TIP-1020}}. Keychain wrapper signatures MUST NOT be used for this @@ -341,8 +345,10 @@ same billing period. ## Billing Anchor and Subscription State -The billing anchor for a Tempo subscription is the settlement time of -the successful activation transaction. +The billing anchor for a Tempo subscription is the block timestamp, or +equivalent consensus settlement timestamp, of the block containing the +successful activation transaction. Servers MUST derive this anchor from +chain settlement data rather than local wall-clock time. Billing periods are defined as: @@ -396,8 +402,8 @@ Servers MUST also verify that the authorization contains a spending limit for `currency` whose amount equals `amount` and whose billing period equals `periodSeconds`. -When {{TIP-1011}} is active on the target chain, servers MUST verify -that the signed key authorization's `allowed_calls` scope: +Servers MUST verify that the signed key authorization's `allowed_calls` +scope: - contains exactly one target scope, and that scope is for `currency` - uses explicit selector rules rather than unrestricted target mode From 9dca692429f976dae1afe8619bd7fb4e3826f0e1 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Sun, 12 Apr 2026 15:18:44 -0700 Subject: [PATCH 09/16] docs: resolve review notes, add T3 requirement and access key isolation guidance - Remove placeholder review notes from intent and tempo method specs - Add T3 network upgrade requirement for TIP-1011 features - Rename 'Tempo Network' to 'Tempo' in ASCII diagrams - Add Access Key Isolation section: servers SHOULD use one key per subscription for fault isolation; documents risks of key reuse including shared TokenLimit, wider blast radius, and bulk revocation --- .../draft-payment-intent-subscription-00.md | 2 +- .../tempo/draft-tempo-subscription-00.md | 35 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/specs/intents/draft-payment-intent-subscription-00.md b/specs/intents/draft-payment-intent-subscription-00.md index 42ac54b9..f90b9482 100644 --- a/specs/intents/draft-payment-intent-subscription-00.md +++ b/specs/intents/draft-payment-intent-subscription-00.md @@ -114,7 +114,7 @@ Subscription Identifier The "subscription" intent represents a request for a recurring fixed-amount payment of `amount`, charged once per billing period until -`subscriptionExpires` or cancellation. +`subscriptionExpires` or explicit cancellation. ## Properties diff --git a/specs/methods/tempo/draft-tempo-subscription-00.md b/specs/methods/tempo/draft-tempo-subscription-00.md index e3d7707f..c517d3e5 100644 --- a/specs/methods/tempo/draft-tempo-subscription-00.md +++ b/specs/methods/tempo/draft-tempo-subscription-00.md @@ -89,12 +89,18 @@ and `allowed_calls` restrictions described in this document. Servers MUST reject request objects on chains or deployments that cannot enforce those restrictions. +The {{TIP-1011}} features required by this specification — periodic +spending limits, `allowed_calls` target and selector scoping, and +recipient-bound selector rules — are introduced in the Tempo T3 network +upgrade. Servers MUST NOT issue `intent="subscription"` challenges on +chains or deployments running a pre-T3 protocol version. + ## Subscription Flow The following diagram illustrates the Tempo subscription flow: ~~~ - Client Server Tempo Network + Client Server Tempo │ │ │ │ (1) GET /api/resource │ │ │--------------------------> │ │ @@ -307,7 +313,7 @@ For `intent="subscription"`, activation and the first billing-period charge are a single atomic operation: ~~~ - Client Server Tempo Network + Client Server Tempo | | | | (1) Authorization: | | | Payment | | @@ -479,6 +485,31 @@ needed to support recurring charges. Implementations SHOULD avoid unrestricted target scopes and SHOULD limit the key to the subscription token, the permitted transfer selectors, and the configured recipient. +## Access Key Isolation + +Servers SHOULD generate a unique key pair and use a distinct access key +for each subscription. This provides fault isolation: compromise of one +server-held key affects only the subscription associated with that key, +and revoking one subscription's key via `revokeKey()` does not +invalidate other active subscriptions between the same payer and server. + +If a server reuses a single access key across multiple subscriptions +from the same payer, the key's permissions must be broad enough to cover +all active subscriptions — potentially spanning multiple tokens, +recipients, or spending limits. This widens the blast radius if the key +is compromised and forces revocation of all subscriptions at once. It +also complicates spending-limit accounting, since {{TIP-1011}} enforces +a single `TokenLimit` per `(account, key, token)` tuple: two +subscriptions for the same token on the same key would share one +periodic limit rather than being independently capped. + +Servers that reuse keys across subscriptions MUST ensure the combined +`TokenLimit` and `allowed_calls` scope still satisfies the per- +subscription authorization scope verification requirements in this +document. In practice this is difficult to guarantee, and +implementations SHOULD prefer one key per subscription for simplicity +and security. + ## Caching Responses to subscription challenges (402 Payment Required) MUST include From 187c251ddb24085d9bd38f6cc83bd1134dfd9296 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 15 Apr 2026 12:13:55 -0700 Subject: [PATCH 10/16] docs: clarify subscription intent scope and expiry --- .../draft-payment-intent-subscription-00.md | 110 +++++++++++------- .../stripe/draft-stripe-subscription-00.md | 52 ++++++--- .../tempo/draft-tempo-subscription-00.md | 32 +++-- 3 files changed, 126 insertions(+), 68 deletions(-) diff --git a/specs/intents/draft-payment-intent-subscription-00.md b/specs/intents/draft-payment-intent-subscription-00.md index f90b9482..1b6308c9 100644 --- a/specs/intents/draft-payment-intent-subscription-00.md +++ b/specs/intents/draft-payment-intent-subscription-00.md @@ -48,8 +48,10 @@ normative: This document defines the "subscription" payment intent for use with the Payment HTTP Authentication Scheme. The "subscription" intent represents a recurring fixed-amount payment where the payer grants the -server permission to charge the same amount once per billing period -until a specified expiry time. +server permission to charge the same amount once per billing period. +It standardizes the recurring payment authorization itself, not the full +billing relationship that many application-level systems also call a +subscription. --- middle @@ -58,11 +60,25 @@ until a specified expiry time. The "subscription" intent enables recurring fixed-amount payments. A successful subscription activation creates an authorization for the server to collect the same payment amount once per billing period until -the subscription expires or is cancelled. +the payer cancels it or the authorization otherwise becomes invalid. This intent is useful for recurring API plans, content subscriptions, and other services with a stable price per billing period. +This document intentionally standardizes the payment agreement, not the +entire billing system around it. In particular, the shared intent does +not define price catalogs, quantities or seat counts, plan swaps, +prorations, deferred starts, billing-cycle realignment, invoice state, +or other product-management behavior that many billing platforms also +associate with a "subscription". Those concerns belong to the +application layer or to a narrower payment-method profile. + +This is a deliberate trade-off. Using the name "subscription" keeps the +user-facing concept familiar, but the interoperable wire contract is +intentionally narrower: it means "charge this fixed amount every +interval", not "model every behavior of a commercial subscription +object". + ## Relationship to Payment Methods Payment methods implement "subscription" using method-specific recurring @@ -77,6 +93,12 @@ subset of a richer underlying subscription system. A method MUST either preserve the semantics in this document exactly or reject the request; it MUST NOT approximate them. +Payment method specifications MAY also impose additional constraints +that are not part of the shared contract, such as an explicit expiry or +recipient requirements, when the underlying payment system cannot safely +support the shared intent without them. Such constraints MUST be made +explicit by the method specification. + # Requirements Language {::boilerplate bcp14-tagged} @@ -100,8 +122,7 @@ Renewal billing period. Cancellation -: The act of ending a subscription before `subscriptionExpires`, - preventing future renewals. +: The act of ending a subscription, preventing future renewals. Subscription Identifier : A server-issued opaque identifier for an activated subscription, @@ -114,7 +135,8 @@ Subscription Identifier The "subscription" intent represents a request for a recurring fixed-amount payment of `amount`, charged once per billing period until -`subscriptionExpires` or explicit cancellation. +explicit cancellation or until the recurring authorization otherwise +becomes invalid. ## Properties @@ -123,7 +145,7 @@ fixed-amount payment of `amount`, charged once per billing period until | **Intent Identifier** | `subscription` | | **Payment Timing** | Recurring (initial charge at activation, then once per period) | | **Idempotency** | Credential single-use; subscription grant reusable across billing periods | -| **Reversibility** | Cancellable before expiry | +| **Reversibility** | Cancellable | ## Flow @@ -182,7 +204,6 @@ supported subset. | `amount` | string | Fixed payment amount per billing period in base units | | `currency` | string | Currency or asset identifier (see {{currency-formats}}) | | `periodSeconds` | string | Billing period duration in seconds | -| `subscriptionExpires` | string | Subscription expiry timestamp in {{RFC3339}} format | The `amount` value MUST be a string representation of a positive integer in base 10 with no sign, decimal point, exponent, or @@ -215,32 +236,36 @@ The `subscriptionId` field is absent during initial activation. Servers MAY include it when issuing a challenge tied to an existing subscription. +Payment methods MAY define additional top-level request fields when the +underlying payment system requires data that is not part of the shared +contract. Such fields MUST be documented by the payment method +specification and MUST NOT change the meaning of the shared fields in +this section. + Servers issuing `intent="subscription"` challenges SHOULD include the `expires` auth-param in `WWW-Authenticate` per {{I-D.httpauth-payment}}, using {{RFC3339}} format. Request objects MUST NOT duplicate the -challenge expiry value. The `subscriptionExpires` field instead defines -when the subscription itself expires. +challenge expiry value. -If the challenge includes `expires`, the `subscriptionExpires` value -MUST be strictly later than the challenge `expires` timestamp. Servers -MUST reject credentials where `subscriptionExpires` is at or before the -challenge `expires`. - -The first billing period begins when the subscription is activated. -Payment methods MAY define additional activation controls in +The first billing period begins immediately when the subscription is +activated. Payment methods MAY define additional activation controls in `methodDetails`, but MUST define exact activation semantics if they do so. -The billing anchor for a subscription is the time activation succeeds. -Billing periods are contiguous fixed-duration windows derived by adding -`periodSeconds` to that anchor. +The billing anchor for a subscription is the time activation succeeds, +or an equivalent network-native timestamp defined by the payment method +specification. Billing periods are contiguous fixed-duration windows +derived by adding `periodSeconds` to that anchor. + +This shared intent does not define deferred starts or merchant-selected +billing anchors. A payment method that needs a more specific anchor rule +MUST document it explicitly. The shared fields in this section are the canonical subscription contract. Payment method specifications MUST document how they map -`amount`, `periodSeconds`, `subscriptionExpires`, and activation to the -underlying payment system. If a payment method cannot represent those -fields or semantics exactly, it MUST reject the request rather than -approximate it. +`amount`, `periodSeconds`, and activation to the underlying payment +system. If a payment method cannot represent those fields or semantics +exactly, it MUST reject the request rather than approximate it. ## Currency Formats {#currency-formats} @@ -276,14 +301,14 @@ In particular: - Methods should support only request shapes they can represent exactly. - Methods should document the supported and rejected ranges or values of - `periodSeconds`, how `subscriptionExpires` is enforced, and what - conditions make activation succeed. + `periodSeconds`, any additional bounded-lifetime or expiry rules they + impose, and what conditions make activation succeed. - Activation should not be reported as successful until both subscription setup and the first billing-period charge have succeeded. - Methods should preserve the shared invariants of one successful charge per billing period, no automatic accumulation of missed periods, and - no renewals after expiry. + no renewals after cancellation or any method-specific expiry. - Richer network-native features such as trials, prorations, discounts, metered billing, pause or resume controls, quantity changes, plan changes, or open-ended renewals should be disabled or @@ -302,7 +327,6 @@ In particular: "amount": "9900", "currency": "usd", "periodSeconds": "2592000", - "subscriptionExpires": "2026-01-01T00:00:00Z", "description": "Pro plan" } ~~~ @@ -314,10 +338,9 @@ In particular: "amount": "10000000", "currency": "0x20c0000000000000000000000000000000000001", "periodSeconds": "2592000", - "subscriptionExpires": "2026-01-01T00:00:00Z", "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00", "methodDetails": { - "chainId": 4217 + "chainId": 42431 } } ~~~ @@ -343,9 +366,9 @@ Servers MUST reject replayed credentials. A successfully activated subscription may be reused for later billing periods until: -- The `subscriptionExpires` timestamp is reached - The payer explicitly cancels it - The payment method revokes or invalidates the authorization +- Any method-specific expiry or bounded lifetime is reached # Subscription Lifecycle @@ -415,7 +438,7 @@ At minimum, servers MUST track: - Billing anchor or equivalent current billing-period start time - Last successfully charged billing-period index, or whether the current billing period has been charged -- Subscription expiry +- Any method-specific expiry or bounded-lifetime state - Cancellation or revocation status For non-idempotent requests, clients SHOULD send an `Idempotency-Key` @@ -425,7 +448,8 @@ duplicate idempotent request. ## Cancellation -Payers SHOULD be able to cancel subscriptions before expiry. +Payers SHOULD be able to cancel subscriptions before any applicable +method-specific expiry. Cancellation mechanisms, effective-time rules, and any continued access for already-paid service are method-specific and MUST be documented by the payment method or application profile. @@ -440,7 +464,7 @@ MUST return an appropriate HTTP status code: | Condition | Status Code | Behavior | |-----------|-------------|----------| -| Subscription expired | 402 Payment Required | Issue new challenge | +| Method-specific expiry reached | 402 Payment Required | Issue new challenge | | Cancellation effective or authorization revoked | 402 Payment Required | Issue new challenge | | Current billing period unpaid or renewal failed | 402 Payment Required | Issue new challenge | | Invalid credential | 402 Payment Required | Issue new challenge | @@ -461,7 +485,6 @@ Suppose a server offers a plan with these request fields: - `amount = "9900"` - `currency = "usd"` - `periodSeconds = "2592000"` -- `subscriptionExpires = "2026-07-14T12:00:00Z"` If activation succeeds at `2026-01-15T12:03:10Z`, that time becomes the billing anchor. The resulting billing periods are: @@ -502,12 +525,16 @@ intent authorizes at most one charge for Period 4. The missed Period 3 charge does not automatically accumulate into authority to collect both Period 3 and Period 4. -## Natural Expiry Example +## Method-Specific Expiry Example -Suppose `subscriptionExpires` is `2026-07-14T12:00:00Z`. Once that time -is reached, the server stops treating the subscription as reusable for -future billing periods. Requests after that time receive -`402 Payment Required` with a fresh challenge. +The shared subscription intent does not require an expiry field. Some +payment methods define an optional or required bounded lifetime for the +recurring authorization. + +Once such a method-specific expiry is reached, the server stops +treating the subscription as reusable for future billing periods. +Requests after that time receive `402 Payment Required` with a fresh +challenge. # Security Considerations @@ -524,7 +551,8 @@ Clients MUST verify before activating a subscription: 1. `amount` is acceptable for the service 2. `currency` is expected 3. `periodSeconds` matches the expected billing interval -4. `subscriptionExpires` is acceptable +4. Any method-specific fields or constraints are understood and + acceptable Clients MUST NOT rely on the `description` field for payment verification. diff --git a/specs/methods/stripe/draft-stripe-subscription-00.md b/specs/methods/stripe/draft-stripe-subscription-00.md index 7166b6dc..59d1e63f 100644 --- a/specs/methods/stripe/draft-stripe-subscription-00.md +++ b/specs/methods/stripe/draft-stripe-subscription-00.md @@ -59,8 +59,9 @@ informative: This document defines the `subscription` intent for the `stripe` payment method within the Payment HTTP Authentication Scheme {{I-D.httpauth-payment}}. It specifies a constrained Stripe Billing -profile for fixed-price, bounded recurring subscriptions whose -activation succeeds only after the first invoice is paid. +profile for fixed-price recurring subscriptions, optionally bounded by +an explicit expiry, whose activation succeeds only after the first +invoice is paid. --- middle @@ -79,6 +80,11 @@ schedule changes. This method supports only the subset that preserves the shared subscription semantics exactly. Servers MUST reject request objects or Stripe configurations that would broaden those semantics. +This profile models the recurring payment agreement, not the full Stripe +Billing object surface. Quantities or seat counts, plan schedules, +prorations, billing-anchor resets, and other commercial-policy behavior +remain out of scope even though Stripe can support them. + ## Stripe Subscription Flow The following diagram illustrates the Stripe subscription flow: @@ -150,14 +156,19 @@ base64url-encoded JSON object. The `request` JSON MUST be serialized using JSON Canonicalization Scheme (JCS) {{RFC8785}} and base64url-encoded without padding per {{I-D.httpauth-payment}}. -## Shared Fields +## Request Fields + +The Stripe `subscription` profile uses the shared `amount`, `currency`, +`periodSeconds`, `description`, `externalId`, and `subscriptionId` +fields from {{I-D.payment-intent-subscription}}. It additionally defines +the following request constraints and fields: | Field | Type | Required | Description | |-------|------|----------|-------------| | `amount` | string | REQUIRED | Fixed payment amount per billing period in the currency's smallest unit | | `currency` | string | REQUIRED | Lowercase ISO 4217 currency code | | `periodSeconds` | string | REQUIRED | Billing period duration in seconds | -| `subscriptionExpires` | string | REQUIRED | Subscription expiry timestamp in {{RFC3339}} format | +| `subscriptionExpires` | string | OPTIONAL | Subscription expiry timestamp in {{RFC3339}} format. When present, it bounds the subscription lifetime. | | `description` | string | OPTIONAL | Human-readable subscription description | | `externalId` | string | OPTIONAL | Merchant's reference for the subscription | | `subscriptionId` | string | OPTIONAL | Server-issued opaque identifier for an existing subscription | @@ -219,16 +230,20 @@ the `week` representation. Servers MUST reject any `periodSeconds` value that would require approximation, calendar-month interpretation, calendar-year interpretation, or an unsupported Stripe interval count. -`subscriptionExpires` MUST define a bounded subscription lifetime. -Servers MUST configure Stripe so that no renewal may occur after -`subscriptionExpires`, typically by setting Stripe's `cancel_at` field -{{STRIPE-BILLING-CANCEL}}. In this profile, `subscriptionExpires` MUST -fall on a canonical billing-period boundary derived from the activation -anchor and `periodSeconds`. Servers MUST reject any request whose -expiry would require a prorated invoice, a partial final billing +If `subscriptionExpires` is present, it defines a bounded subscription +lifetime. Servers MUST configure Stripe so that no renewal may occur +after `subscriptionExpires`, typically by setting Stripe's `cancel_at` +field {{STRIPE-BILLING-CANCEL}}. In this profile, `subscriptionExpires` +MUST fall on a canonical billing-period boundary derived from the +activation anchor and `periodSeconds`. Servers MUST reject any request +whose expiry would require a prorated invoice, a partial final billing period, or any other amount or timing change relative to the shared subscription intent. +If `subscriptionExpires` is absent, the Stripe subscription MAY remain +open-ended until canceled or otherwise terminated according to this +profile. + This profile supports only a fixed quantity of 1 for the single subscription item. Servers MUST reject any request or server-side configuration that would vary quantity during the active lifetime of the @@ -249,7 +264,6 @@ features: - mid-cycle plan changes - quantity changes during an active subscription - pause or resume controls -- open-ended subscriptions # Credential Schema @@ -285,8 +299,8 @@ Servers MUST verify Payment credentials for Stripe subscription intent: 1. Verify the challenge ID matches the one issued 2. Verify the challenge has not expired 3. Decode the request object and verify it matches this constrained - profile, including exact `periodSeconds` and `subscriptionExpires` - support + profile, including exact `periodSeconds` support and, if present, + exact `subscriptionExpires` support 4. Extract the `paymentMethod` and optional `customer` from the credential payload 5. Verify the Stripe PaymentMethod exists, is reusable by the @@ -307,8 +321,8 @@ For `intent="subscription"`, the server MUST: 3. Create or reuse a Stripe Price whose amount, currency, and recurring cadence exactly match the request 4. Create a Stripe Subscription with exactly one recurring item, - quantity 1, no unsupported features, and bounded expiry at - `subscriptionExpires` + quantity 1, no unsupported features, and, if `subscriptionExpires` + is present, bounded expiry at that timestamp 5. Treat activation as successful only after the first invoice for that subscription is paid 6. Initialize durable local subscription state for later renewals @@ -346,9 +360,9 @@ collection of older unpaid invoices. If a Stripe recovery or retry flow cannot be mapped exactly to the shared one-charge-per-period invariant, servers MUST disable that flow or reject the request. -Once `subscriptionExpires` is reached, servers MUST stop treating the -Stripe subscription as authority for additional renewals, even if Stripe -later reports a paid invoice. +Once `subscriptionExpires` is reached, if that field was present, +servers MUST stop treating the Stripe subscription as authority for +additional renewals, even if Stripe later reports a paid invoice. ## Receipt Generation diff --git a/specs/methods/tempo/draft-tempo-subscription-00.md b/specs/methods/tempo/draft-tempo-subscription-00.md index c517d3e5..842922a5 100644 --- a/specs/methods/tempo/draft-tempo-subscription-00.md +++ b/specs/methods/tempo/draft-tempo-subscription-00.md @@ -64,7 +64,8 @@ This document defines the "subscription" intent for the "tempo" payment method in the Payment HTTP Authentication Scheme. It specifies how clients grant servers permission to collect a fixed TIP-20 token payment once per billing period using recipient-scoped access keys on -the Tempo blockchain. +the Tempo blockchain. This profile intentionally models the recurring +transfer authorization itself, not a richer billing object. --- middle @@ -79,11 +80,21 @@ This specification inherits the shared `subscription` intent semantics from {{I-D.payment-intent-subscription}} and defines Tempo-specific request fields, payloads, and settlement behavior. +This profile is intentionally narrower than a general billing +subscription. It standardizes a recurring token-transfer authorization, +not price catalogs, quantities, prorations, deferred starts, or +billing-anchor resets. + Tempo subscriptions support only key-authorization fulfillment. Tempo transactions containing standalone `approve` calls and push-mode hash credentials do not provide the per-period enforcement required for this intent. +Tempo also imposes an additional constraint that is not part of the +shared intent: the recurring authorization MUST have an explicit expiry. +This method therefore requires a `subscriptionExpires` field because the +underlying Tempo key authorization itself is time-bounded. + Tempo subscriptions also require the {{TIP-1011}} periodic token-limit and `allowed_calls` restrictions described in this document. Servers MUST reject request objects on chains or deployments that cannot enforce @@ -159,7 +170,12 @@ base64url-encoded JSON object. The `request` JSON MUST be serialized using JSON Canonicalization Scheme (JCS) {{RFC8785}} and base64url-encoded without padding per {{I-D.httpauth-payment}}. -## Shared Fields +## Request Fields + +Tempo uses the shared `amount`, `currency`, `periodSeconds`, +`recipient`, `description`, `externalId`, and `subscriptionId` fields +from {{I-D.payment-intent-subscription}}. It additionally requires the +following request field because Tempo key authorizations must expire: | Field | Type | Required | Description | |-------|------|----------|-------------| @@ -184,7 +200,7 @@ surrounding whitespace. Leading zeros MUST NOT be used. | Field | Type | Required | Description | |-------|------|----------|-------------| -| `methodDetails.chainId` | number | OPTIONAL | Tempo chain ID. If omitted, the default value is 4217 (Tempo mainnet). | +| `methodDetails.chainId` | number | OPTIONAL | Tempo chain ID. If omitted, the default value is 42431 (Tempo mainnet). | Servers issuing `intent="subscription"` challenges SHOULD include the `expires` auth-param in `WWW-Authenticate` per {{I-D.httpauth-payment}}, @@ -214,7 +230,7 @@ be represented in the Tempo key authorization expiry field. "subscriptionExpires": "2026-01-01T00:00:00Z", "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00", "methodDetails": { - "chainId": 4217 + "chainId": 42431 } } ~~~ @@ -253,7 +269,7 @@ base64url-encoded JSON object per {{I-D.httpauth-payment}}. |-------|------|----------|-------------| | `challenge` | object | REQUIRED | Echo of the challenge from the server | | `payload` | object | REQUIRED | Tempo-specific payload object | -| `source` | string | OPTIONAL | Payer identifier as a DID (e.g., `did:pkh:eip155:4217:0x...`) | +| `source` | string | OPTIONAL | Payer identifier as a DID (e.g., `did:pkh:eip155:42431:0x...`) | The `source` field, if present, SHOULD use the `did:pkh` method with the chain ID applicable to the challenge and the payer's Ethereum @@ -301,7 +317,7 @@ field. "signature": "0xf8c1...signed authorization bytes...", "type": "keyAuthorization" }, - "source": "did:pkh:eip155:4217:0x1234567890abcdef1234567890abcdef12345678" + "source": "did:pkh:eip155:42431:0x1234567890abcdef1234567890abcdef12345678" } ~~~ @@ -557,7 +573,7 @@ The `request` decodes to: "subscriptionExpires": "2026-01-01T00:00:00Z", "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00", "methodDetails": { - "chainId": 4217 + "chainId": 42431 } } ~~~ @@ -581,7 +597,7 @@ seconds until 2026-01-01T00:00:00Z. "signature": "0xf8c1...signed authorization bytes...", "type": "keyAuthorization" }, - "source": "did:pkh:eip155:4217:0x1234567890abcdef1234567890abcdef12345678" + "source": "did:pkh:eip155:42431:0x1234567890abcdef1234567890abcdef12345678" } ~~~ From f4842007e890d8b606979d4c0314b9f170f0943b Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Mon, 20 Apr 2026 14:44:55 -0700 Subject: [PATCH 11/16] docs: simplify subscription expiry semantics --- .../draft-payment-intent-subscription-00.md | 66 ++++++++----------- .../stripe/draft-stripe-subscription-00.md | 18 +++-- .../tempo/draft-tempo-subscription-00.md | 27 +++++--- 3 files changed, 58 insertions(+), 53 deletions(-) diff --git a/specs/intents/draft-payment-intent-subscription-00.md b/specs/intents/draft-payment-intent-subscription-00.md index 1b6308c9..1791aafb 100644 --- a/specs/intents/draft-payment-intent-subscription-00.md +++ b/specs/intents/draft-payment-intent-subscription-00.md @@ -193,9 +193,9 @@ JSON MUST be serialized using JSON Canonicalization Scheme (JCS) ## Shared Fields All payment methods implementing the "subscription" intent MUST support -these shared fields. Payment methods MAY elevate OPTIONAL fields to -REQUIRED in their method specification, and MUST document any narrower -supported subset. +the required shared fields below. Method specifications MAY support, +forbid, or elevate optional shared fields to REQUIRED, but MUST +document those choices explicitly. ### Required Fields @@ -227,14 +227,17 @@ cadence. | Field | Type | Description | |-------|------|-------------| | `recipient` | string | Payment recipient in method-native format | +| `subscriptionExpires` | string | Optional recurring-authorization expiry timestamp in {{RFC3339}} format | | `description` | string | Human-readable subscription description | | `externalId` | string | Merchant's reference for the subscription | -| `subscriptionId` | string | Server-issued opaque identifier for an existing subscription | | `methodDetails` | object | Method-specific extension data | -The `subscriptionId` field is absent during initial activation. Servers -MAY include it when issuing a challenge tied to an existing -subscription. +When present, `subscriptionExpires` bounds the reusable lifetime of the +subscription authorization. Once that timestamp is reached, the server +MUST stop treating the subscription as authority for future billing +periods. Payment methods MAY require this field and MAY impose +additional constraints, such as billing-period boundary alignment or +network-native representation limits. Payment methods MAY define additional top-level request fields when the underlying payment system requires data that is not part of the shared @@ -368,7 +371,7 @@ periods until: - The payer explicitly cancels it - The payment method revokes or invalidates the authorization -- Any method-specific expiry or bounded lifetime is reached +- The `subscriptionExpires` timestamp, if present, is reached # Subscription Lifecycle @@ -405,27 +408,23 @@ Payment method specifications define the concrete renewal, retry, recovery, and cancellation mechanisms, but they MUST preserve the invariants in this section. -## Reauthentication +## Subscription Identifier After successful activation, the server MUST return a `subscriptionId` in the `Payment-Receipt`. The value MUST be a base64url {{RFC4648}} string without padding and MUST be unique within the server's subscription namespace. -Clients SHOULD retain the `subscriptionId` and, when intending to use an -existing subscription on a later request, MAY send it in the -`Subscription-Id` request header. - -The `Subscription-Id` header is only a subscription-selection hint. It -does not, by itself, prove authority to use the subscription. +Clients MAY retain the `subscriptionId` as application data when +referring to the active subscription in later interactions. -If a request is associated with an existing subscription, the server MAY -echo that identifier in the challenge `request.subscriptionId` field to -bind the challenge to the intended subscription. +This specification does not define a dedicated HTTP request header for +carrying `subscriptionId`. Servers MUST authenticate or otherwise authorize the client's use of the identified subscription before granting access or collecting a renewal -charge. A matching `Subscription-Id` alone is insufficient. +charge. Possession or presentation of a `subscriptionId` alone is +insufficient. ## Server Accounting and Idempotency @@ -478,7 +477,7 @@ usable and initiate a new subscription flow. This section is non-normative. -## Monthly Billing Example +## 30-Day Billing Example Suppose a server offers a plan with these request fields: @@ -525,16 +524,15 @@ intent authorizes at most one charge for Period 4. The missed Period 3 charge does not automatically accumulate into authority to collect both Period 3 and Period 4. -## Method-Specific Expiry Example +## Expiry Example -The shared subscription intent does not require an expiry field. Some -payment methods define an optional or required bounded lifetime for the -recurring authorization. +The shared subscription intent defines `subscriptionExpires` as an +optional top-level field. Some payment methods require it and others +leave it optional. -Once such a method-specific expiry is reached, the server stops -treating the subscription as reusable for future billing periods. -Requests after that time receive `402 Payment Required` with a fresh -challenge. +Once `subscriptionExpires` is reached, the server stops treating the +subscription as reusable for future billing periods. Requests after that +time receive `402 Payment Required` with a fresh challenge. # Security Considerations @@ -551,8 +549,8 @@ Clients MUST verify before activating a subscription: 1. `amount` is acceptable for the service 2. `currency` is expected 3. `periodSeconds` matches the expected billing interval -4. Any method-specific fields or constraints are understood and - acceptable +4. Any `subscriptionExpires` value and method-specific constraints are + understood and acceptable Clients MUST NOT rely on the `description` field for payment verification. @@ -584,14 +582,6 @@ receipts. # IANA Considerations -## Header Field Registration - -This document registers the following header fields: - -| Field Name | Status | Reference | -|------------|--------|-----------| -| `Subscription-Id` | permanent | This document | - ## Payment Intent Registration This document registers the "subscription" intent in the "HTTP Payment diff --git a/specs/methods/stripe/draft-stripe-subscription-00.md b/specs/methods/stripe/draft-stripe-subscription-00.md index 59d1e63f..d8d51ff4 100644 --- a/specs/methods/stripe/draft-stripe-subscription-00.md +++ b/specs/methods/stripe/draft-stripe-subscription-00.md @@ -159,9 +159,9 @@ base64url-encoded without padding per {{I-D.httpauth-payment}}. ## Request Fields The Stripe `subscription` profile uses the shared `amount`, `currency`, -`periodSeconds`, `description`, `externalId`, and `subscriptionId` -fields from {{I-D.payment-intent-subscription}}. It additionally defines -the following request constraints and fields: +`periodSeconds`, `subscriptionExpires`, `description`, and +`externalId` fields from {{I-D.payment-intent-subscription}}. It +additionally defines the following request constraints: | Field | Type | Required | Description | |-------|------|----------|-------------| @@ -171,7 +171,6 @@ the following request constraints and fields: | `subscriptionExpires` | string | OPTIONAL | Subscription expiry timestamp in {{RFC3339}} format. When present, it bounds the subscription lifetime. | | `description` | string | OPTIONAL | Human-readable subscription description | | `externalId` | string | OPTIONAL | Merchant's reference for the subscription | -| `subscriptionId` | string | OPTIONAL | Server-issued opaque identifier for an existing subscription | | `recipient` | string | MUST NOT | This profile identifies the merchant by the challenged Stripe account and `methodDetails.networkId`, not by a request-native recipient field | The `amount` value MUST be a string representation of a positive @@ -189,9 +188,14 @@ Servers MUST reject request objects that include `recipient`. | Field | Type | Required | Description | |-------|------|----------|-------------| | `methodDetails.networkId` | string | REQUIRED | Stripe Business Network Profile ID for the challenged merchant | -| `methodDetails.paymentMethodTypes` | []string | REQUIRED | Stripe payment method types accepted for the first invoice | +| `methodDetails.paymentMethodTypes` | []string | REQUIRED | Stripe payment method types accepted for activation and future off-session recurring invoices | | `methodDetails.metadata` | object | OPTIONAL | Merchant-defined metadata to attach to Stripe objects | +Servers MUST include only payment method types that can complete this +profile's activation flow, including any asynchronous or +customer-action-required first-invoice path, and can also be reused for +future off-session recurring charges under the challenged account. + **Example:** ~~~json @@ -304,7 +308,9 @@ Servers MUST verify Payment credentials for Stripe subscription intent: 4. Extract the `paymentMethod` and optional `customer` from the credential payload 5. Verify the Stripe PaymentMethod exists, is reusable by the - challenged merchant, and has a type allowed by the challenge + challenged merchant, has a type allowed by the challenge, and can + support both the profile's first-invoice activation flow and future + off-session recurring charges 6. Verify the credential has not been replayed for the same challenge Servers MUST complete challenge validation before creating or mutating diff --git a/specs/methods/tempo/draft-tempo-subscription-00.md b/specs/methods/tempo/draft-tempo-subscription-00.md index 842922a5..069863b5 100644 --- a/specs/methods/tempo/draft-tempo-subscription-00.md +++ b/specs/methods/tempo/draft-tempo-subscription-00.md @@ -173,9 +173,10 @@ base64url-encoded without padding per {{I-D.httpauth-payment}}. ## Request Fields Tempo uses the shared `amount`, `currency`, `periodSeconds`, -`recipient`, `description`, `externalId`, and `subscriptionId` fields -from {{I-D.payment-intent-subscription}}. It additionally requires the -following request field because Tempo key authorizations must expire: +`subscriptionExpires`, `recipient`, `description`, and `externalId` +fields from {{I-D.payment-intent-subscription}}. Tempo additionally +requires `subscriptionExpires` because Tempo key authorizations must +expire: | Field | Type | Required | Description | |-------|------|----------|-------------| @@ -186,7 +187,6 @@ following request field because Tempo key authorizations must expire: | `recipient` | string | REQUIRED | Recipient address authorized for subscription charges | | `description` | string | OPTIONAL | Human-readable subscription description | | `externalId` | string | OPTIONAL | Merchant's reference for the subscription | -| `subscriptionId` | string | OPTIONAL | Server-issued opaque identifier for an existing subscription | The `amount` value MUST be a string representation of a positive integer in base 10 with no sign, decimal point, exponent, or @@ -196,6 +196,14 @@ The `periodSeconds` value MUST be a string representation of a positive integer in base 10 with no sign, decimal point, exponent, or surrounding whitespace. Leading zeros MUST NOT be used. +Hex values in this profile use lowercase hexadecimal with `0x` prefix +and no padding or truncation. Implementations MUST use lowercase hex +when generating addresses, token identifiers, selectors, and +hex-encoded signed payloads. Implementations SHOULD accept mixed-case +input, but MUST normalize it to lowercase before comparison. Address, +selector, and token-identifier comparisons are by decoded value, not +raw string form. + ## Method Details | Field | Type | Required | Description | @@ -227,7 +235,7 @@ be represented in the Tempo key authorization expiry field. "amount": "10000000", "currency": "0x20c0000000000000000000000000000000000001", "periodSeconds": "2592000", - "subscriptionExpires": "2026-01-01T00:00:00Z", + "subscriptionExpires": "2026-07-14T12:00:00Z", "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00", "methodDetails": { "chainId": 42431 @@ -357,6 +365,9 @@ charge are a single atomic operation: Servers MUST treat the subscription as active only after the activation transaction succeeds. +Servers MUST NOT treat activation as successful if the activation +transaction settles at or after `subscriptionExpires`. + ## Renewal For each later billing period, the server MAY submit one transaction @@ -570,7 +581,7 @@ The `request` decodes to: "amount": "10000000", "currency": "0x20c0000000000000000000000000000000000001", "periodSeconds": "2592000", - "subscriptionExpires": "2026-01-01T00:00:00Z", + "subscriptionExpires": "2026-07-14T12:00:00Z", "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00", "methodDetails": { "chainId": 42431 @@ -579,7 +590,7 @@ The `request` decodes to: ~~~ This requests a recurring payment of 10.00 alphaUSD every 2,592,000 -seconds until 2026-01-01T00:00:00Z. +seconds until 2026-07-14T12:00:00Z. **Credential:** @@ -643,7 +654,6 @@ authorization: ~~~http GET /api/resource HTTP/1.1 Host: api.example.com -Subscription-Id: c3ViXzAxMjM0NTY ~~~ When Period 1 begins, the server determines that billing-period index 1 @@ -682,7 +692,6 @@ that time continue to succeed without another renewal charge: ~~~http GET /api/resource HTTP/1.1 Host: api.example.com -Subscription-Id: c3ViXzAxMjM0NTY ~~~ Once `2026-04-15T12:03:10Z` is reached, the server stops submitting From 4ff21e353751ba5c914fa51c381d77efafc0dd28 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Mon, 20 Apr 2026 15:43:13 -0700 Subject: [PATCH 12/16] Clarify subscription reuse identifier --- specs/intents/draft-payment-intent-subscription-00.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/specs/intents/draft-payment-intent-subscription-00.md b/specs/intents/draft-payment-intent-subscription-00.md index 1791aafb..f7ecdcb9 100644 --- a/specs/intents/draft-payment-intent-subscription-00.md +++ b/specs/intents/draft-payment-intent-subscription-00.md @@ -418,8 +418,13 @@ subscription namespace. Clients MAY retain the `subscriptionId` as application data when referring to the active subscription in later interactions. -This specification does not define a dedicated HTTP request header for -carrying `subscriptionId`. +The `Subscription-Id` header is only a subscription-selection hint. It +does not, by itself, prove authority to use the subscription. + +The challenge `request` object does not carry `subscriptionId`. The +canonical reusable identifier is the `subscriptionId` field returned in +the `Payment-Receipt`, and the corresponding request-time selector is +the `Subscription-Id` header. Servers MUST authenticate or otherwise authorize the client's use of the identified subscription before granting access or collecting a renewal From 1cdca2f8c23b4fcd5c80ac11ec9d2f7a64107778 Mon Sep 17 00:00:00 2001 From: Brendan Ryan <1572504+brendanjryan@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:51:22 -0700 Subject: [PATCH 13/16] Tighten subscription intent and Stripe profile --- .../draft-payment-intent-subscription-00.md | 46 +++++++++---------- .../stripe/draft-stripe-subscription-00.md | 45 ++++-------------- 2 files changed, 32 insertions(+), 59 deletions(-) diff --git a/specs/intents/draft-payment-intent-subscription-00.md b/specs/intents/draft-payment-intent-subscription-00.md index f7ecdcb9..64f9faa0 100644 --- a/specs/intents/draft-payment-intent-subscription-00.md +++ b/specs/intents/draft-payment-intent-subscription-00.md @@ -114,8 +114,8 @@ Billing Period may be collected. Activation -: The successful initial registration of a subscription, which also - collects the first billing-period charge. +: The successful initial setup of a subscription, which includes + collection of the first billing-period charge. Renewal : A later charge that collects the subscription amount for a subsequent @@ -126,8 +126,8 @@ Cancellation Subscription Identifier : A server-issued opaque identifier for an activated subscription, - used by clients to re-authenticate into that subscription on later - requests. + used by servers and applications to refer to that subscription in + later interactions. # Intent Semantics @@ -239,11 +239,9 @@ periods. Payment methods MAY require this field and MAY impose additional constraints, such as billing-period boundary alignment or network-native representation limits. -Payment methods MAY define additional top-level request fields when the -underlying payment system requires data that is not part of the shared -contract. Such fields MUST be documented by the payment method -specification and MUST NOT change the meaning of the shared fields in -this section. +Payment methods MUST place all method-specific request parameters in +`methodDetails`. They MAY require or forbid shared optional fields, but +MUST NOT define additional top-level request fields. Servers issuing `intent="subscription"` challenges SHOULD include the `expires` auth-param in `WWW-Authenticate` per {{I-D.httpauth-payment}}, @@ -286,9 +284,9 @@ support and how to interpret amounts for each format. ## Method Extensions -Payment methods MAY define additional fields in the `methodDetails` -object. These fields are method-specific and MUST be documented in the -payment method specification. +Payment methods MAY define additional fields only in the +`methodDetails` object. Shared top-level fields retain the meanings +defined in this document. ## Implementor Guidance @@ -341,6 +339,7 @@ In particular: "amount": "10000000", "currency": "0x20c0000000000000000000000000000000000001", "periodSeconds": "2592000", + "subscriptionExpires": "2026-07-14T12:00:00Z", "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00", "methodDetails": { "chainId": 42431 @@ -380,12 +379,14 @@ periods until: When the server receives a "subscription" credential, it MUST: 1. Verify the subscription authorization proof -2. Activate the subscription -3. Collect the first billing-period charge -4. Initialize durable subscription state for later renewals -5. Return success (200) with a `Payment-Receipt` for the first charge, +2. Perform any method-specific subscription setup and collect the first + billing-period charge +3. Initialize durable subscription state for later renewals +4. Return success (200) with a `Payment-Receipt` for the first charge, including a `subscriptionId` +The subscription becomes active only after these steps succeed. + ## Renewal For each later billing period, the server MAY collect one renewal @@ -416,15 +417,12 @@ string without padding and MUST be unique within the server's subscription namespace. Clients MAY retain the `subscriptionId` as application data when -referring to the active subscription in later interactions. - -The `Subscription-Id` header is only a subscription-selection hint. It -does not, by itself, prove authority to use the subscription. +referring to the active subscription in later interactions. Applications +MAY instead use application-defined identifiers or other context to +associate a later request with an existing subscription. -The challenge `request` object does not carry `subscriptionId`. The -canonical reusable identifier is the `subscriptionId` field returned in -the `Payment-Receipt`, and the corresponding request-time selector is -the `Subscription-Id` header. +This specification does not define a dedicated request header or +parameter for selecting an existing subscription. Servers MUST authenticate or otherwise authorize the client's use of the identified subscription before granting access or collecting a renewal diff --git a/specs/methods/stripe/draft-stripe-subscription-00.md b/specs/methods/stripe/draft-stripe-subscription-00.md index d8d51ff4..d34d5876 100644 --- a/specs/methods/stripe/draft-stripe-subscription-00.md +++ b/specs/methods/stripe/draft-stripe-subscription-00.md @@ -159,8 +159,8 @@ base64url-encoded without padding per {{I-D.httpauth-payment}}. ## Request Fields The Stripe `subscription` profile uses the shared `amount`, `currency`, -`periodSeconds`, `subscriptionExpires`, `description`, and -`externalId` fields from {{I-D.payment-intent-subscription}}. It +`periodSeconds`, `description`, and `externalId` fields from +{{I-D.payment-intent-subscription}}. It additionally defines the following request constraints: | Field | Type | Required | Description | @@ -168,7 +168,6 @@ additionally defines the following request constraints: | `amount` | string | REQUIRED | Fixed payment amount per billing period in the currency's smallest unit | | `currency` | string | REQUIRED | Lowercase ISO 4217 currency code | | `periodSeconds` | string | REQUIRED | Billing period duration in seconds | -| `subscriptionExpires` | string | OPTIONAL | Subscription expiry timestamp in {{RFC3339}} format. When present, it bounds the subscription lifetime. | | `description` | string | OPTIONAL | Human-readable subscription description | | `externalId` | string | OPTIONAL | Merchant's reference for the subscription | | `recipient` | string | MUST NOT | This profile identifies the merchant by the challenged Stripe account and `methodDetails.networkId`, not by a request-native recipient field | @@ -181,7 +180,8 @@ The `periodSeconds` value MUST be a string representation of a positive integer in base 10 with no sign, decimal point, exponent, or surrounding whitespace. Leading zeros MUST NOT be used. -Servers MUST reject request objects that include `recipient`. +Servers MUST reject request objects that include `recipient` or +`subscriptionExpires`. ## Method Details @@ -203,7 +203,6 @@ future off-session recurring charges under the challenged account. "amount": "5000", "currency": "usd", "periodSeconds": "604800", - "subscriptionExpires": "2026-03-12T12:03:10Z", "description": "Weekly Pro plan", "externalId": "sub_12345", "methodDetails": { @@ -234,20 +233,6 @@ the `week` representation. Servers MUST reject any `periodSeconds` value that would require approximation, calendar-month interpretation, calendar-year interpretation, or an unsupported Stripe interval count. -If `subscriptionExpires` is present, it defines a bounded subscription -lifetime. Servers MUST configure Stripe so that no renewal may occur -after `subscriptionExpires`, typically by setting Stripe's `cancel_at` -field {{STRIPE-BILLING-CANCEL}}. In this profile, `subscriptionExpires` -MUST fall on a canonical billing-period boundary derived from the -activation anchor and `periodSeconds`. Servers MUST reject any request -whose expiry would require a prorated invoice, a partial final billing -period, or any other amount or timing change relative to the shared -subscription intent. - -If `subscriptionExpires` is absent, the Stripe subscription MAY remain -open-ended until canceled or otherwise terminated according to this -profile. - This profile supports only a fixed quantity of 1 for the single subscription item. Servers MUST reject any request or server-side configuration that would vary quantity during the active lifetime of the @@ -279,7 +264,6 @@ Stripe subscription, the `payload` object contains the following fields: |-------|------|----------|-------------| | `paymentMethod` | string | REQUIRED | Stripe PaymentMethod ID to use for the first invoice and future recurring charges | | `customer` | string | OPTIONAL | Existing Stripe Customer ID if the merchant already has one for the payer | -| `externalId` | string | OPTIONAL | Client's reference ID | The `paymentMethod` MUST reference a Stripe PaymentMethod whose type is included in `methodDetails.paymentMethodTypes` and which is suitable for @@ -291,8 +275,7 @@ account. ~~~json { "paymentMethod": "pm_1Qabc32eZvKYlo2C7b8H1234", - "customer": "cus_S7x1Pq5R9n2Lm4", - "externalId": "client_sub_789" + "customer": "cus_S7x1Pq5R9n2Lm4" } ~~~ @@ -303,8 +286,7 @@ Servers MUST verify Payment credentials for Stripe subscription intent: 1. Verify the challenge ID matches the one issued 2. Verify the challenge has not expired 3. Decode the request object and verify it matches this constrained - profile, including exact `periodSeconds` support and, if present, - exact `subscriptionExpires` support + profile, including exact `periodSeconds` support 4. Extract the `paymentMethod` and optional `customer` from the credential payload 5. Verify the Stripe PaymentMethod exists, is reusable by the @@ -327,8 +309,7 @@ For `intent="subscription"`, the server MUST: 3. Create or reuse a Stripe Price whose amount, currency, and recurring cadence exactly match the request 4. Create a Stripe Subscription with exactly one recurring item, - quantity 1, no unsupported features, and, if `subscriptionExpires` - is present, bounded expiry at that timestamp + quantity 1, and no unsupported features 5. Treat activation as successful only after the first invoice for that subscription is paid 6. Initialize durable local subscription state for later renewals @@ -366,10 +347,6 @@ collection of older unpaid invoices. If a Stripe recovery or retry flow cannot be mapped exactly to the shared one-charge-per-period invariant, servers MUST disable that flow or reject the request. -Once `subscriptionExpires` is reached, if that field was present, -servers MUST stop treating the Stripe subscription as authority for -additional renewals, even if Stripe later reports a paid invoice. - ## Receipt Generation Upon successful activation or renewal, servers MUST return a @@ -454,7 +431,6 @@ The `request` decodes to: "amount": "5000", "currency": "usd", "periodSeconds": "604800", - "subscriptionExpires": "2026-03-12T12:03:10Z", "description": "Weekly Pro plan", "methodDetails": { "networkId": "profile_1MqDcVKA5fEO2tZvKQm9g8Yj", @@ -473,9 +449,9 @@ The `request` decodes to: ~~~ The server creates or reuses a Stripe Customer, creates or reuses a -weekly fixed-price Stripe Price, creates a bounded Stripe Subscription, -and waits for the first invoice to be paid. Once Stripe reports the -first invoice as paid, the `Payment-Receipt` payload decodes to: +weekly fixed-price Stripe Price, creates a Stripe Subscription, and +waits for the first invoice to be paid. Once Stripe reports the first +invoice as paid, the `Payment-Receipt` payload decodes to: ~~~json { @@ -501,7 +477,6 @@ whole number of days or weeks: "amount": "5000", "currency": "usd", "periodSeconds": "90000", - "subscriptionExpires": "2026-03-12T12:03:10Z", "methodDetails": { "networkId": "profile_1MqDcVKA5fEO2tZvKQm9g8Yj", "paymentMethodTypes": ["card"] From 1ec24ff7ccaebb6adfa03e8c6a1ab014a42109c5 Mon Sep 17 00:00:00 2001 From: Brendan Ryan <1572504+brendanjryan@users.noreply.github.com> Date: Tue, 5 May 2026 20:16:04 -0700 Subject: [PATCH 14/16] docs: tighten subscription profiles --- .../draft-payment-intent-subscription-00.md | 4 +- .../stripe/draft-stripe-subscription-00.md | 158 +++++++++++++++--- .../tempo/draft-tempo-subscription-00.md | 29 ++-- 3 files changed, 154 insertions(+), 37 deletions(-) diff --git a/specs/intents/draft-payment-intent-subscription-00.md b/specs/intents/draft-payment-intent-subscription-00.md index 64f9faa0..af903f93 100644 --- a/specs/intents/draft-payment-intent-subscription-00.md +++ b/specs/intents/draft-payment-intent-subscription-00.md @@ -31,7 +31,7 @@ normative: RFC8785: I-D.httpauth-payment: title: "The 'Payment' HTTP Authentication Scheme" - target: https://datatracker.ietf.org/doc/draft-ietf-httpauth-payment/ + target: https://datatracker.ietf.org/doc/draft-ryan-httpauth-payment/ author: - name: Jake Moxey date: 2026-01 @@ -340,7 +340,7 @@ In particular: "currency": "0x20c0000000000000000000000000000000000001", "periodSeconds": "2592000", "subscriptionExpires": "2026-07-14T12:00:00Z", - "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00", + "recipient": "0x742d35cc6634c0532925a3b844bc9e7595f8fe00", "methodDetails": { "chainId": 42431 } diff --git a/specs/methods/stripe/draft-stripe-subscription-00.md b/specs/methods/stripe/draft-stripe-subscription-00.md index d34d5876..823f7bfc 100644 --- a/specs/methods/stripe/draft-stripe-subscription-00.md +++ b/specs/methods/stripe/draft-stripe-subscription-00.md @@ -1,5 +1,5 @@ --- -title: Stripe subscription Intent for HTTP Payment Authentication +title: Stripe Subscription Intent for HTTP Payment Authentication abbrev: Stripe Subscription docname: draft-stripe-subscription-00 version: 00 @@ -12,11 +12,11 @@ author: - name: Brendan Ryan ins: B. Ryan email: brendan@tempo.xyz - organization: Tempo Labs + org: Tempo Labs - name: Steve Kaliski ins: S. Kaliski email: stevekaliski@stripe.com - organization: Stripe + org: Stripe normative: RFC2119: @@ -25,7 +25,7 @@ normative: RFC8785: I-D.httpauth-payment: title: "The 'Payment' HTTP Authentication Scheme" - target: https://datatracker.ietf.org/doc/draft-ietf-httpauth-payment/ + target: https://datatracker.ietf.org/doc/draft-ryan-httpauth-payment/ author: - name: Jake Moxey date: 2026-01 @@ -52,6 +52,21 @@ informative: title: Using webhooks with subscriptions author: - org: Stripe, Inc. + STRIPE-SUBSCRIPTIONS-API: + target: https://docs.stripe.com/api/subscriptions/create + title: Create a subscription + author: + - org: Stripe, Inc. + STRIPE-METADATA: + target: https://docs.stripe.com/api/metadata + title: Metadata + author: + - org: Stripe, Inc. + STRIPE-SETUP-FUTURE: + target: https://docs.stripe.com/payments/setup-intents + title: Set up future payments + author: + - org: Stripe, Inc. --- --- abstract @@ -59,9 +74,8 @@ informative: This document defines the `subscription` intent for the `stripe` payment method within the Payment HTTP Authentication Scheme {{I-D.httpauth-payment}}. It specifies a constrained Stripe Billing -profile for fixed-price recurring subscriptions, optionally bounded by -an explicit expiry, whose activation succeeds only after the first -invoice is paid. +profile for fixed-price recurring subscriptions whose activation +succeeds only when the first invoice is paid synchronously. --- middle @@ -188,13 +202,21 @@ Servers MUST reject request objects that include `recipient` or | Field | Type | Required | Description | |-------|------|----------|-------------| | `methodDetails.networkId` | string | REQUIRED | Stripe Business Network Profile ID for the challenged merchant | -| `methodDetails.paymentMethodTypes` | []string | REQUIRED | Stripe payment method types accepted for activation and future off-session recurring invoices | -| `methodDetails.metadata` | object | OPTIONAL | Merchant-defined metadata to attach to Stripe objects | +| `methodDetails.paymentMethodTypes` | []string | REQUIRED | Stripe payment method types accepted for synchronous activation and future off-session recurring invoices | +| `methodDetails.metadata` | object | OPTIONAL | Stripe metadata as a string key/value map | Servers MUST include only payment method types that can complete this -profile's activation flow, including any asynchronous or -customer-action-required first-invoice path, and can also be reused for +profile's activation flow synchronously and can also be reused for future off-session recurring charges under the challenged account. +Servers MUST reject payment method types that require an asynchronous +first-invoice settlement path or customer action after the credential is +submitted. + +If `methodDetails.metadata` is present, every key and value MUST be a +JSON string and the object MUST satisfy Stripe metadata limits +{{STRIPE-METADATA}}. Metadata MUST NOT affect payment authorization, +amount, period, recipient, invoice validation, cancellation, or +access-control decisions. **Example:** @@ -207,7 +229,10 @@ future off-session recurring charges under the challenged account. "externalId": "sub_12345", "methodDetails": { "networkId": "profile_1MqDcVKA5fEO2tZvKQm9g8Yj", - "paymentMethodTypes": ["card", "link"] + "paymentMethodTypes": ["card", "link"], + "metadata": { + "plan": "weekly-pro" + } } } ~~~ @@ -238,6 +263,26 @@ subscription item. Servers MUST reject any request or server-side configuration that would vary quantity during the active lifetime of the subscription. +When creating a Stripe Subscription for this profile, servers MUST use +the following create Subscription parameters {{STRIPE-SUBSCRIPTIONS-API}}: + +- `collection_method=charge_automatically` +- `payment_behavior=error_if_incomplete` +- `proration_behavior=none` +- exactly one subscription item with `quantity=1` +- no `add_invoice_items` +- no `billing_cycle_anchor` other than immediate activation +- no `backdate_start_date` +- no `cancel_at` or `cancel_at_period_end` at activation +- no `pending_invoice_item_interval` +- no subscription schedule + +Servers MUST create the subscription using an idempotency key bound to +the challenge ID, payer, payment method, amount, currency, and +`periodSeconds`. If an idempotent retry returns an existing Stripe +Subscription, the server MUST verify that the existing object still +matches this profile before treating the retry as successful. + ## Unsupported Stripe Billing Features Servers implementing this profile MUST disable or reject the following @@ -248,11 +293,16 @@ features: - prorations - discounts or coupons - automatic tax +- additional invoice items +- pending invoice items - usage-based billing - metered add-ons - mid-cycle plan changes - quantity changes during an active subscription - pause or resume controls +- asynchronous first-invoice settlement +- customer-action-required first-invoice flows +- manual invoice collection # Credential Schema @@ -270,6 +320,12 @@ included in `methodDetails.paymentMethodTypes` and which is suitable for future off-session recurring charges under the challenged Stripe account. +Before submitting a credential, the client or Stripe-native collection +flow MUST have obtained any authorization, mandate, or setup required by +Stripe for future off-session recurring charges {{STRIPE-SETUP-FUTURE}}. +Servers MUST reject PaymentMethods that are not reusable for the +challenged merchant and subscription terms. + **Example:** ~~~json @@ -291,8 +347,8 @@ Servers MUST verify Payment credentials for Stripe subscription intent: credential payload 5. Verify the Stripe PaymentMethod exists, is reusable by the challenged merchant, has a type allowed by the challenge, and can - support both the profile's first-invoice activation flow and future - off-session recurring charges + support both the profile's synchronous first-invoice activation flow + and future off-session recurring charges 6. Verify the credential has not been replayed for the same challenge Servers MUST complete challenge validation before creating or mutating @@ -309,27 +365,46 @@ For `intent="subscription"`, the server MUST: 3. Create or reuse a Stripe Price whose amount, currency, and recurring cadence exactly match the request 4. Create a Stripe Subscription with exactly one recurring item, - quantity 1, and no unsupported features -5. Treat activation as successful only after the first invoice for that - subscription is paid -6. Initialize durable local subscription state for later renewals -7. Return success (200) with a `Payment-Receipt` for the first invoice, + quantity 1, the creation parameters defined above, and no + unsupported features +5. Verify the first invoice and its PaymentIntent completed + synchronously +6. Treat activation as successful only after the first invoice for that + subscription is paid and validated +7. Initialize durable local subscription state for later renewals +8. Return success (200) with a `Payment-Receipt` for the first invoice, including a `subscriptionId` Servers MUST NOT treat the subscription as active, grant access, or return a success receipt while the first invoice is unpaid, requires additional customer action, or remains incomplete. -If the first invoice requires an immediate customer confirmation step, -the implementation MAY complete that step using Stripe-native flows, but -the HTTP subscription activation remains incomplete until the first -invoice is paid. +If Stripe cannot pay the first invoice synchronously, including because +the invoice requires customer action, remains incomplete, enters +processing, or depends on asynchronous settlement, the server MUST treat +activation as failed and return `402 Payment Required` with a fresh +challenge. The server MUST NOT expose a protocol continuation state for +that incomplete Stripe Subscription. The canonical billing anchor for this profile is the start timestamp of the first paid Stripe invoice period. Servers MUST use that anchor when mapping later Stripe invoices to the shared `periodSeconds` billing periods. +Before activating a subscription or recording a renewal, servers MUST +validate the paid Stripe invoice. The invoice MUST: + +- belong to the expected Stripe Subscription and Customer +- have status `paid` +- contain exactly one subscription line item +- have no invoice items outside the subscription item +- have no discounts, tax, credits, or prorations that change the amount +- match the challenged `amount` and `currency` +- map to exactly one canonical billing period derived from the billing + anchor and `periodSeconds` +- not have already been recorded for another billing period or + subscription + ## Renewal Later billing periods are fulfilled by Stripe renewal invoices. Servers @@ -347,6 +422,43 @@ collection of older unpaid invoices. If a Stripe recovery or retry flow cannot be mapped exactly to the shared one-charge-per-period invariant, servers MUST disable that flow or reject the request. +Implementations MUST process Stripe invoice events idempotently by +recording the Stripe event ID, invoice ID, subscription ID, and +canonical billing-period index. A duplicate webhook or API retry MUST +return the previously recorded result without creating a second local +payment record or granting another billing period. + +Servers MUST NOT rely on `invoice.created` delivery or acknowledgement +for access decisions. Access can be granted only after a validated paid +invoice has been durably recorded. If webhook delivery, invoice +finalization, or automatic collection is delayed, the corresponding +billing period remains unpaid until a later validated paid invoice is +recorded. + +## Cancellation + +Payers MUST be able to cancel Stripe subscriptions. For this profile, +the default cancellation effective time is the end of the current paid +canonical billing period. + +When a payer cancels, the server MUST set the Stripe Subscription to +cancel at the period end corresponding to the last paid canonical +billing period, and MUST record that cancellation effective time in +durable local state. The server MAY cancel immediately only if the +application separately handles any already-paid access period without +creating an additional charge. + +Servers MUST treat `customer.subscription.deleted` and equivalent +Stripe cancellation state as revocation for future renewals. Servers +MUST NOT collect or record renewal invoices for billing periods whose +start time is at or after the cancellation effective time. + +Servers MUST prevent pending invoice items from being collected after +cancellation. If any pending invoice item, proration, credit, tax, or +other non-profile invoice component exists for the Customer or +Subscription, the server MUST remove it before cancellation or reject +the subscription as no longer conforming to this profile. + ## Receipt Generation Upon successful activation or renewal, servers MUST return a diff --git a/specs/methods/tempo/draft-tempo-subscription-00.md b/specs/methods/tempo/draft-tempo-subscription-00.md index 069863b5..6fe39de9 100644 --- a/specs/methods/tempo/draft-tempo-subscription-00.md +++ b/specs/methods/tempo/draft-tempo-subscription-00.md @@ -12,15 +12,15 @@ author: - name: Jake Moxey ins: J. Moxey email: jake@tempo.xyz - organization: Tempo Labs + org: Tempo Labs - name: Brendan Ryan ins: B. Ryan email: brendan@tempo.xyz - organization: Tempo Labs + org: Tempo Labs - name: Tom Meagher ins: T. Meagher email: thomas@tempo.xyz - organization: Tempo Labs + org: Tempo Labs normative: RFC2119: @@ -31,7 +31,7 @@ normative: RFC8785: I-D.httpauth-payment: title: "The 'Payment' HTTP Authentication Scheme" - target: https://datatracker.ietf.org/doc/draft-ietf-httpauth-payment/ + target: https://datatracker.ietf.org/doc/draft-ryan-httpauth-payment/ author: - name: Jake Moxey date: 2026-01 @@ -236,7 +236,7 @@ be represented in the Tempo key authorization expiry field. "currency": "0x20c0000000000000000000000000000000000001", "periodSeconds": "2592000", "subscriptionExpires": "2026-07-14T12:00:00Z", - "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00", + "recipient": "0x742d35cc6634c0532925a3b844bc9e7595f8fe00", "methodDetails": { "chainId": 42431 } @@ -319,7 +319,7 @@ field. "method": "tempo", "intent": "subscription", "request": "eyJ...", - "expires": "2025-02-05T12:05:00Z" + "expires": "2026-01-15T12:05:00Z" }, "payload": { "signature": "0xf8c1...signed authorization bytes...", @@ -395,6 +395,7 @@ including at least: - subscription identifier - billing anchor - last charged billing-period index +- any in-flight billing-period index and renewal transaction identifier - subscription expiry - revocation status @@ -404,8 +405,12 @@ When granting access in a later billing period, servers MUST: - Determine the current billing-period index from the anchor and `periodSeconds` - Verify that the current billing period has not already been charged -- Atomically record the current billing period as charged before, or - atomically with, delivering the corresponding service +- Atomically record any renewal attempt for the current billing period + as in-flight before submitting the renewal transaction +- Mark the current billing period as charged only after the renewal + transaction settles successfully +- Grant access only after, or atomically with, durably recording the + successful renewal charge For duplicate idempotent requests, servers MUST NOT charge the same billing period more than once. @@ -458,7 +463,7 @@ include a `Payment-Receipt` header on error responses. On activation, servers MUST include the `subscriptionId` defined by {{I-D.payment-intent-subscription}} in the receipt. On renewal, servers -SHOULD return the same `subscriptionId` for the active subscription. +MUST return the same `subscriptionId` for the active subscription. The receipt payload for Tempo subscription: @@ -570,7 +575,7 @@ WWW-Authenticate: Payment id="qT8wErYuI3oPlKjH6gFdSa", realm="api.example.com", method="tempo", intent="subscription", - expires="2025-02-05T12:05:00Z", + expires="2026-01-15T12:05:00Z", request="" ~~~ @@ -582,7 +587,7 @@ The `request` decodes to: "currency": "0x20c0000000000000000000000000000000000001", "periodSeconds": "2592000", "subscriptionExpires": "2026-07-14T12:00:00Z", - "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00", + "recipient": "0x742d35cc6634c0532925a3b844bc9e7595f8fe00", "methodDetails": { "chainId": 42431 } @@ -602,7 +607,7 @@ seconds until 2026-07-14T12:00:00Z. "method": "tempo", "intent": "subscription", "request": "eyJ...", - "expires": "2025-02-05T12:05:00Z" + "expires": "2026-01-15T12:05:00Z" }, "payload": { "signature": "0xf8c1...signed authorization bytes...", From 79ad5b83ae7d4611a0c8c88b6e51581f851aa0b6 Mon Sep 17 00:00:00 2001 From: Brendan Ryan <1572504+brendanjryan@users.noreply.github.com> Date: Wed, 6 May 2026 11:45:32 -0700 Subject: [PATCH 15/16] docs: tighten subscription renewal bindings --- .../draft-payment-intent-subscription-00.md | 22 ++++++++----- .../stripe/draft-stripe-subscription-00.md | 12 ++++++- .../tempo/draft-tempo-subscription-00.md | 31 +++++++++++++++++-- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/specs/intents/draft-payment-intent-subscription-00.md b/specs/intents/draft-payment-intent-subscription-00.md index af903f93..1ff2f04a 100644 --- a/specs/intents/draft-payment-intent-subscription-00.md +++ b/specs/intents/draft-payment-intent-subscription-00.md @@ -31,7 +31,7 @@ normative: RFC8785: I-D.httpauth-payment: title: "The 'Payment' HTTP Authentication Scheme" - target: https://datatracker.ietf.org/doc/draft-ryan-httpauth-payment/ + target: https://datatracker.ietf.org/doc/draft-ietf-httpauth-payment/ author: - name: Jake Moxey date: 2026-01 @@ -342,6 +342,10 @@ In particular: "subscriptionExpires": "2026-07-14T12:00:00Z", "recipient": "0x742d35cc6634c0532925a3b844bc9e7595f8fe00", "methodDetails": { + "accessKey": { + "accessKeyAddress": "0x1111111111111111111111111111111111111111", + "keyType": "p256" + }, "chainId": 42431 } } @@ -416,13 +420,17 @@ in the `Payment-Receipt`. The value MUST be a base64url {{RFC4648}} string without padding and MUST be unique within the server's subscription namespace. -Clients MAY retain the `subscriptionId` as application data when -referring to the active subscription in later interactions. Applications -MAY instead use application-defined identifiers or other context to -associate a later request with an existing subscription. - This specification does not define a dedicated request header or -parameter for selecting an existing subscription. +parameter for selecting an existing subscription. Selecting an existing +subscription is an application-layer concern. Applications MAY use +authenticated session state, account identity, resource scope, an +application-defined selector, or other context to associate a later +request with an existing subscription. + +Clients MAY retain the `subscriptionId` as application data when +referring to the active subscription in later interactions, but the +`subscriptionId` is only a receipt identifier unless an application +explicitly assigns it additional application-layer meaning. Servers MUST authenticate or otherwise authorize the client's use of the identified subscription before granting access or collecting a renewal diff --git a/specs/methods/stripe/draft-stripe-subscription-00.md b/specs/methods/stripe/draft-stripe-subscription-00.md index 823f7bfc..bf2bd85e 100644 --- a/specs/methods/stripe/draft-stripe-subscription-00.md +++ b/specs/methods/stripe/draft-stripe-subscription-00.md @@ -25,7 +25,7 @@ normative: RFC8785: I-D.httpauth-payment: title: "The 'Payment' HTTP Authentication Scheme" - target: https://datatracker.ietf.org/doc/draft-ryan-httpauth-payment/ + target: https://datatracker.ietf.org/doc/draft-ietf-httpauth-payment/ author: - name: Jake Moxey date: 2026-01 @@ -422,6 +422,16 @@ collection of older unpaid invoices. If a Stripe recovery or retry flow cannot be mapped exactly to the shared one-charge-per-period invariant, servers MUST disable that flow or reject the request. +Servers MUST prevent Stripe from collecting invoices for canonical +billing periods that are no longer payable under the shared +subscription intent. If a renewal invoice remains unpaid when a later +canonical billing period begins, the server MUST void it, mark it +uncollectible, cancel automatic collection for it, or configure Stripe +retry and recovery behavior so the stale invoice cannot later collect +payment for that closed period. A later paid invoice for an older +canonical billing period MUST NOT be recorded as a successful +subscription charge. + Implementations MUST process Stripe invoice events idempotently by recording the Stripe event ID, invoice ID, subscription ID, and canonical billing-period index. A duplicate webhook or API retry MUST diff --git a/specs/methods/tempo/draft-tempo-subscription-00.md b/specs/methods/tempo/draft-tempo-subscription-00.md index 6fe39de9..111fdbea 100644 --- a/specs/methods/tempo/draft-tempo-subscription-00.md +++ b/specs/methods/tempo/draft-tempo-subscription-00.md @@ -31,7 +31,7 @@ normative: RFC8785: I-D.httpauth-payment: title: "The 'Payment' HTTP Authentication Scheme" - target: https://datatracker.ietf.org/doc/draft-ryan-httpauth-payment/ + target: https://datatracker.ietf.org/doc/draft-ietf-httpauth-payment/ author: - name: Jake Moxey date: 2026-01 @@ -208,6 +208,9 @@ raw string form. | Field | Type | Required | Description | |-------|------|----------|-------------| +| `methodDetails.accessKey` | object | REQUIRED | Access key descriptor that the payer authorizes for subscription renewal charges | +| `methodDetails.accessKey.accessKeyAddress` | string | REQUIRED | Address of the access key to authorize | +| `methodDetails.accessKey.keyType` | string | REQUIRED | Access key type. The value MUST be `p256`, `secp256k1`, or `webAuthn` | | `methodDetails.chainId` | number | OPTIONAL | Tempo chain ID. If omitted, the default value is 42431 (Tempo mainnet). | Servers issuing `intent="subscription"` challenges SHOULD include the @@ -238,6 +241,10 @@ be represented in the Tempo key authorization expiry field. "subscriptionExpires": "2026-07-14T12:00:00Z", "recipient": "0x742d35cc6634c0532925a3b844bc9e7595f8fe00", "methodDetails": { + "accessKey": { + "accessKeyAddress": "0x1111111111111111111111111111111111111111", + "keyType": "p256" + }, "chainId": 42431 } } @@ -246,12 +253,15 @@ be represented in the Tempo key authorization expiry field. The client fulfills this by signing a key authorization with: - Expiry = `subscriptionExpires` +- Access key = `methodDetails.accessKey` - Per-period spending limit = `amount` - Billing period = `periodSeconds` - Destination restriction = `recipient` The signed key authorization MUST additionally configure: +- the exact access-key address and key type from + `methodDetails.accessKey` - a `TokenLimit` for `currency` whose `amount` equals the challenge `amount` and whose `period` equals `periodSeconds` - exactly one `allowed_calls` target scope whose `target` equals @@ -293,7 +303,7 @@ The encoded value MUST be a signed key authorization containing at least: - the Tempo chain ID -- the access-key identifier +- the access-key address and key type - the authorization expiry - the TIP-20 token spending limit - the billing-period limit configuration @@ -432,10 +442,20 @@ address from the signed key authorization using {{TIP-1020}}-compatible verification semantics over the encoded key authorization payload. +If `source` is present, servers MUST verify that it identifies the +recovered root signer on the same chain as `methodDetails.chainId`, or +on chain 42431 when `methodDetails.chainId` is omitted. + ## Authorization Scope Verification When validating a Tempo subscription credential, servers MUST verify that the signed key authorization expiry equals `subscriptionExpires`. +Servers MUST verify that the signed key authorization chain ID equals +`methodDetails.chainId`, or 42431 when `methodDetails.chainId` is +omitted. Servers MUST verify that the signed key authorization +authorizes the exact access key described by +`methodDetails.accessKey`, including both `accessKeyAddress` and +`keyType`. Servers MUST also verify that the authorization contains a spending limit for `currency` whose amount equals `amount` and whose billing period equals `periodSeconds`. @@ -589,6 +609,10 @@ The `request` decodes to: "subscriptionExpires": "2026-07-14T12:00:00Z", "recipient": "0x742d35cc6634c0532925a3b844bc9e7595f8fe00", "methodDetails": { + "accessKey": { + "accessKeyAddress": "0x1111111111111111111111111111111111111111", + "keyType": "p256" + }, "chainId": 42431 } } @@ -642,6 +666,7 @@ The server records at least: - `subscriptionId = "c3ViXzAxMjM0NTY"` - `billing anchor = 2026-01-15T12:03:10Z` - `periodSeconds = 2592000` +- `accessKeyAddress = "0x1111111111111111111111111111111111111111"` - `last charged billing-period index = 0` ## Renewal Across Multiple Periods @@ -659,6 +684,7 @@ authorization: ~~~http GET /api/resource HTTP/1.1 Host: api.example.com +Cookie: session= ~~~ When Period 1 begins, the server determines that billing-period index 1 @@ -697,6 +723,7 @@ that time continue to succeed without another renewal charge: ~~~http GET /api/resource HTTP/1.1 Host: api.example.com +Cookie: session= ~~~ Once `2026-04-15T12:03:10Z` is reached, the server stops submitting From 0e20654cee8c9feea8f92ffe7e0b9e878d71ebe8 Mon Sep 17 00:00:00 2001 From: Brendan Ryan <1572504+brendanjryan@users.noreply.github.com> Date: Wed, 6 May 2026 15:59:55 -0700 Subject: [PATCH 16/16] docs: support calendar subscription periods --- .../draft-payment-intent-subscription-00.md | 75 ++++++++++++------- .../stripe/draft-stripe-subscription-00.md | 56 +++++++------- .../tempo/draft-tempo-subscription-00.md | 55 ++++++++------ 3 files changed, 113 insertions(+), 73 deletions(-) diff --git a/specs/intents/draft-payment-intent-subscription-00.md b/specs/intents/draft-payment-intent-subscription-00.md index 1ff2f04a..c0205889 100644 --- a/specs/intents/draft-payment-intent-subscription-00.md +++ b/specs/intents/draft-payment-intent-subscription-00.md @@ -110,8 +110,8 @@ Subscription billing period. Billing Period -: A fixed-duration window during which at most one subscription charge - may be collected. +: A recurrence window, defined by the subscription period fields, during + which at most one subscription charge may be collected. Activation : The successful initial setup of a subscription, which includes @@ -203,24 +203,36 @@ document those choices explicitly. |-------|------|-------------| | `amount` | string | Fixed payment amount per billing period in base units | | `currency` | string | Currency or asset identifier (see {{currency-formats}}) | -| `periodSeconds` | string | Billing period duration in seconds | +| `periodUnit` | string | Billing period unit. The value MUST be `day`, `week`, or `month` | +| `periodCount` | string | Positive integer count of `periodUnit` values per billing period | The `amount` value MUST be a string representation of a positive integer in base 10 with no sign, decimal point, exponent, or surrounding whitespace. Leading zeros MUST NOT be used. -The `periodSeconds` value MUST be a string representation of a positive +The `periodCount` value MUST be a string representation of a positive integer in base 10 with no sign, decimal point, exponent, or surrounding whitespace. Leading zeros MUST NOT be used. -`periodSeconds` defines fixed-duration billing periods measured in -elapsed seconds. It does not, by itself, encode calendar-month or -calendar-year alignment. +The `periodUnit` value defines how billing-period boundaries are +computed: -Payment methods MUST reject request objects whose `periodSeconds` value -they cannot represent exactly. They MUST NOT approximate the requested -period by rounding, truncating, or substituting a nearby network-native -cadence. +- `day`: fixed elapsed-time periods of `periodCount * 86400` seconds +- `week`: fixed elapsed-time periods of `periodCount * 604800` seconds +- `month`: calendar-month periods anchored at activation + +For `periodUnit="month"`, billing-period boundaries are computed by +adding `N * periodCount` calendar months to the original activation +anchor using UTC calendar fields. If the target month does not contain +the activation anchor's day-of-month, the boundary uses the last valid +day of the target month while preserving the activation time-of-day. +Boundaries MUST be computed from the original activation anchor, not by +adding months to the previous boundary. + +Payment methods MUST reject request objects whose `periodUnit` or +`periodCount` values they cannot represent exactly. They MUST NOT +approximate the requested period by rounding, truncating, or +substituting a nearby network-native cadence. ### Optional Fields @@ -255,8 +267,8 @@ so. The billing anchor for a subscription is the time activation succeeds, or an equivalent network-native timestamp defined by the payment method -specification. Billing periods are contiguous fixed-duration windows -derived by adding `periodSeconds` to that anchor. +specification. Billing periods are contiguous windows derived from that +anchor according to `periodUnit` and `periodCount`. This shared intent does not define deferred starts or merchant-selected billing anchors. A payment method that needs a more specific anchor rule @@ -264,9 +276,10 @@ MUST document it explicitly. The shared fields in this section are the canonical subscription contract. Payment method specifications MUST document how they map -`amount`, `periodSeconds`, and activation to the underlying payment -system. If a payment method cannot represent those fields or semantics -exactly, it MUST reject the request rather than approximate it. +`amount`, `periodUnit`, `periodCount`, and activation to the underlying +payment system. If a payment method cannot represent those fields or +semantics exactly, it MUST reject the request rather than approximate +it. ## Currency Formats {#currency-formats} @@ -302,8 +315,8 @@ In particular: - Methods should support only request shapes they can represent exactly. - Methods should document the supported and rejected ranges or values of - `periodSeconds`, any additional bounded-lifetime or expiry rules they - impose, and what conditions make activation succeed. + `periodUnit` and `periodCount`, any additional bounded-lifetime or + expiry rules they impose, and what conditions make activation succeed. - Activation should not be reported as successful until both subscription setup and the first billing-period charge have succeeded. @@ -327,7 +340,8 @@ In particular: { "amount": "9900", "currency": "usd", - "periodSeconds": "2592000", + "periodUnit": "month", + "periodCount": "1", "description": "Pro plan" } ~~~ @@ -338,7 +352,8 @@ In particular: { "amount": "10000000", "currency": "0x20c0000000000000000000000000000000000001", - "periodSeconds": "2592000", + "periodUnit": "day", + "periodCount": "30", "subscriptionExpires": "2026-07-14T12:00:00Z", "recipient": "0x742d35cc6634c0532925a3b844bc9e7595f8fe00", "methodDetails": { @@ -488,20 +503,21 @@ usable and initiate a new subscription flow. This section is non-normative. -## 30-Day Billing Example +## Monthly Billing Example Suppose a server offers a plan with these request fields: - `amount = "9900"` - `currency = "usd"` -- `periodSeconds = "2592000"` +- `periodUnit = "month"` +- `periodCount = "1"` If activation succeeds at `2026-01-15T12:03:10Z`, that time becomes the billing anchor. The resulting billing periods are: -- Period 0: `[2026-01-15T12:03:10Z, 2026-02-14T12:03:10Z)` -- Period 1: `[2026-02-14T12:03:10Z, 2026-03-16T12:03:10Z)` -- Period 2: `[2026-03-16T12:03:10Z, 2026-04-15T12:03:10Z)` +- Period 0: `[2026-01-15T12:03:10Z, 2026-02-15T12:03:10Z)` +- Period 1: `[2026-02-15T12:03:10Z, 2026-03-15T12:03:10Z)` +- Period 2: `[2026-03-15T12:03:10Z, 2026-04-15T12:03:10Z)` Activation collects the Period 0 charge. Requests during Period 0 do not require another renewal charge. When Period 1 begins, the server @@ -509,6 +525,13 @@ may collect one renewal charge for Period 1 before, or atomically with, granting access for that period. After that renewal succeeds, additional requests during Period 1 do not permit another charge for Period 1. +If the same monthly plan activates at `2026-01-31T12:03:10Z`, the +resulting boundaries are computed from the January 31 anchor: + +- Period 0: `[2026-01-31T12:03:10Z, 2026-02-28T12:03:10Z)` +- Period 1: `[2026-02-28T12:03:10Z, 2026-03-31T12:03:10Z)` +- Period 2: `[2026-03-31T12:03:10Z, 2026-04-30T12:03:10Z)` + ## Cancellation Example Suppose the subscription above has already been charged through Period 2 @@ -559,7 +582,7 @@ Clients MUST verify before activating a subscription: 1. `amount` is acceptable for the service 2. `currency` is expected -3. `periodSeconds` matches the expected billing interval +3. `periodUnit` and `periodCount` match the expected billing interval 4. Any `subscriptionExpires` value and method-specific constraints are understood and acceptable diff --git a/specs/methods/stripe/draft-stripe-subscription-00.md b/specs/methods/stripe/draft-stripe-subscription-00.md index bf2bd85e..735019c8 100644 --- a/specs/methods/stripe/draft-stripe-subscription-00.md +++ b/specs/methods/stripe/draft-stripe-subscription-00.md @@ -173,7 +173,7 @@ base64url-encoded without padding per {{I-D.httpauth-payment}}. ## Request Fields The Stripe `subscription` profile uses the shared `amount`, `currency`, -`periodSeconds`, `description`, and `externalId` fields from +`periodUnit`, `periodCount`, `description`, and `externalId` fields from {{I-D.payment-intent-subscription}}. It additionally defines the following request constraints: @@ -181,7 +181,8 @@ additionally defines the following request constraints: |-------|------|----------|-------------| | `amount` | string | REQUIRED | Fixed payment amount per billing period in the currency's smallest unit | | `currency` | string | REQUIRED | Lowercase ISO 4217 currency code | -| `periodSeconds` | string | REQUIRED | Billing period duration in seconds | +| `periodUnit` | string | REQUIRED | Billing period unit. The value MUST be `day`, `week`, or `month` | +| `periodCount` | string | REQUIRED | Positive integer count of `periodUnit` values per billing period | | `description` | string | OPTIONAL | Human-readable subscription description | | `externalId` | string | OPTIONAL | Merchant's reference for the subscription | | `recipient` | string | MUST NOT | This profile identifies the merchant by the challenged Stripe account and `methodDetails.networkId`, not by a request-native recipient field | @@ -190,7 +191,7 @@ The `amount` value MUST be a string representation of a positive integer in base 10 with no sign, decimal point, exponent, or surrounding whitespace. Leading zeros MUST NOT be used. -The `periodSeconds` value MUST be a string representation of a positive +The `periodCount` value MUST be a string representation of a positive integer in base 10 with no sign, decimal point, exponent, or surrounding whitespace. Leading zeros MUST NOT be used. @@ -224,7 +225,8 @@ access-control decisions. { "amount": "5000", "currency": "usd", - "periodSeconds": "604800", + "periodUnit": "week", + "periodCount": "1", "description": "Weekly Pro plan", "externalId": "sub_12345", "methodDetails": { @@ -247,15 +249,17 @@ Stripe Subscription containing exactly one recurring Stripe Price. The Price MUST have a fixed `unit_amount`, fixed `currency`, and fixed recurring cadence for the full life of the subscription. -The `periodSeconds` field MUST map exactly to a Stripe recurring cadence -using one of the following forms: +The period fields MUST map exactly to a Stripe recurring cadence using +one of the following forms: -- `week`, where `periodSeconds = interval_count * 604800` -- `day`, where `periodSeconds = interval_count * 86400` +- `periodUnit="day"`, where `periodCount` maps to `interval_count` + and Stripe `recurring.interval` is `day` +- `periodUnit="week"`, where `periodCount` maps to `interval_count` + and Stripe `recurring.interval` is `week` +- `periodUnit="month"`, where `periodCount` maps to `interval_count` + and Stripe `recurring.interval` is `month` -If `periodSeconds` is divisible by both values, servers SHOULD prefer -the `week` representation. Servers MUST reject any `periodSeconds` -value that would require approximation, calendar-month interpretation, +Servers MUST reject any period fields that would require approximation, calendar-year interpretation, or an unsupported Stripe interval count. This profile supports only a fixed quantity of 1 for the single @@ -279,9 +283,10 @@ the following create Subscription parameters {{STRIPE-SUBSCRIPTIONS-API}}: Servers MUST create the subscription using an idempotency key bound to the challenge ID, payer, payment method, amount, currency, and -`periodSeconds`. If an idempotent retry returns an existing Stripe -Subscription, the server MUST verify that the existing object still -matches this profile before treating the retry as successful. +`periodUnit` and `periodCount`. If an idempotent retry returns an +existing Stripe Subscription, the server MUST verify that the existing +object still matches this profile before treating the retry as +successful. ## Unsupported Stripe Billing Features @@ -342,7 +347,7 @@ Servers MUST verify Payment credentials for Stripe subscription intent: 1. Verify the challenge ID matches the one issued 2. Verify the challenge has not expired 3. Decode the request object and verify it matches this constrained - profile, including exact `periodSeconds` support + profile, including exact support for `periodUnit` and `periodCount` 4. Extract the `paymentMethod` and optional `customer` from the credential payload 5. Verify the Stripe PaymentMethod exists, is reusable by the @@ -388,8 +393,7 @@ that incomplete Stripe Subscription. The canonical billing anchor for this profile is the start timestamp of the first paid Stripe invoice period. Servers MUST use that anchor when -mapping later Stripe invoices to the shared `periodSeconds` billing -periods. +mapping later Stripe invoices to the shared billing periods. Before activating a subscription or recording a renewal, servers MUST validate the paid Stripe invoice. The invoice MUST: @@ -401,7 +405,7 @@ validate the paid Stripe invoice. The invoice MUST: - have no discounts, tax, credits, or prorations that change the amount - match the challenged `amount` and `currency` - map to exactly one canonical billing period derived from the billing - anchor and `periodSeconds` + anchor, `periodUnit`, and `periodCount` - not have already been recorded for another billing period or subscription @@ -410,7 +414,7 @@ validate the paid Stripe invoice. The invoice MUST: Later billing periods are fulfilled by Stripe renewal invoices. Servers MUST use durable local state to map Stripe invoices and webhook events onto canonical billing periods derived from the activation anchor and -`periodSeconds`. +the period fields. Servers MUST treat a later billing period as paid only after they observe a successful paid Stripe invoice for that subscription and @@ -552,7 +556,8 @@ The `request` decodes to: { "amount": "5000", "currency": "usd", - "periodSeconds": "604800", + "periodUnit": "week", + "periodCount": "1", "description": "Weekly Pro plan", "methodDetails": { "networkId": "profile_1MqDcVKA5fEO2tZvKQm9g8Yj", @@ -588,17 +593,18 @@ invoice as paid, the `Payment-Receipt` payload decodes to: ## Rejected Unsupported Cadence -If a request uses a `periodSeconds` value that cannot be represented as -an exact whole number of Stripe `day` or `week` intervals, the server +If a request uses period fields that cannot be represented by the +shared subscription contract and Stripe recurring cadence, the server rejects it rather than approximating. For example, the following request -is invalid for this profile because `90000` seconds is not an exact -whole number of days or weeks: +is invalid for this profile because `year` is not a supported +`periodUnit`: ~~~json { "amount": "5000", "currency": "usd", - "periodSeconds": "90000", + "periodUnit": "year", + "periodCount": "1", "methodDetails": { "networkId": "profile_1MqDcVKA5fEO2tZvKQm9g8Yj", "paymentMethodTypes": ["card"] diff --git a/specs/methods/tempo/draft-tempo-subscription-00.md b/specs/methods/tempo/draft-tempo-subscription-00.md index 111fdbea..3457bdcd 100644 --- a/specs/methods/tempo/draft-tempo-subscription-00.md +++ b/specs/methods/tempo/draft-tempo-subscription-00.md @@ -172,7 +172,7 @@ base64url-encoded without padding per {{I-D.httpauth-payment}}. ## Request Fields -Tempo uses the shared `amount`, `currency`, `periodSeconds`, +Tempo uses the shared `amount`, `currency`, `periodUnit`, `periodCount`, `subscriptionExpires`, `recipient`, `description`, and `externalId` fields from {{I-D.payment-intent-subscription}}. Tempo additionally requires `subscriptionExpires` because Tempo key authorizations must @@ -182,7 +182,8 @@ expire: |-------|------|----------|-------------| | `amount` | string | REQUIRED | Fixed payment amount per billing period in base units | | `currency` | string | REQUIRED | TIP-20 token address | -| `periodSeconds` | string | REQUIRED | Billing period duration in seconds | +| `periodUnit` | string | REQUIRED | Billing period unit. The value MUST be `day` or `week` | +| `periodCount` | string | REQUIRED | Positive integer count of `periodUnit` values per billing period | | `subscriptionExpires` | string | REQUIRED | Subscription expiry timestamp in {{RFC3339}} format | | `recipient` | string | REQUIRED | Recipient address authorized for subscription charges | | `description` | string | OPTIONAL | Human-readable subscription description | @@ -192,7 +193,7 @@ The `amount` value MUST be a string representation of a positive integer in base 10 with no sign, decimal point, exponent, or surrounding whitespace. Leading zeros MUST NOT be used. -The `periodSeconds` value MUST be a string representation of a positive +The `periodCount` value MUST be a string representation of a positive integer in base 10 with no sign, decimal point, exponent, or surrounding whitespace. Leading zeros MUST NOT be used. @@ -224,12 +225,18 @@ MUST be strictly later than the challenge `expires` timestamp. Servers MUST reject credentials where `subscriptionExpires` is at or before the challenge `expires`. -Tempo subscriptions map `periodSeconds` to the {{TIP-1011}} `TokenLimit` -`period` field and map `subscriptionExpires` to the Tempo key -authorization expiry field. Servers MUST reject request objects where -`periodSeconds` cannot be represented as an unsigned 64-bit integer. -Servers MUST reject request objects where `subscriptionExpires` cannot -be represented in the Tempo key authorization expiry field. +Tempo subscriptions map the shared period fields to the {{TIP-1011}} +`TokenLimit` `period` field as follows: + +- `periodUnit="day"` maps to `periodCount * 86400` seconds +- `periodUnit="week"` maps to `periodCount * 604800` seconds + +Servers MUST reject `periodUnit="month"` because {{TIP-1011}} periodic +token limits are fixed elapsed-time periods and cannot represent +calendar-month billing exactly. Servers MUST reject request objects +where the mapped period cannot be represented as an unsigned 64-bit +integer. Servers MUST reject request objects where `subscriptionExpires` +cannot be represented in the Tempo key authorization expiry field. **Example:** @@ -237,7 +244,8 @@ be represented in the Tempo key authorization expiry field. { "amount": "10000000", "currency": "0x20c0000000000000000000000000000000000001", - "periodSeconds": "2592000", + "periodUnit": "day", + "periodCount": "30", "subscriptionExpires": "2026-07-14T12:00:00Z", "recipient": "0x742d35cc6634c0532925a3b844bc9e7595f8fe00", "methodDetails": { @@ -255,7 +263,7 @@ The client fulfills this by signing a key authorization with: - Expiry = `subscriptionExpires` - Access key = `methodDetails.accessKey` - Per-period spending limit = `amount` -- Billing period = `periodSeconds` +- Billing period = mapped period in seconds - Destination restriction = `recipient` The signed key authorization MUST additionally configure: @@ -263,7 +271,7 @@ The signed key authorization MUST additionally configure: - the exact access-key address and key type from `methodDetails.accessKey` - a `TokenLimit` for `currency` whose `amount` equals the challenge - `amount` and whose `period` equals `periodSeconds` + `amount` and whose `period` equals the mapped period in seconds - exactly one `allowed_calls` target scope whose `target` equals `currency` - explicit selector rules for `transfer(address,uint256)` @@ -395,9 +403,9 @@ chain settlement data rather than local wall-clock time. Billing periods are defined as: -- Period 0: `[anchor, anchor + periodSeconds)` -- Period 1: `[anchor + periodSeconds, anchor + 2*periodSeconds)` -- Period N: `[anchor + N*periodSeconds, anchor + (N+1)*periodSeconds)` +- Period 0: `[anchor, anchor + mappedPeriodSeconds)` +- Period 1: `[anchor + mappedPeriodSeconds, anchor + 2*mappedPeriodSeconds)` +- Period N: `[anchor + N*mappedPeriodSeconds, anchor + (N+1)*mappedPeriodSeconds)` Servers MUST maintain durable local state for each subscription, including at least: @@ -413,7 +421,7 @@ When granting access in a later billing period, servers MUST: - Verify the subscription has not expired or been revoked - Determine the current billing-period index from the anchor and - `periodSeconds` + the mapped period in seconds - Verify that the current billing period has not already been charged - Atomically record any renewal attempt for the current billing period as in-flight before submitting the renewal transaction @@ -458,7 +466,7 @@ authorizes the exact access key described by `keyType`. Servers MUST also verify that the authorization contains a spending limit for `currency` whose amount equals `amount` and whose billing -period equals `periodSeconds`. +period equals the mapped period in seconds. Servers MUST verify that the signed key authorization's `allowed_calls` scope: @@ -511,7 +519,7 @@ Clients MUST parse and verify the `request` payload before signing: 1. Verify `amount` is reasonable for the service 2. Verify `currency` is the expected TIP-20 token address -3. Verify `periodSeconds` matches expectations +3. Verify `periodUnit` and `periodCount` match expectations 4. Verify `recipient` is controlled by the expected party 5. Verify `subscriptionExpires` is acceptable @@ -605,7 +613,8 @@ The `request` decodes to: { "amount": "10000000", "currency": "0x20c0000000000000000000000000000000000001", - "periodSeconds": "2592000", + "periodUnit": "day", + "periodCount": "30", "subscriptionExpires": "2026-07-14T12:00:00Z", "recipient": "0x742d35cc6634c0532925a3b844bc9e7595f8fe00", "methodDetails": { @@ -618,8 +627,8 @@ The `request` decodes to: } ~~~ -This requests a recurring payment of 10.00 alphaUSD every 2,592,000 -seconds until 2026-07-14T12:00:00Z. +This requests a recurring payment of 10.00 alphaUSD every 30 days until +2026-07-14T12:00:00Z. **Credential:** @@ -665,7 +674,9 @@ The server records at least: - `subscriptionId = "c3ViXzAxMjM0NTY"` - `billing anchor = 2026-01-15T12:03:10Z` -- `periodSeconds = 2592000` +- `periodUnit = "day"` +- `periodCount = "30"` +- `mappedPeriodSeconds = 2592000` - `accessKeyAddress = "0x1111111111111111111111111111111111111111"` - `last charged billing-period index = 0`