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..c0205889 --- /dev/null +++ b/specs/intents/draft-payment-intent-subscription-00.md @@ -0,0 +1,635 @@ +--- +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-ietf-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. +It standardizes the recurring payment authorization itself, not the full +billing relationship that many application-level systems also call a +subscription. + +--- 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 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 +authorization mechanisms. This document defines the abstract semantics +and shared request fields. Payment method specifications define how +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. + +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} + +# Terminology + +Subscription +: A recurring payment authorization for a fixed amount charged once per + billing period. + +Billing Period +: 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 + collection of 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, preventing future renewals. + +Subscription Identifier +: A server-issued opaque identifier for an activated subscription, + used by servers and applications to refer to that subscription in + later interactions. + +# Intent Semantics + +## Definition + +The "subscription" intent represents a request for a recurring +fixed-amount payment of `amount`, charged once per billing period until +explicit cancellation or until the recurring authorization otherwise +becomes invalid. + +## 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 | + +## 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 +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 + +| Field | Type | Description | +|-------|------|-------------| +| `amount` | string | Fixed payment amount per billing period in base units | +| `currency` | string | Currency or asset identifier (see {{currency-formats}}) | +| `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 `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. + +The `periodUnit` value defines how billing-period boundaries are +computed: + +- `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 + +| 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 | +| `methodDetails` | object | Method-specific extension data | + +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 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}}, +using {{RFC3339}} format. Request objects MUST NOT duplicate the +challenge expiry value. + +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, +or an equivalent network-native timestamp defined by the payment method +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 +MUST document it explicitly. + +The shared fields in this section are the canonical subscription +contract. Payment method specifications MUST document how they map +`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} + +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 only in the +`methodDetails` object. Shared top-level fields retain the meanings +defined in this document. + +## 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 + `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. +- Methods should preserve the shared invariants of one successful charge + per billing period, no automatic accumulation of missed periods, and + 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 + 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 + +~~~ json +{ + "amount": "9900", + "currency": "usd", + "periodUnit": "month", + "periodCount": "1", + "description": "Pro plan" +} +~~~ + +### Blockchain Payment (Tempo) + +~~~ json +{ + "amount": "10000000", + "currency": "0x20c0000000000000000000000000000000000001", + "periodUnit": "day", + "periodCount": "30", + "subscriptionExpires": "2026-07-14T12:00:00Z", + "recipient": "0x742d35cc6634c0532925a3b844bc9e7595f8fe00", + "methodDetails": { + "accessKey": { + "accessKeyAddress": "0x1111111111111111111111111111111111111111", + "keyType": "p256" + }, + "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 payer explicitly cancels it +- The payment method revokes or invalidates the authorization +- The `subscriptionExpires` timestamp, if present, is reached + +# Subscription Lifecycle + +## Activation + +When the server receives a "subscription" credential, it MUST: + +1. Verify the subscription authorization proof +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 +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 +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. + +Payment method specifications define the concrete renewal, retry, +recovery, and cancellation mechanisms, but they MUST preserve the +invariants in this section. + +## 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. + +This specification does not define a dedicated request header or +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 +charge. Possession or presentation of a `subscriptionId` alone is +insufficient. + +## 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 +- Billing anchor or equivalent current billing-period start time +- Last successfully charged billing-period index, or whether the + current billing period has been charged +- Any method-specific expiry or bounded-lifetime state +- 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 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. + +Servers MUST NOT collect renewal charges for billing periods 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 | +|-----------|-------------|----------| +| 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 | + +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. + +# 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"` +- `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-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 +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 +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. + +## Expiry Example + +The shared subscription intent defines `subscriptionExpires` as an +optional top-level field. Some payment methods require it and others +leave it optional. + +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 + +## 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. `periodUnit` and `periodCount` match the expected billing interval +4. Any `subscriptionExpires` value and method-specific constraints are + understood and 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/stripe/draft-stripe-subscription-00.md b/specs/methods/stripe/draft-stripe-subscription-00.md new file mode 100644 index 00000000..735019c8 --- /dev/null +++ b/specs/methods/stripe/draft-stripe-subscription-00.md @@ -0,0 +1,618 @@ +--- +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 + org: Tempo Labs + - name: Steve Kaliski + ins: S. Kaliski + email: stevekaliski@stripe.com + org: 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. + 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 + +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 whose activation +succeeds only when the first invoice is paid synchronously. + +--- 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. + +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: + +~~~ + 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}}. + +## Request Fields + +The Stripe `subscription` profile uses the shared `amount`, `currency`, +`periodUnit`, `periodCount`, `description`, and `externalId` fields from +{{I-D.payment-intent-subscription}}. It +additionally defines the following request constraints: + +| 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 | +| `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 | + +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 `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. + +Servers MUST reject request objects that include `recipient` or +`subscriptionExpires`. + +## 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 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 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:** + +~~~json +{ + "amount": "5000", + "currency": "usd", + "periodUnit": "week", + "periodCount": "1", + "description": "Weekly Pro plan", + "externalId": "sub_12345", + "methodDetails": { + "networkId": "profile_1MqDcVKA5fEO2tZvKQm9g8Yj", + "paymentMethodTypes": ["card", "link"], + "metadata": { + "plan": "weekly-pro" + } + } +} +~~~ + +## 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 period fields MUST map exactly to a Stripe recurring cadence using +one of the following forms: + +- `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` + +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 +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 +`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 + +Servers implementing this profile MUST disable or reject the following +features: + +- free trials +- paid trials +- 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 + +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 | + +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. + +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 +{ + "paymentMethod": "pm_1Qabc32eZvKYlo2C7b8H1234", + "customer": "cus_S7x1Pq5R9n2Lm4" +} +~~~ + +# 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 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 + challenged merchant, has a type allowed by the challenge, and can + 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 +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, 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 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 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, `periodUnit`, and `periodCount` +- not have already been recorded for another billing period or + subscription + +## 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 +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 +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. + +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 +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 +`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", + "periodUnit": "week", + "periodCount": "1", + "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 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 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 `year` is not a supported +`periodUnit`: + +~~~json +{ + "amount": "5000", + "currency": "usd", + "periodUnit": "year", + "periodCount": "1", + "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 new file mode 100644 index 00000000..3457bdcd --- /dev/null +++ b/specs/methods/tempo/draft-tempo-subscription-00.md @@ -0,0 +1,791 @@ +--- +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 + 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-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 + TEMPO-ACCOUNT-KEYCHAIN: + title: "Account Keychain Precompile" + 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 + 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. This profile intentionally models the recurring +transfer authorization itself, not a richer billing object. + +--- 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. + +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 +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 + │ │ │ + │ (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 {{TEMPO-ACCOUNT-KEYCHAIN}}. + +# 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}}. + +## Request Fields + +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 +expire: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `amount` | string | REQUIRED | Fixed payment amount per billing period in base units | +| `currency` | string | REQUIRED | TIP-20 token address | +| `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 | +| `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 `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. + +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 | +|-------|------|----------|-------------| +| `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 +`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. + +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`. + +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:** + +~~~json +{ + "amount": "10000000", + "currency": "0x20c0000000000000000000000000000000000001", + "periodUnit": "day", + "periodCount": "30", + "subscriptionExpires": "2026-07-14T12:00:00Z", + "recipient": "0x742d35cc6634c0532925a3b844bc9e7595f8fe00", + "methodDetails": { + "accessKey": { + "accessKeyAddress": "0x1111111111111111111111111111111111111111", + "keyType": "p256" + }, + "chainId": 42431 + } +} +~~~ + +The client fulfills this by signing a key authorization with: + +- Expiry = `subscriptionExpires` +- Access key = `methodDetails.accessKey` +- Per-period spending limit = `amount` +- Billing period = mapped period in seconds +- 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 the mapped period in seconds +- 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 +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 address and key type +- the authorization expiry +- the TIP-20 token spending limit +- the billing-period limit configuration +- the recipient restriction +- 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 +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": "2026-01-15T12:05:00Z" + }, + "payload": { + "signature": "0xf8c1...signed authorization bytes...", + "type": "keyAuthorization" + }, + "source": "did:pkh:eip155:42431:0x1234567890abcdef1234567890abcdef12345678" +} +~~~ + +# 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 + | | | + | (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. + +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 +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 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: + +- 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: + +- subscription identifier +- billing anchor +- last charged billing-period index +- any in-flight billing-period index and renewal transaction identifier +- 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 + 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 +- 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. + +{{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 +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. + +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 the mapped period in seconds. + +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 +`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 +MUST return the same `subscriptionId` for the active subscription. + +The receipt payload for Tempo subscription: + +| Field | Type | Description | +|-------|------|-------------| +| `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 | + +# Security Considerations + +## Destination Scoping + +Tempo subscription access keys MUST be restricted to the `recipient` +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 + +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 `periodUnit` and `periodCount` match 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 {{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 +{{TEMPO-ACCOUNT-KEYCHAIN}}. Servers MUST implement durable local state +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. + +## 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 +`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 + +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="tempo", + intent="subscription", + expires="2026-01-15T12:05:00Z", + request="" +~~~ + +The `request` decodes to: + +~~~json +{ + "amount": "10000000", + "currency": "0x20c0000000000000000000000000000000000001", + "periodUnit": "day", + "periodCount": "30", + "subscriptionExpires": "2026-07-14T12:00:00Z", + "recipient": "0x742d35cc6634c0532925a3b844bc9e7595f8fe00", + "methodDetails": { + "accessKey": { + "accessKeyAddress": "0x1111111111111111111111111111111111111111", + "keyType": "p256" + }, + "chainId": 42431 + } +} +~~~ + +This requests a recurring payment of 10.00 alphaUSD every 30 days until +2026-07-14T12:00:00Z. + +**Credential:** + +~~~json +{ + "challenge": { + "id": "qT8wErYuI3oPlKjH6gFdSa", + "realm": "api.example.com", + "method": "tempo", + "intent": "subscription", + "request": "eyJ...", + "expires": "2026-01-15T12:05:00Z" + }, + "payload": { + "signature": "0xf8c1...signed authorization bytes...", + "type": "keyAuthorization" + }, + "source": "did:pkh:eip155:42431:0x1234567890abcdef1234567890abcdef12345678" +} +~~~ + +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` +- `periodUnit = "day"` +- `periodCount = "30"` +- `mappedPeriodSeconds = 2592000` +- `accessKeyAddress = "0x1111111111111111111111111111111111111111"` +- `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 +Cookie: session= +~~~ + +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 +Cookie: session= +~~~ + +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 +specification.