mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
feat(premium): let self-hosted instances sell premium and gifts (#3025)
This commit is contained in:
@@ -10,8 +10,8 @@ Admin gift code generation issues [premium](/http-api/premium/) codes in bulk, e
|
||||
|
||||
This resource has that one operation. No Admin operation lists, revokes, or redeems a code, or reports who redeemed one.
|
||||
|
||||
:::caution[A self-hosted instance answers 403]
|
||||
The error code is `FEATURE_NOT_AVAILABLE_SELF_HOSTED`. Body validation runs first, so a self-hosted instance answers a body that fails validation with 400 `INVALID_FORM_BODY`.
|
||||
:::caution[A self-hosted instance needs the `mirror` premium mode]
|
||||
A self-hosted instance whose [premium mode](/admin-api/instance/#premium-modes) is `everyone` answers 403 `FEATURE_NOT_AVAILABLE_SELF_HOSTED`. Body validation runs first, so it answers a body that fails validation with 400 `INVALID_FORM_BODY`. Generated codes need no payment provider.
|
||||
:::
|
||||
|
||||
## Gift duration units
|
||||
@@ -63,9 +63,9 @@ Generates the requested number of unredeemed gift codes. Requires `gift_codes:ge
|
||||
| Status | Body | Condition |
|
||||
| --- | --- | --- |
|
||||
| 200 | response body | The codes were generated |
|
||||
| 403<sup>1</sup> | [error response](/admin-api/#error-response) | The account does not hold `admin:authenticate`, does not hold `gift_codes:generate`, or the instance is self-hosted |
|
||||
| 403<sup>1</sup> | [error response](/admin-api/#error-response) | The account lacks `admin:authenticate` or `gift_codes:generate`, or the instance is self-hosted in the `everyone` premium mode |
|
||||
|
||||
<sup>1</sup> Returned as `MISSING_PERMISSIONS` without `admin:authenticate`, as `MISSING_ACL` with that but without `gift_codes:generate`, and as `FEATURE_NOT_AVAILABLE_SELF_HOSTED` on a self-hosted instance
|
||||
<sup>1</sup> Returned as `MISSING_PERMISSIONS` without `admin:authenticate`, as `MISSING_ACL` with that but without `gift_codes:generate`, and as `FEATURE_NOT_AVAILABLE_SELF_HOSTED` on a self-hosted instance in the `everyone` premium mode
|
||||
|
||||
:::caution[The codes are returned once]
|
||||
Reading a code back requires presenting it to [Get gift](/http-api/gifts/#get-gift), so a lost response cannot be recovered.
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Instance configuration, branding, registration control, limits, and
|
||||
|
||||
import RouteHeader from '@/components/RouteHeader.astro';
|
||||
|
||||
Instance configuration is everything an operator can change at runtime. It covers single sign-on, Gateway rollout, registration policy, branding and legal links, instance policy, third party integrations, media retention, and the ordered limit configuration. The [Instance](/http-api/instance/) resource serves the subset published to unauthenticated clients.
|
||||
Instance configuration is everything an operator can change at runtime. It covers single sign-on, Gateway rollout, registration policy, branding and legal links, instance policy, third party integrations, premium billing, media retention, and the ordered limit configuration. The [Instance](/http-api/instance/) resource serves the subset published to unauthenticated clients.
|
||||
|
||||
Every write is a merge over the stored configuration, and an omitted key leaves the stored value unchanged. The limit configuration write is the one exception and replaces the stored document.
|
||||
|
||||
@@ -46,6 +46,7 @@ Missing settings use the defaults documented below. Invalid stored configuration
|
||||
| policy | [instance policy](#instance-policy-object) object | Community, direct message, premium, and gating policy |
|
||||
| integrations | [instance integrations](#instance-integrations-object) object | Third party provider settings and their resolved availability |
|
||||
| media | [instance media](#instance-media-object) object | Attachment retention settings |
|
||||
| billing | [instance billing](#instance-billing-object) object | Premium billing settings and the state resolved from them |
|
||||
|
||||
## SSO configuration object
|
||||
|
||||
@@ -381,6 +382,8 @@ Community, direct message, premium and gating policy for the whole deployment.
|
||||
| mirror | Resolve premium access from the account's own entitlement and premium flags |
|
||||
| everyone | Grant premium access to every account on a self-hosted deployment |
|
||||
|
||||
A self-hosted deployment issues gift codes and sells premium only in `mirror`.
|
||||
|
||||
## Deferred phone gate object
|
||||
|
||||
A rule for accounts whose phone verification requirement was deferred. When such an account joins a guild within `window_hours` of registration, and the guild has the `DISCOVERABLE` feature or more than `member_threshold` members, Fluxer refuses the join until the account verifies a phone.
|
||||
@@ -464,6 +467,58 @@ Client identity the deployment presents to Bluesky, and the number of signing ke
|
||||
|
||||
Signing keys are write-only and must have unique identifiers.
|
||||
|
||||
## Instance billing object
|
||||
|
||||
Stripe billing for the premium tier. Each stored field is nullable, and null falls back to the [deployment configuration](/operator/configuration/#payments).
|
||||
|
||||
### Structure
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| enabled | ?boolean | Operator override of the billing switch, or null for no override |
|
||||
| effective_enabled | boolean | Whether billing is switched on once the override and the deployment configuration are combined |
|
||||
| stripe_secret_key_set | boolean | Whether a Stripe secret key is stored or supplied by deployment configuration |
|
||||
| stripe_webhook_secret_set | boolean | Whether a Stripe webhook secret is stored or supplied by deployment configuration |
|
||||
| stripe_secret_key_stored | boolean | Whether a Stripe secret key is stored, not counting deployment configuration |
|
||||
| stripe_webhook_secret_stored | boolean | Whether a Stripe webhook secret is stored, not counting deployment configuration |
|
||||
| automatic_tax<sup>4</sup> | ?boolean | Stored switch for Stripe automatic tax at checkout, or null for the default |
|
||||
| tax_id_collection<sup>4</sup> | ?boolean | Stored switch for collecting a buyer tax ID at checkout, or null for the default |
|
||||
| terms_consent_required<sup>4</sup> | ?boolean | Stored switch for requiring terms of service consent at checkout, or null for the default |
|
||||
| effective_automatic_tax | boolean | Whether checkout uses Stripe automatic tax |
|
||||
| effective_tax_id_collection | boolean | Whether checkout collects a buyer tax ID |
|
||||
| effective_terms_consent_required | boolean | Whether checkout requires terms of service consent |
|
||||
| default_currency | ?string | Currency used when no country mapping applies, or null |
|
||||
| prices<sup>1</sup> | ?object | Stored [billing price sets](#billing-price-set-object) keyed by currency, or null to use the deployment configuration |
|
||||
| country_currencies | ?object | Stored map of two-letter country codes to currencies, or null |
|
||||
| legacy_prices<sup>2</sup> | ?object | Stored map of legacy price slots to arrays of Stripe price IDs, or null to use the deployment configuration |
|
||||
| billing_active<sup>3</sup> | boolean | Whether premium purchases are available |
|
||||
| stripe_serviceable<sup>5</sup> | boolean | Whether existing Stripe subscriptions can be managed, cancelled and billed |
|
||||
| catalog_mode | string | `operator` while `prices` is stored, otherwise `env` |
|
||||
| webhook_url | string | The URL to register as the Stripe webhook endpoint |
|
||||
|
||||
<sup>1</sup> Stored prices replace the deployment catalogue entirely and take any three-letter currency code. A buyer gets the currency mapped to their country, then `default_currency`, then the first configured currency. Fluxer's country checks for localised currencies do not apply to them
|
||||
|
||||
<sup>2</sup> A slot is `monthly`, `yearly`, `gift_1_month`, or `gift_1_year`, an underscore, and a currency, such as `monthly_GBP`. Existing subscriptions on a listed price keep renewing
|
||||
|
||||
<sup>3</sup> True when billing is switched on, a Stripe secret key is set, at least one currency has both a monthly and a yearly price, and premium tiering is in force. Premium tiering is always in force on a hosted deployment, and on a self-hosted one only in the `mirror` [premium mode](#premium-modes)
|
||||
|
||||
<sup>4</sup> A hosted deployment always turns all three on. A self-hosted deployment defaults all three to off. Automatic tax needs Stripe Tax set up, and terms consent needs a terms of service URL in the Stripe public details
|
||||
|
||||
<sup>5</sup> True while a Stripe secret key is set and premium tiering is in force. On a hosted deployment billing must also be switched on. Only the checkout routes follow `billing_active`
|
||||
|
||||
## Billing price set object
|
||||
|
||||
The Stripe price IDs for one currency. Each is nullable.
|
||||
|
||||
### Structure
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| monthly | ?string | Price ID of the monthly recurring plan |
|
||||
| yearly | ?string | Price ID of the yearly recurring plan |
|
||||
| gift_1_month | ?string | Price ID of the one-month gift |
|
||||
| gift_1_year | ?string | Price ID of the one-year gift |
|
||||
|
||||
## Instance media object
|
||||
|
||||
Attachment retention overrides the operator has set, and the values in force.
|
||||
@@ -642,10 +697,11 @@ The body has one optional object for each section. Fluxer leaves an absent secti
|
||||
| integrations?<sup>3</sup> | object | `gif`, `youtube`, `captcha`, `email`, and `bluesky` sub-objects, the last of which also has the `keys` array |
|
||||
| media? | object | `attachment_decay` overrides, each nullable to restore the deployment default |
|
||||
| policy? | [instance policy update](#instance-policy-update-structure) object | Community, direct message, premium, and gating policy |
|
||||
| billing?<sup>4</sup> | object | Stored [instance billing](#instance-billing-object) fields, with `stripe_secret_key` and `stripe_webhook_secret` for the secrets |
|
||||
|
||||
<sup>1</sup> The supplied fields are merged over the stored configuration and the result is validated as a whole. Every supplied endpoint URL uses `https`, has no credentials and no fragment, and resolves to a publicly routable address, otherwise the request returns 400 `INVALID_FORM_BODY` with `INVALID_URL_FORMAT` or `URL_NOT_PUBLICLY_ROUTABLE`
|
||||
|
||||
<sup>2</sup> `branding.product_name` is 1 to 80 characters, every branding and legal URL is at most 2048 characters and nullable, `branding.theme_color` is at most 64 characters and nullable, and `setup.configured` and `registration.collect_date_of_birth` are booleans. Every string is trimmed before it is stored
|
||||
<sup>2</sup> `branding.product_name` is 1 to 80 characters, every branding and legal URL is at most 2048 characters and nullable, `branding.theme_color` is at most 64 characters and nullable, and `setup.configured` and `registration.collect_date_of_birth` are booleans. `branding.premium_product_name` is 1 to 40 characters, and null restores the default. `branding.premium_info_url` is an absolute `http` or `https` URL. Every string is trimmed before it is stored
|
||||
|
||||
`voice_noise_suppression` takes every [voice noise suppression configuration](#voice-noise-suppression-configuration-object) field except `config_version`, each bound as documented there. Fluxer raises `config_version` by one on each request that supplies at least one of them. A section that is absent, or present with no field set, writes nothing and leaves `config_version` alone.
|
||||
|
||||
@@ -661,6 +717,8 @@ The body has one optional object for each section. Fluxer leaves an absent secti
|
||||
|
||||
<sup>3</sup> A secret such as `klipy_api_key`, `api_key`, `hcaptcha_secret_key`, `turnstile_secret_key`, or the SMTP `password` is written when supplied and left alone when absent. `integrations.bluesky.keys` is the only way to write the Bluesky signing keys counted as `bluesky.key_count`. It takes up to 8 entries of `kid` (1-255 characters) and nullable `private_key` (up to 10000 characters), and replaces the stored key set outright
|
||||
|
||||
<sup>4</sup> Accepted only on a self-hosted deployment. A hosted deployment rejects the section, and `app_public.branding.premium_product_name` and `premium_info_url`, with 400 `INVALID_FORM_BODY`. A Stripe secret, `enabled`, `automatic_tax`, `tax_id_collection` and `terms_consent_required` are written when supplied, cleared by null, and left alone when absent. A null `enabled` follows `FLUXER_STRIPE_ENABLED`. `prices`, `country_currencies`, and `legacy_prices` replace the stored value outright, a price slot left out is stored as null, and an empty `prices` object restores the deployment catalogue. A price ID is `price_` followed by letters and digits. A currency is three upper-case letters and a country code is two
|
||||
|
||||
:::note[Single sign-on URL validation is conditional]
|
||||
Fluxer skips URL validation while the merged configuration leaves single sign-on disabled. A configuration both enabled and enforced fails validation against `sso` with `SSO_MISCONFIGURED` unless it resolves an authorisation endpoint, a token endpoint, a client identifier, and a claims source, which is `jwks_url` or `userinfo_url`. A set `issuer` meets the endpoint and claims source requirements, because Fluxer can discover them from the issuer.
|
||||
:::
|
||||
@@ -685,6 +743,8 @@ Fluxer skips URL validation while the merged configuration leaves single sign-on
|
||||
|
||||
`direct_messages_locked` accepts only false, and a body that sets it to true fails with 400 `INVALID_FORM_BODY`.
|
||||
|
||||
On a self-hosted deployment, billing and the `everyone` premium mode exclude each other. A body that would leave billing switched on in `everyone` fails with 400 `INVALID_FORM_BODY` and writes nothing, whether it enables billing or switches the premium mode.
|
||||
|
||||
### Response
|
||||
|
||||
| Status | Body | Condition |
|
||||
@@ -693,12 +753,12 @@ Fluxer skips URL validation while the merged configuration leaves single sign-on
|
||||
| 400 | [error response](/admin-api/#error-response) | A policy transition is refused, returned as `INSTANCE_POLICY_TRANSITION_NOT_ALLOWED` |
|
||||
|
||||
:::caution[Sections are applied one after another]
|
||||
The order is `gateway_rollout`, `voice_noise_suppression`, `push_relay`, `domain_migration`, `altcha_captcha`, `profile_timezone`, `experiment_delivery`, `sso`, `registration`, `app_public` branding, legal, and registration fields, `integrations`, `media`, `policy`, and finally `app_public.setup`. A failure part way through leaves the earlier sections written.
|
||||
The order is `gateway_rollout`, `voice_noise_suppression`, `push_relay`, `domain_migration`, `altcha_captcha`, `profile_timezone`, `experiment_delivery`, `sso`, `registration`, `app_public` branding, legal, and registration fields, `integrations`, `media`, `policy`, `billing`, and finally `app_public.setup`. A failure part way through leaves the earlier sections written.
|
||||
:::
|
||||
|
||||
### Side effects
|
||||
|
||||
Fluxer publishes a `gateway_rollout` change to the Gateway cluster. A `push_relay` change reaches the push service without a restart. Premium mode changes affect the limits in force without replacing the saved limit configuration. On self-hosted deployments, `everyone` hides premium-filtered rules. Switching back to `mirror` restores them unless an Admin has replaced the limit configuration in the meantime. Enabling single community mode creates the community when none is designated, with the acting Admin as owner.
|
||||
Fluxer publishes a `gateway_rollout` change to the Gateway cluster. A `push_relay` change reaches the push service without a restart. A `billing` change applies without a restart. Premium mode changes affect the limits in force without replacing the saved limit configuration. On self-hosted deployments, `everyone` hides premium-filtered rules. Switching back to `mirror` restores them unless an Admin has replaced the limit configuration in the meantime. Enabling single community mode creates the community when none is designated, with the acting Admin as owner.
|
||||
|
||||
Initial setup completes on the first update that sets `app_public.setup.configured` to true from a session credential whose account holds neither `admin:authenticate` nor the wildcard. That update grants the account the wildcard Admin ACL and marks the deployment as bootstrapped.
|
||||
|
||||
|
||||
@@ -587,7 +587,7 @@ The operation records one [Admin audit entry](/admin-api/#admin-audit-entry-obje
|
||||
|
||||
Replaces the username, allocates or claims a discriminator, and returns the resulting account. Requires `user:update:username`.
|
||||
|
||||
A target account may hold a custom discriminator on every self-hosted instance, and on any other instance only when the `feature_custom_discriminator` limit resolves to a value above zero for that account.
|
||||
A target account may hold a custom discriminator on a self-hosted instance in the `everyone` [premium mode](/admin-api/instance/#premium-modes), and on any other instance only when the `feature_custom_discriminator` limit resolves to a value above zero for that account.
|
||||
|
||||
### Path parameters
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ 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.
|
||||
|
||||
Every route here is hosted-only, 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) and [continue localised card preapproval](#continue-localised-card-preapproval).
|
||||
|
||||
:::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.
|
||||
|
||||
@@ -1,66 +1,91 @@
|
||||
---
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
title: Deployment availability
|
||||
description: The hosted-only routes and the instance flags that report deployment kind.
|
||||
description: The routes a self-hosted deployment may not serve and the instance flags that decide them.
|
||||
---
|
||||
|
||||
The routes listed under [Hosted-only routes](#hosted-only-routes) exist only on the deployment Fluxer hosts. A self-hosted deployment runs the same release, and most of the HTTP API is identical on both.
|
||||
The routes listed under [Conditional routes](#conditional-routes) depend on the deployment. The deployment Fluxer hosts serves all of them. A self-hosted deployment serves some of them only while its operator runs a premium tier or sells it, and never serves the rest. Both deployments run the same release, and most of the HTTP API is identical on both.
|
||||
|
||||
A self-hosted deployment does not register those routes. A request to one returns 404 `NOT_FOUND` with no feature-specific code, so a caller cannot tell an unavailable route from an unrecognised path.
|
||||
When a self-hosted deployment does not serve one of these routes, a request to it returns 404 `NOT_FOUND` with no feature-specific code, so a caller cannot tell an unavailable route from an unrecognised path.
|
||||
|
||||
Credentials, permissions, premium state and OAuth2 scopes do not change route availability. Read the deployment kind from instance discovery.
|
||||
Credentials, permissions, premium state and OAuth2 scopes do not change route availability. Read the deployment kind and the premium flags from instance discovery.
|
||||
|
||||
## Deployment kind
|
||||
|
||||
Every deployment reports its kind in `self_hosted` on the [instance features object](/http-api/instance/#instance-features-object). The unauthenticated [instance discovery document](/http-api/instance/#get-instance-discovery) publishes it before a client holds any credential. `self_hosted` alone decides whether the API registers the routes below.
|
||||
Every deployment reports its kind in `self_hosted` on the [instance features object](/http-api/instance/#instance-features-object). The unauthenticated [instance discovery document](/http-api/instance/#get-instance-discovery) publishes it before a client holds any credential. A hosted deployment serves every route below whatever the other flags say.
|
||||
|
||||
`stripe_enabled` on the same object reports only the `integrations.stripe.enabled` configuration value. A hosted deployment that reports it false still serves every route in the table below. A deployment reporting `stripe_enabled` true with no provider secret key configured behaves exactly like one reporting it false.
|
||||
On a self-hosted deployment, three flags on the same object decide which routes below are served. All three are computed on each request, so an operator's change applies without a restart.
|
||||
|
||||
- `premium_enabled` is true while the instance [premium mode](/admin-api/instance/#premium-modes) is `mirror`. The routes under [Served in the mirror premium mode](#served-in-the-mirror-premium-mode) are served only then.
|
||||
- `stripe_enabled` is true while billing is active. Billing is active when the operator has switched it on, set a Stripe secret key, and set a monthly and a yearly price for at least one currency, and `premium_enabled` is true. The routes under [Served while billing is active](#served-while-billing-is-active) are served only then.
|
||||
- `stripe_serviceable` is true while a Stripe secret key is set and `premium_enabled` is true. The routes under [Served while Stripe is serviceable](#served-while-stripe-is-serviceable) are served only then, so existing subscriptions can still be managed after the operator stops sales.
|
||||
- The routes under [Never served on a self-hosted deployment](#never-served-on-a-self-hosted-deployment) are never served.
|
||||
|
||||
[Receive Stripe webhook](/http-api/billing/#receive-stripe-webhook) also needs a Stripe webhook secret.
|
||||
|
||||
:::caution[Read `self_hosted` for the deployment kind]
|
||||
Neither flag promises that a provider-dependent operation succeeds.
|
||||
`stripe_enabled`, `stripe_serviceable` and `premium_enabled` do not promise that a provider-dependent operation succeeds.
|
||||
:::
|
||||
|
||||
When `stripe_enabled` is false or no provider secret key is configured, the answer depends on the operation. An operation that has to reach the provider fails with 400 `STRIPE_PAYMENT_NOT_AVAILABLE`, and [Receive Stripe webhook](/http-api/billing/#receive-stripe-webhook) fails with 400 `STRIPE_WEBHOOK_NOT_AVAILABLE`. These read operations report the absence in a 200 body instead:
|
||||
When billing is switched off or no Stripe secret key is configured on a hosted deployment, the answer depends on the operation. An operation that has to reach the provider fails with 400 `STRIPE_PAYMENT_NOT_AVAILABLE`, and [Receive Stripe webhook](/http-api/billing/#receive-stripe-webhook) fails with 400 `STRIPE_WEBHOOK_NOT_AVAILABLE`. These read operations report the absence in a 200 body instead:
|
||||
|
||||
- [Get refund eligibility](/http-api/billing/#get-refund-eligibility) reports `eligible` false with the reason `feature_unavailable`.
|
||||
- [Get current subscription price](/http-api/premium/#get-current-subscription-price) reports null.
|
||||
- [Get price IDs](/http-api/premium/#get-price-ids) reports the configured price IDs with every amount null.
|
||||
|
||||
## Hosted-only routes
|
||||
## Conditional routes
|
||||
|
||||
### Served in the mirror premium mode
|
||||
|
||||
| Method | Route | Operation |
|
||||
| --- | --- | --- |
|
||||
| POST | /v1/donations/request-link | [Request donation management link](/http-api/donations/#request-donation-management-link) |
|
||||
| GET | /v1/donations/manage | [Manage donation](/http-api/donations/#manage-donation) |
|
||||
| POST | /v1/donations/checkout | [Create donation checkout](/http-api/donations/#create-donation-checkout) |
|
||||
| GET | /v1/gifts/{code} | [Get gift](/http-api/gifts/#get-gift) |
|
||||
| POST | /v1/gifts/{code}/redeem | [Redeem gift](/http-api/gifts/#redeem-gift) |
|
||||
| GET | /v1/users/@me/gifts<sup>1</sup> | [List current user gifts](/http-api/users/gifts/#list-current-user-gifts) |
|
||||
|
||||
### Served while billing is active
|
||||
|
||||
| Method | Route | Operation |
|
||||
| --- | --- | --- |
|
||||
| POST | /v1/stripe/checkout/subscription | [Create subscription checkout](/http-api/billing/#create-subscription-checkout) |
|
||||
| POST | /v1/stripe/checkout/subscription/preapproval | [Create localised card preapproval](/http-api/billing/#create-localised-card-preapproval) |
|
||||
| POST | /v1/stripe/checkout/subscription/preapproval/continue | [Continue localised card preapproval](/http-api/billing/#continue-localised-card-preapproval) |
|
||||
| POST | /v1/stripe/checkout/gift | [Create gift checkout](/http-api/billing/#create-gift-checkout) |
|
||||
| POST | /v1/stripe/webhook<sup>2</sup> | [Receive Stripe webhook](/http-api/billing/#receive-stripe-webhook) |
|
||||
| POST | /v1/users/@me/age-verification<sup>1</sup> | [Create age verification session](/http-api/billing/#create-age-verification-session) |
|
||||
| GET | /v1/premium/refund-eligibility<sup>3</sup> | [Get refund eligibility](/http-api/billing/#get-refund-eligibility) |
|
||||
| POST | /v1/premium/refund-latest | [Refund latest purchase](/http-api/billing/#refund-latest-purchase) |
|
||||
| GET | /v1/premium/price-ids | [Get price IDs](/http-api/premium/#get-price-ids) |
|
||||
| GET | /v1/premium/current-subscription-price<sup>4</sup> | [Get current subscription price](/http-api/premium/#get-current-subscription-price) |
|
||||
|
||||
### Served while Stripe is serviceable
|
||||
|
||||
| Method | Route | Operation |
|
||||
| --- | --- | --- |
|
||||
| POST | /v1/stripe/webhook<sup>2</sup> | [Receive Stripe webhook](/http-api/billing/#receive-stripe-webhook) |
|
||||
| GET | /v1/premium/current-subscription-price<sup>3</sup> | [Get current subscription price](/http-api/premium/#get-current-subscription-price) |
|
||||
| POST | /v1/premium/customer-portal | [Create customer portal](/http-api/premium/#create-customer-portal) |
|
||||
| POST | /v1/premium/grace/end | [End premium grace period](/http-api/premium/#end-premium-grace-period) |
|
||||
| POST | /v1/premium/cancel-subscription | [Cancel subscription](/http-api/premium/#cancel-subscription) |
|
||||
| POST | /v1/premium/reactivate-subscription | [Reactivate subscription](/http-api/premium/#reactivate-subscription) |
|
||||
| POST | /v1/premium/change-subscription | [Change subscription billing cycle](/http-api/premium/#change-subscription-billing-cycle) |
|
||||
| POST | /v1/premium/cancel-pending-subscription-change | [Cancel pending subscription change](/http-api/premium/#cancel-pending-subscription-change) |
|
||||
|
||||
### Never served on a self-hosted deployment
|
||||
|
||||
| Method | Route | Operation |
|
||||
| --- | --- | --- |
|
||||
| POST | /v1/donations/request-link | [Request donation management link](/http-api/donations/#request-donation-management-link) |
|
||||
| GET | /v1/donations/manage | [Manage donation](/http-api/donations/#manage-donation) |
|
||||
| POST | /v1/donations/checkout | [Create donation checkout](/http-api/donations/#create-donation-checkout) |
|
||||
| POST | /v1/users/@me/age-verification<sup>1</sup> | [Create age verification session](/http-api/billing/#create-age-verification-session) |
|
||||
| GET | /v1/premium/refund-eligibility<sup>4</sup> | [Get refund eligibility](/http-api/billing/#get-refund-eligibility) |
|
||||
| POST | /v1/premium/refund-latest | [Refund latest purchase](/http-api/billing/#refund-latest-purchase) |
|
||||
| POST | /v1/premium/switch-to-list-price | [Switch subscription to the list price](/http-api/premium/#switch-subscription-to-the-list-price) |
|
||||
| POST | /v1/premium/visionary/rejoin | [Rejoin Visionary guild](/http-api/premium/#rejoin-visionary-guild) |
|
||||
|
||||
<sup>1</sup> These are the only `/users/@me` routes a self-hosted deployment does not serve
|
||||
<sup>1</sup> These are the only `/users/@me` routes a self-hosted deployment may not serve
|
||||
|
||||
<sup>2</sup> The webhook takes no credential and is authenticated by the provider signature header alone
|
||||
|
||||
<sup>3</sup> The same object appears as `billing.refund_eligibility` on [Get premium state](/http-api/premium/#get-premium-state), which every deployment serves, and there a self-hosted deployment reports `eligible` false with the reason `feature_unavailable`
|
||||
<sup>3</sup> The same object appears as `billing.current_subscription_price` on [Get premium state](/http-api/premium/#get-premium-state), which every deployment serves
|
||||
|
||||
<sup>4</sup> The same object appears as `billing.current_subscription_price` on [Get premium state](/http-api/premium/#get-premium-state), which every deployment serves
|
||||
<sup>4</sup> The same object appears as `billing.refund_eligibility` on [Get premium state](/http-api/premium/#get-premium-state), which every deployment serves, and there a self-hosted deployment reports `eligible` false with the reason `feature_unavailable`
|
||||
|
||||
## Registered routes that resolve differently
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
|
||||
|
||||
A gift is a code that grants premium to the account that redeems it. Until then it belongs to no account. [Create gift checkout](/http-api/billing/#create-gift-checkout) defines how one is bought.
|
||||
|
||||
Both routes are hosted-only, as [deployment availability](/http-api/deployment-availability/) describes. [Redeem gift](#redeem-gift) is user-only.
|
||||
A self-hosted deployment serves both routes only while its [premium mode](/admin-api/instance/#premium-modes) is `mirror`, as [deployment availability](/http-api/deployment-availability/) describes. [Redeem gift](#redeem-gift) is user-only.
|
||||
|
||||
:::caution[A code is a bearer credential]
|
||||
A gift records no recipient, so Fluxer binds the code to whichever eligible account presents it first. [Get gift](#get-gift) takes no credential and returns the full public record for any code. Treat an unredeemed code as a secret.
|
||||
|
||||
@@ -170,9 +170,9 @@ A 429 `RESOURCE_LOCKED` response has `Retry-After: 1`, and a 429 `IP_AUTHORIZATI
|
||||
|
||||
A global denial has `Retry-After`, `X-RateLimit-Scope`, and `X-RateLimit-Global` alone.
|
||||
|
||||
## Hosted-only routes
|
||||
## Conditional routes
|
||||
|
||||
A small set of routes exists only on the hosted Fluxer deployment. A self-hosted deployment answers one of them with 404 `NOT_FOUND`. [Deployment availability](/http-api/deployment-availability/) lists every hosted-only route and states how a client resolves the deployment kind before authenticating.
|
||||
A small set of routes depends on the deployment. A self-hosted deployment serves some of them only while its operator runs a premium tier or sells it, never serves the rest, and answers an unserved one with 404 `NOT_FOUND`. [Deployment availability](/http-api/deployment-availability/) lists every such route and states how a client reads which ones a deployment serves before authenticating.
|
||||
|
||||
## Cross-origin requests
|
||||
|
||||
|
||||
@@ -139,13 +139,21 @@ Deployment-wide switches a client reads before it offers a feature, plus whether
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| voice_enabled | boolean | Whether voice and video calling is enabled |
|
||||
| stripe_enabled | boolean | Whether Stripe [billing](/http-api/billing/) is enabled |
|
||||
| stripe_enabled<sup>2</sup> | boolean | Whether premium purchases through Stripe [billing](/http-api/billing/) are available |
|
||||
| premium_enabled<sup>3</sup> | boolean | Whether the deployment has a premium tier, so premium state, gifts and perks apply |
|
||||
| stripe_serviceable<sup>4</sup> | boolean | Whether existing Stripe subscriptions can be managed, cancelled and billed |
|
||||
| self_hosted | boolean | Whether this deployment identifies itself as self-hosted |
|
||||
| presigned_attachment_uploads | boolean | Whether a client can request presigned attachment upload URLs |
|
||||
| emails_enabled<sup>1</sup> | boolean | Whether the deployment sends email |
|
||||
|
||||
<sup>1</sup> The value is true only when email is switched on and the transport is completely configured
|
||||
|
||||
<sup>2</sup> True only when billing is switched on, a Stripe secret key is set, at least one currency has both a monthly and a yearly price, and `premium_enabled` is true. It is computed on each request
|
||||
|
||||
<sup>3</sup> Always true on a hosted deployment. A self-hosted deployment reports true only while its [premium mode](/admin-api/instance/#premium-modes) is `mirror`
|
||||
|
||||
<sup>4</sup> On a hosted deployment, true while billing is switched on and a Stripe secret key is set. A self-hosted deployment reports true while a Stripe secret key is set and `premium_enabled` is true, even after billing is switched off
|
||||
|
||||
A deployment that reports `emails_enabled` as false sends no verification, password recovery, or IP authorisation message, and the flows that depend on one are unusable there. [Deployment availability](/http-api/deployment-availability/) states which routes a self-hosted deployment does not serve at all.
|
||||
|
||||
## GIF provider object
|
||||
@@ -375,7 +383,7 @@ Branding, setup state, legal documents, and registration fields a client reads b
|
||||
|
||||
## Public branding object
|
||||
|
||||
The instance identity a client renders: its name, its images, its theme colour, and its status page links. The image URLs customise browser metadata, install metadata, and link previews.
|
||||
The instance identity a client renders: its name, its images, its theme colour, its status page links, and the name of its premium tier. The image URLs customise browser metadata, install metadata, and link previews.
|
||||
|
||||
### Structure
|
||||
|
||||
@@ -390,6 +398,8 @@ The instance identity a client renders: its name, its images, its theme colour,
|
||||
| theme_color | ?string | Browser theme colour, or null |
|
||||
| status_page_url | ?string | Public status page URL, or null |
|
||||
| status_page_incident_history_url | ?string | Status page incident history URL, or null |
|
||||
| premium_product_name | string | Name of the premium tier a client displays. A self-hosted deployment reports `Premium` until the operator sets a name |
|
||||
| premium_info_url | ?string | Absolute URL of a page describing the premium tier, or null |
|
||||
|
||||
## Public setup state object
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
|
||||
|
||||
Premium is the paid tier of a Fluxer account. A recurring subscription, a redeemed gift, or a lifetime Visionary purchase pays for it. The routes here read that entitlement and run the subscription self-service operations, while [Billing](/http-api/billing/) owns purchase creation, refunds and the payment provider webhook.
|
||||
|
||||
Every route except [get price IDs](#get-price-ids) is user-only. A [self-hosted deployment](/http-api/deployment-availability/) serves only [get premium state](#get-premium-state) and [set premium perks disabled](#set-premium-perks-disabled).
|
||||
Every route except [get price IDs](#get-price-ids) is user-only. A [self-hosted deployment](/http-api/deployment-availability/) always serves [get premium state](#get-premium-state) and [set premium perks disabled](#set-premium-perks-disabled). It never serves [switch subscription to the list price](#switch-subscription-to-the-list-price) or [rejoin Visionary guild](#rejoin-visionary-guild), serves [get price IDs](#get-price-ids) only while billing is active, and serves the subscription management routes while Stripe is serviceable.
|
||||
|
||||
| Object | Purpose |
|
||||
| --- | --- |
|
||||
@@ -47,6 +47,8 @@ Every route except [get price IDs](#get-price-ids) is user-only. A [self-hosted
|
||||
|
||||
Every amount field is in the minor unit of the currency field its description names, and two amounts in one object can have different currencies.
|
||||
|
||||
A hosted deployment uses the currencies below. A self-hosted deployment whose operator sets its own prices uses the three-letter ISO 4217 codes the operator configured.
|
||||
|
||||
| Value | Description |
|
||||
| --- | --- |
|
||||
| USD | United States dollar |
|
||||
@@ -76,12 +78,14 @@ The checkout prices resolved for one country: monthly and yearly recurring, and
|
||||
| gift_1_month_amount_minor?<sup>2</sup> | ?integer | The one-month gift amount, in the minor unit of `gift_currency` |
|
||||
| gift_1_year_amount_minor?<sup>2</sup> | ?integer | The one-year gift amount, in the minor unit of `gift_currency` |
|
||||
| currency | string | [Display currency](#display-currencies) of the recurring prices |
|
||||
| gift_currency | string | [Display currency](#display-currencies) of the gift prices |
|
||||
| gift_currency<sup>3</sup> | ?string | [Display currency](#display-currencies) of the gift prices |
|
||||
|
||||
<sup>1</sup> Fluxer resolves the recurring pair and the gift pair separately. Each pair takes the first currency for which both of its prices are configured, so the pairs can resolve to different currencies
|
||||
|
||||
<sup>2</sup> Null when the amount is unavailable. Price changes can take up to one hour to appear
|
||||
|
||||
<sup>3</sup> Null on a self-hosted deployment with no currency that has both gift prices. The gift price IDs and amounts are null too, and gift checkout is unavailable
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
@@ -269,7 +273,7 @@ The effective state decides whether premium features are available. It differs f
|
||||
| premium_lifetime_sequence<sup>3</sup> | ?integer | The Visionary sequence number, or null without lifetime entitlement |
|
||||
| premium_grace_ends_at<sup>3</sup> | ?ISO8601 timestamp | The time the post-cancellation grace access ends, or null when no grace is active |
|
||||
| premium_enabled_override<sup>4</sup> | boolean | Whether an operator premium override applies to the account |
|
||||
| premium_purchase_disabled<sup>4</sup> | boolean | Whether the account has the purchase-disabled flag |
|
||||
| premium_purchase_disabled<sup>4</sup> | boolean | Whether purchases are disabled for the account |
|
||||
| premium_perks_disabled<sup>4</sup> | boolean | Whether the account has the perks-disabled flag |
|
||||
| self_hosted<sup>5</sup> | boolean | Whether the deployment is self-hosted |
|
||||
| bot<sup>6</sup> | boolean | Whether the credential is a bot account with premium-equivalent service access |
|
||||
@@ -280,7 +284,7 @@ The effective state decides whether premium features are available. It differs f
|
||||
|
||||
<sup>3</sup> Copied from the [actual premium state](#actual-premium-state-object) unchanged, so the value does not react to `is_premium`
|
||||
|
||||
<sup>4</sup> Reflects the matching [premium flag](/admin-api/users/#premium-flags) on the account
|
||||
<sup>4</sup> True when the account has the matching [premium flag](/admin-api/users/#premium-flags), or when the deployment has no active billing
|
||||
|
||||
<sup>5</sup> `is_premium` follows this only while the instance [premium mode](/admin-api/instance/#premium-modes) is `everyone`
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import RouteHeader from '@/components/RouteHeader.astro';
|
||||
|
||||
The gift inventory lists the premium gift codes the current account created, with the redemption state of each. The [Gifts resource](/http-api/gifts/) has public gift lookup and redemption, and the [Billing resource](/http-api/billing/) has gift purchase.
|
||||
|
||||
A self-hosted deployment returns 404 `NOT_FOUND` here, as [deployment availability](/http-api/deployment-availability/) describes.
|
||||
A self-hosted deployment returns 404 `NOT_FOUND` here unless its [premium mode](/admin-api/instance/#premium-modes) is `mirror`, as [deployment availability](/http-api/deployment-availability/) describes.
|
||||
|
||||
A code enters this inventory only through a completed gift checkout with the payment provider. The Admin API records a code it issues against the system account, so that code never appears in any account's inventory.
|
||||
|
||||
@@ -91,7 +91,7 @@ A redeemed code that has dropped out of the listing still exists, still resolves
|
||||
| Status | Body | Condition |
|
||||
| --- | --- | --- |
|
||||
| 200 | array[[gift inventory](#gift-inventory-object) object] | The inventory was returned |
|
||||
| 404 | [error response](/http-api/#error-response) | The deployment is self-hosted and the request returns `NOT_FOUND` |
|
||||
| 404 | [error response](/http-api/#error-response) | The deployment is self-hosted in the `everyone` premium mode and the request returns `NOT_FOUND` |
|
||||
|
||||
### Rate limit
|
||||
|
||||
|
||||
@@ -911,11 +911,13 @@ Default `false`. Accepts the push relay supplemental privacy notice for this `pu
|
||||
|
||||
## Payments
|
||||
|
||||
Stripe billing, which the shipped stack keeps off. All are optional.
|
||||
Stripe billing for a paid premium tier and gift purchases. All are optional. On a self-hosted instance, set billing in the admin dashboard's [Premium & billing](#runtime-settings-in-the-admin-dashboard) group instead. A value stored there wins over the matching variable below.
|
||||
|
||||
A self-hosted instance sells premium only while its premium mode is `mirror`, billing is switched on, a Stripe secret key is set, and at least one currency has both a monthly and a yearly price. Instance discovery reports this as `stripe_enabled`. Existing subscribers can still manage, cancel and renew while the premium mode is `mirror` and a Stripe secret key is set, even after sales are switched off or the prices are removed. Instance discovery reports this as `stripe_serviceable`. Stripe sends events to `https://<FLUXER_DOMAIN>/api/stripe/webhook`, which the dashboard shows as the webhook URL to register in Stripe.
|
||||
|
||||
#### `FLUXER_STRIPE_ENABLED`
|
||||
|
||||
Default `false`. The Stripe switch. Compose hardcodes `false`, so a self-hosted instance cannot enable it from `.env`.
|
||||
Default `false`. The Stripe switch. Compose forwards it from `.env`.
|
||||
|
||||
#### `FLUXER_STRIPE_SECRET_KEY`
|
||||
|
||||
@@ -927,16 +929,20 @@ Default empty. The webhook signing secret. Not forwarded by the shipped Compose
|
||||
|
||||
#### `FLUXER_STRIPE_PRICES`
|
||||
|
||||
Default `{}`. Every price ID at once. JSON object. An individual price variable overrides the matching price in this object.
|
||||
Default `{}`. Every price ID at once. JSON object. An individual price variable overrides the matching price in this object. Not forwarded by the shipped Compose file.
|
||||
|
||||
Individual price variables also exist, one per product and currency: `FLUXER_STRIPE_PRICE_MONTHLY_`, `FLUXER_STRIPE_PRICE_YEARLY_`, `FLUXER_STRIPE_PRICE_GIFT_1_MONTH_` and `FLUXER_STRIPE_PRICE_GIFT_1_YEAR_` in USD, EUR, BRL, DKK, INR, NOK, PLN, SEK, and TRY, plus `FLUXER_STRIPE_PRICE_VISIONARY_` and `FLUXER_STRIPE_PRICE_GIFT_VISIONARY_` in USD and EUR.
|
||||
Individual price variables also exist, one per product and currency: `FLUXER_STRIPE_PRICE_MONTHLY_`, `FLUXER_STRIPE_PRICE_YEARLY_`, `FLUXER_STRIPE_PRICE_GIFT_1_MONTH_` and `FLUXER_STRIPE_PRICE_GIFT_1_YEAR_` in USD, EUR, BRL, DKK, INR, NOK, PLN, SEK, and TRY.
|
||||
|
||||
Prices saved in the dashboard replace this catalogue entirely. They take any three-letter currency code. A buyer gets the currency mapped to their country, then the default currency, then the first configured one. Fluxer's country checks for localised currencies apply only to prices set by these variables.
|
||||
|
||||
#### `FLUXER_STRIPE_LEGACY_PRICES`
|
||||
|
||||
Default `{}`. Retired price IDs, keyed by the same slot names `FLUXER_STRIPE_PRICES` uses, each mapped to a list: `{"monthly_brl": ["price_..."]}`. JSON object. Existing subscriptions on these prices keep renewing. The localised checkout catalogue uses `FLUXER_STRIPE_PRICES` alone.
|
||||
Default `{}`. Retired price IDs, keyed by the same slot names `FLUXER_STRIPE_PRICES` uses, each mapped to a list: `{"monthly_brl": ["price_..."]}`. JSON object. Existing subscriptions on these prices keep renewing. The localised checkout catalogue uses `FLUXER_STRIPE_PRICES` alone. Legacy prices saved in the dashboard replace this variable, and there the currency in a slot name is upper case, such as `monthly_GBP`.
|
||||
|
||||
Repricing a slot has a fixed order. Doing the steps in another order leaves a price ID unknown to the API, and invoices on that price fail. Create the new price in Stripe, move the ID it replaces into this variable, roll the API, and only then point `FLUXER_STRIPE_PRICES` at the new price. A price ID that neither variable names is unknown to the API: a renewal invoice on it fails the webhook with `Unknown product for invoice renewal`, a checkout completing on it fails with `Unknown price ID for checkout session`, and both keep failing until the ID is registered. The API answers Stripe as soon as the signature verifies and hands the event to `worker`, so any retry comes from `worker` rerunning the `processStripeWebhook` job: the Stripe dashboard shows the delivery as succeeded and the error is in the `worker` logs. Keep a retired ID listed for as long as any subscription still bills on it, which for a yearly price is at least a year after the switch.
|
||||
|
||||
With prices saved in the dashboard, add the retired ID to its legacy prices before you replace the price.
|
||||
|
||||
## Moderation and abuse
|
||||
|
||||
All are optional.
|
||||
@@ -1151,7 +1157,7 @@ Default `development`. The runtime mode. `development`, `production`, or `test`.
|
||||
|
||||
#### `FLUXER_SELF_HOSTED`
|
||||
|
||||
Default `false`. The self-host switch. Compose sets `true`. It relaxes the production Postgres SSL requirement, seeds the limit tier, gates registration, billing and discovery controllers, and turns blocklist feeds off.
|
||||
Default `false`. The self-host switch. Compose sets `true`. It relaxes the production Postgres SSL requirement, seeds the limit tier, gates registration, donation and discovery controllers, keeps premium billing off until it is set up as [Payments](#payments) describes, and turns blocklist feeds off.
|
||||
|
||||
`/_metrics` on `api`, `media-proxy`, `gateway`, and `push`, plus the Gateway's `/_health/ready`, `/_health/drain`, and `/_health/undrain`, are gated to loopback peers, so no proxy reaches them. The probes that work from outside are `/api/_health`, `/gateway/_health`, `/media/_health`, and the edge's own `/_health`.
|
||||
|
||||
@@ -1751,6 +1757,10 @@ Single-community mode, direct messages and friends, the premium model, and optio
|
||||
|
||||
The Klipy GIF key, the YouTube Data API key, the CAPTCHA provider and its keys, email delivery with an SMTP connection test, and Bluesky OAuth.
|
||||
|
||||
#### Instance Config, Premium & billing
|
||||
|
||||
The premium tier name and information link, the billing switch, the Stripe secret key and webhook secret, the webhook URL to register in Stripe, and the price catalogue with its default currency, country currencies and legacy prices. See [Payments](#payments).
|
||||
|
||||
#### Instance Config, Media Expiry
|
||||
|
||||
Size-based attachment lifetimes. The built-in default is on.
|
||||
|
||||
Reference in New Issue
Block a user