|
|
|
@@ -1,14 +1,14 @@
|
|
|
|
|
---
|
|
|
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
|
title: Billing
|
|
|
|
|
description: Checkout, card preapproval, gift purchase, age verification, refunds, and the Stripe webhook.
|
|
|
|
|
description: Checkout, gift purchase, age verification, refunds, and the Stripe webhook.
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
import RouteHeader from '@/components/RouteHeader.astro';
|
|
|
|
|
|
|
|
|
|
These routes take payment for premium, verify an account holder's age, and refund the most recent purchase. Checkout, card preapproval and age verification each finish on the payment provider's own pages, so those routes create the provider session and return its URL for the browser to open. [Receive Stripe webhook](#receive-stripe-webhook) takes the signed events the provider sends back. The [Premium resource](/http-api/premium/) owns entitlement state, mirrored billing data and subscription self-service.
|
|
|
|
|
These routes take payment for premium, verify an account holder's age, and refund the most recent purchase. Checkout and age verification each finish on the payment provider's own pages, so those routes create the provider session and return its URL for the browser to open. [Receive Stripe webhook](#receive-stripe-webhook) takes the signed events the provider sends back. The [Premium resource](/http-api/premium/) owns entitlement state, mirrored billing data and subscription self-service.
|
|
|
|
|
|
|
|
|
|
A self-hosted deployment never serves age verification or the refund routes, serves the checkout routes only while billing is active, and serves [receive Stripe webhook](#receive-stripe-webhook) while Stripe is serviceable, as [deployment availability](/http-api/deployment-availability/) describes. All of them are user-only except [receive Stripe webhook](#receive-stripe-webhook) and [continue localised card preapproval](#continue-localised-card-preapproval).
|
|
|
|
|
A self-hosted deployment never serves age verification or the refund routes, serves the checkout routes only while billing is active, and serves [receive Stripe webhook](#receive-stripe-webhook) while Stripe is serviceable, as [deployment availability](/http-api/deployment-availability/) describes. All of them are user-only except [receive Stripe webhook](#receive-stripe-webhook).
|
|
|
|
|
|
|
|
|
|
:::note[Every response is a Fluxer object]
|
|
|
|
|
The responses are Fluxer redirect, eligibility, refund and acknowledgement objects. They copy only a few payment provider values, such as the identifiers on a [refund](#refund-object) and the `invoice_id` of a [refund eligibility](#refund-eligibility-object) object.
|
|
|
|
@@ -16,7 +16,7 @@ The responses are Fluxer redirect, eligibility, refund and acknowledgement objec
|
|
|
|
|
|
|
|
|
|
## Redirect URL object
|
|
|
|
|
|
|
|
|
|
One absolute URL that completes a billing operation in a browser. [Create subscription checkout](#create-subscription-checkout), [create localised card preapproval](#create-localised-card-preapproval), [create gift checkout](#create-gift-checkout) and [create age verification session](#create-age-verification-session) all answer with it. The object has no session identifier, so a client sends the browser to the returned value.
|
|
|
|
|
One absolute URL that completes a billing operation in a browser. [Create subscription checkout](#create-subscription-checkout), [create gift checkout](#create-gift-checkout) and [create age verification session](#create-age-verification-session) all answer with it. The object has no session identifier, so a client sends the browser to the returned value.
|
|
|
|
|
|
|
|
|
|
### Structure
|
|
|
|
|
|
|
|
|
@@ -34,55 +34,6 @@ One absolute URL that completes a billing operation in a browser. [Create subscr
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Localised card preapproval result object
|
|
|
|
|
|
|
|
|
|
The state of one card preapproval flow. A localised recurring price is offered only to a card issued in the matching country. Fluxer creates the paid session only after a separate setup mode session, which takes no payment, has shown that the card was issued in the matching country. `status` says which of the variants the object is, and each variant defines its own members.
|
|
|
|
|
|
|
|
|
|
### Structure
|
|
|
|
|
|
|
|
|
|
| Field | Type | Description |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| status | string | [Preapproval status](#preapproval-statuses) |
|
|
|
|
|
| url<sup>1</sup> | string | Paid checkout URL |
|
|
|
|
|
| reason<sup>2</sup> | string | [Preapproval rejection reason](#preapproval-rejection-reasons) |
|
|
|
|
|
| actual_country?<sup>3</sup> | ?string | Detected two-letter card issuing country |
|
|
|
|
|
|
|
|
|
|
<sup>1</sup> Present and required only when `status` is `ready`. It is stable for the lifetime of the continuation token
|
|
|
|
|
|
|
|
|
|
<sup>2</sup> Present and required only when `status` is `rejected`
|
|
|
|
|
|
|
|
|
|
<sup>3</sup> Defined only inside the `rejected` variant, where it is optional. It has a value only for `country_mismatch`, and is null there when the card reported no issuing country
|
|
|
|
|
|
|
|
|
|
### Example
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"status": "rejected",
|
|
|
|
|
"reason": "country_mismatch",
|
|
|
|
|
"actual_country": "PT"
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Preapproval statuses
|
|
|
|
|
|
|
|
|
|
| Value | Description |
|
|
|
|
|
| --- | --- |
|
|
|
|
|
| pending | Setup has not completed, or another [continue localised card preapproval](#continue-localised-card-preapproval) call for this token is still running |
|
|
|
|
|
| ready | The card was approved and the paid checkout URL is available |
|
|
|
|
|
| rejected | The card was refused and the paid checkout cannot continue |
|
|
|
|
|
| expired | The token is empty, unknown, or has passed its one-day lifetime |
|
|
|
|
|
|
|
|
|
|
## Preapproval rejection reasons
|
|
|
|
|
|
|
|
|
|
| Value | Description |
|
|
|
|
|
| --- | --- |
|
|
|
|
|
| country_mismatch | Card issuing country does not match the country recorded on the preapproval session |
|
|
|
|
|
| missing_customer | Preapproval session metadata had no payment provider customer |
|
|
|
|
|
| missing_payment_method | Completed setup intent resolved no payment method |
|
|
|
|
|
| missing_setup_intent | Completed session had no setup intent |
|
|
|
|
|
| payment_method_not_card | Approved payment method is not a card |
|
|
|
|
|
| unknown | Preapproval failed without a more specific public reason |
|
|
|
|
|
|
|
|
|
|
## Refund eligibility object
|
|
|
|
|
|
|
|
|
|
Whether the account's most recent purchase can still be refunded without operator involvement. When no refundable purchase was resolved, `eligible` and `cancels_subscription` are false. `invoice_id`, `invoice_amount_paid_cents`, `currency`, `paid_at` and `refund_window_expires_at` are null. `cooldown_expires_at` still reports an active cooldown unless the reason is `feature_unavailable`. A client reads `eligible` and `reason` before rendering any amount or timestamp.
|
|
|
|
@@ -225,10 +176,6 @@ An account already holding an `active` or `trialing` subscription can submit a r
|
|
|
|
|
|
|
|
|
|
The conversion requires a stored billing cycle on the account that differs from the submitted price's cycle. Any other blocking subscription status, no stored billing cycle, and a matching cycle all still produce 403 `PREMIUM_PURCHASE_BLOCKED`. When scheduling the change fails, the caller receives that failure.
|
|
|
|
|
|
|
|
|
|
:::note[Only this route converts a blocked purchase]
|
|
|
|
|
A blocked purchase always fails on [create localised card preapproval](#create-localised-card-preapproval) and [continue localised card preapproval](#continue-localised-card-preapproval).
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
|
|
### JSON body
|
|
|
|
|
|
|
|
|
|
| Field | Type | Description |
|
|
|
|
@@ -271,102 +218,6 @@ Premium entitlement otherwise changes only after the matching signed event reach
|
|
|
|
|
|
|
|
|
|
3 requests per minute for each authenticated user, on the `stripe:checkout:subscription` bucket.
|
|
|
|
|
|
|
|
|
|
## Create localised card preapproval
|
|
|
|
|
|
|
|
|
|
<RouteHeader method="POST" path="/v1/stripe/checkout/subscription/preapproval" />
|
|
|
|
|
|
|
|
|
|
Creates a setup mode session that captures and verifies a card before a localised recurring purchase, and returns a [redirect URL](#redirect-url-object) object.
|
|
|
|
|
|
|
|
|
|
### Limitations
|
|
|
|
|
|
|
|
|
|
- The claimed account, verified email and purchase flag requirements of [create subscription checkout](#create-subscription-checkout) apply unchanged, with the same codes.
|
|
|
|
|
- A request that Fluxer cannot geolocate and that omits `country_code` returns 400 `STRIPE_INVALID_PRODUCT_CONFIGURATION`. A resolved price that is not a recurring price in a currency other than USD and EUR also returns 400 `STRIPE_INVALID_PRODUCT_CONFIGURATION`.
|
|
|
|
|
- The submitted price is always recurring, so the lifetime block and the existing subscription block both apply and return 403 `PREMIUM_PURCHASE_BLOCKED` with the reason `lifetime` or `existing_subscription`.
|
|
|
|
|
|
|
|
|
|
### JSON body
|
|
|
|
|
|
|
|
|
|
| Field | Type | Description |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| price_id | string | Registered localised recurring price ID (1-256 characters) |
|
|
|
|
|
| country_code?<sup>1</sup> | string | Two-letter country used to select the regional price catalogue (2 characters) |
|
|
|
|
|
| client_geoip_country_code? | string | Two-letter country the client previously observed for itself (2 characters) |
|
|
|
|
|
| eu_withdrawal_waiver_accepted? | boolean | Whether the digital content withdrawal waiver was explicitly accepted |
|
|
|
|
|
| is_business? | boolean | Whether to require a billing address for tax invoicing (default false) |
|
|
|
|
|
|
|
|
|
|
<sup>1</sup> Declared optional by the shared request schema, and this operation refuses a request that resolves no country. The request-time geolocation country wins, so this value is read only when Fluxer cannot geolocate the request
|
|
|
|
|
|
|
|
|
|
A submitted `payment_method` is validated against the enum and then discarded. The setup session is always restricted to cards.
|
|
|
|
|
|
|
|
|
|
### Response
|
|
|
|
|
|
|
|
|
|
| Status | Body | Condition |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| 200 | [redirect URL](#redirect-url-object) object | The setup session was created |
|
|
|
|
|
| 400<sup>2</sup> | [error response](/http-api/#error-response) | The country, the price, the account, or the payment provider blocks the setup session |
|
|
|
|
|
| 403 | [error response](/http-api/#error-response) | The email address is unverified, or the purchase is blocked |
|
|
|
|
|
| 404 | [error response](/http-api/#error-response) | The authenticated account record no longer exists |
|
|
|
|
|
|
|
|
|
|
<sup>2</sup> The country is absent, the price is unknown or not valid for the resolved catalogue, the account is unclaimed, the payment provider is not configured, or the payment provider rejected the request and returns `STRIPE_ERROR`
|
|
|
|
|
|
|
|
|
|
### Side effects
|
|
|
|
|
|
|
|
|
|
The returned URL opens a card-only setup flow. Its success callback includes the continuation token. The flow expires one day after its most recent state change.
|
|
|
|
|
|
|
|
|
|
The flow stays pending until [receive Stripe webhook](#receive-stripe-webhook) processes the matching completion event. It is approved only when the card issuing country equals the pricing country. Otherwise the flow records a [preapproval rejection reason](#preapproval-rejection-reasons), and for `country_mismatch` the detected country. No entitlement changes.
|
|
|
|
|
|
|
|
|
|
### Rate limit
|
|
|
|
|
|
|
|
|
|
5 requests per minute for each authenticated user, on the `stripe:checkout:subscription:preapproval` bucket.
|
|
|
|
|
|
|
|
|
|
## Continue localised card preapproval
|
|
|
|
|
|
|
|
|
|
<RouteHeader method="POST" path="/v1/stripe/checkout/subscription/preapproval/continue" unauthenticated />
|
|
|
|
|
|
|
|
|
|
Reports the current state of a preapproval flow and creates the paid checkout session once the card is approved, returning a [localised card preapproval result](#localised-card-preapproval-result-object) object.
|
|
|
|
|
|
|
|
|
|
The continuation token is the credential. Fluxer selects the flow from the token alone, and a credential supplied on the request selects no flow.
|
|
|
|
|
|
|
|
|
|
An empty token, an unknown token, and a token whose one-day flow has expired are all reported as `expired`, so the operation never discloses whether a flow exists.
|
|
|
|
|
|
|
|
|
|
### Limitations
|
|
|
|
|
|
|
|
|
|
- A recorded price that is no longer registered returns 400 `STRIPE_INVALID_PRODUCT`.
|
|
|
|
|
- A recorded price that no longer belongs to the recorded country catalogue returns 400 `STRIPE_INVALID_PRODUCT_CONFIGURATION`.
|
|
|
|
|
- The claimed account, verified email, purchase flag, lifetime and existing subscription refusals all apply to the account that opened the flow.
|
|
|
|
|
|
|
|
|
|
To create the paid session, Fluxer runs every check of [create subscription checkout](#create-subscription-checkout) and creates a checkout session the same way, using the account, price and country recorded on the flow.
|
|
|
|
|
|
|
|
|
|
:::caution[Approval is resolved once]
|
|
|
|
|
Only one call at a time creates the paid session for an approved flow. While one call creates the paid session, another call for the same flow is reported as `pending`.
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
|
|
### JSON body
|
|
|
|
|
|
|
|
|
|
| Field | Type | Description |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| token | string | Continuation token issued by [create localised card preapproval](#create-localised-card-preapproval) (1-256 characters) |
|
|
|
|
|
|
|
|
|
|
### Response
|
|
|
|
|
|
|
|
|
|
| Status | Body | Condition |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| 200 | [localised card preapproval result](#localised-card-preapproval-result-object) object | The current flow state was returned |
|
|
|
|
|
| 400<sup>1</sup> | [error response](/http-api/#error-response) | The recorded price, the recorded account, or the payment provider blocks the flow |
|
|
|
|
|
| 403 | [error response](/http-api/#error-response) | The recorded account has an unverified email address, or its purchase is blocked |
|
|
|
|
|
| 404 | [error response](/http-api/#error-response) | The recorded account record no longer exists |
|
|
|
|
|
|
|
|
|
|
<sup>1</sup> The recorded price is unknown or is no longer valid for the recorded country catalogue, the recorded account is unclaimed, the payment provider is not configured, or the payment provider rejected the paid session and returns `STRIPE_ERROR`
|
|
|
|
|
|
|
|
|
|
### Side effects
|
|
|
|
|
|
|
|
|
|
A `pending`, `rejected` or `expired` result changes nothing. For an approved flow, Fluxer attempts to make the approved card the customer's default invoice payment method, then creates the paid checkout even when that update is unavailable.
|
|
|
|
|
|
|
|
|
|
Fluxer runs the checks of [create subscription checkout](#create-subscription-checkout) and creates the checkout session with the approved price and country. The same token then always resolves to the same paid checkout URL. Entitlement changes only after the matching signed event reaches [receive Stripe webhook](#receive-stripe-webhook).
|
|
|
|
|
|
|
|
|
|
### Rate limit
|
|
|
|
|
|
|
|
|
|
30 requests per minute for each client IP address, or for the authenticated user when a credential happens to be supplied, on the `stripe:checkout:subscription:preapproval:continue` bucket.
|
|
|
|
|
|
|
|
|
|
## Create gift checkout
|
|
|
|
|
|
|
|
|
|
<RouteHeader method="POST" path="/v1/stripe/checkout/gift" />
|
|
|
|
|