Skip to content
Fluxer API

In-app purchases

The mobile apps sell premium through the App Store and Google Play. The store takes the payment, and the app then hands the purchase to Fluxer through a claim route. Fluxer verifies the purchase with the store, links it to the account, and applies it to the account’s premium state. The stores also send notifications for renewals, expiries and refunds, so a purchase stays current without the app.

A subscription purchase grants the same premium as a Stripe subscription. A gift purchase mints one gift code. Get premium state reports the active store subscription in store, and names the platform that owns the account’s recurring subscription in subscription_provider. A client manages the subscription only on that platform, so an app does not offer its own management for a stripe subscriber.

Every route on this page except the store notification webhooks is user-only. A self-hosted deployment never serves any of them, as deployment availability describes.

ObjectPurpose
In-app purchase contextThe account token and the products the apps may sell
Store purchaseOne App Store or Google Play purchase linked to the account
Store purchase claimThe result of a claim
ValueDescription
app_storeApple App Store, with StoreKit 2 in the app
google_playGoogle Play, with Play Billing in the app

Each configured store product sells one Fluxer product, called its slot.

ValueDescription
monthlyPremium subscription billed every month
yearlyPremium subscription billed every year
gift_1_monthOne gift code for one month of premium
gift_1_yearOne gift code for one year of premium
ValueDescription
pendingThe store has not taken the payment yet
activeThe subscription is paid and renews
graceThe renewal payment failed and the store grace period is running
billing_retryThe renewal payment failed and the store is retrying without grace
on_holdGoogle Play has suspended the subscription after a failed payment
pausedThe subscriber paused the subscription in Google Play
canceledThe subscription is paid until expires_at and will not renew
expiredThe subscription has ended
revokedThe store refunded or revoked the subscription
supersededA newer Google Play purchase replaced this one
purchasedThe gift is paid and its code is not minted yet
fulfilledThe gift code is minted
refundedThe store refunded the gift

Only active, grace and canceled can grant premium.

ValueDescription
lifetimeThe account holds lifetime Visionary premium
existing_subscriptionThe account already has an active subscription
purchase_disabledPurchases are disabled for the account

What an app needs before it shows a paywall. Get in-app purchase context returns it.

FieldTypeDescription
app_account_token1stringThe account token, a lowercase UUID
app_storeApp Store settings objectApp Store purchase settings
google_playGoogle Play settings objectGoogle Play purchase settings
purchase_blocked_reason2?stringPurchase blocked reason, or null
blocking_provider3?stringProvider of the blocking subscription, or null

1 Stable for the account. An App Store purchase passes it as appAccountToken, and a Google Play purchase passes it as obfuscatedAccountId. Fluxer links a purchase that has the token to the account even when the store notification arrives before the claim

2 Null when the account can buy a subscription. Gift products stay on sale whatever the value is

3 Either stripe, app_store or google_play. It has a value only when the reason is existing_subscription

{
"app_account_token": "0f7c2a1e-5b3d-4c8e-9a61-2d4f8b7e3c10",
"app_store": {
"enabled": true,
"bundle_ids": ["com.fluxer"],
"products": [
{"product_id": "com.fluxer.plutonium.monthly", "slot": "monthly"},
{"product_id": "com.fluxer.gift.1month", "slot": "gift_1_month"}
]
},
"google_play": {
"enabled": true,
"package_names": ["com.fluxer"],
"products": [
{"product_id": "plutonium", "base_plan_id": "monthly", "slot": "monthly"},
{"product_id": "gift_1_month", "base_plan_id": null, "slot": "gift_1_month"}
]
},
"purchase_blocked_reason": null,
"blocking_provider": null
}

The App Store purchases the deployment accepts. When the App Store is not set up, enabled is false and both arrays are empty.

FieldTypeDescription
enabledbooleanWhether App Store purchases are accepted
bundle_idsarray[string]App bundle IDs whose purchases are accepted
productsarray[App Store product object]Products on sale
FieldTypeDescription
product_idstringApp Store product ID
slotstringStore slot

The Google Play purchases the deployment accepts. When Google Play is not set up, enabled is false and both arrays are empty.

FieldTypeDescription
enabledbooleanWhether Google Play purchases are accepted
package_namesarray[string]App package names whose purchases are accepted
productsarray[Google Play product object]Products on sale

A subscription product has one entry for each base plan on sale.

FieldTypeDescription
product_idstringGoogle Play product ID
base_plan_id?stringBase plan ID, or null for a one-time product
slotstringStore slot

One purchase as Fluxer last read it from the store. It never exposes purchase tokens, transaction IDs or the account token.

FieldTypeDescription
idsnowflakeThe ID of the store purchase
providerstringStore provider
kindstringEither subscription or gift
slotstringStore slot
product_idstringStore product ID
environment1stringEither production or sandbox
statestringStore purchase state
entitled2booleanWhether the purchase grants premium now
expires_at?ISO8601 timestampEnd of the paid period, or null for a gift
entitled_until3?ISO8601 timestampEnd of access from this purchase
will_renew?booleanWhether the subscription renews, or null for a gift
gift_code?stringGift code minted by a gift purchase, or null
created_atISO8601 timestampThe time Fluxer first saw the purchase

1 sandbox is an App Store sandbox purchase or a Google Play license test purchase

2 A sandbox purchase is false unless the account is allowed test purchases on the deployment

3 The grace period end while state is grace, otherwise expires_at. Null for a gift and for a purchase that grants nothing

{
"id": "1501203318237184000",
"provider": "app_store",
"kind": "subscription",
"slot": "monthly",
"product_id": "com.fluxer.plutonium.monthly",
"environment": "production",
"state": "active",
"entitled": true,
"expires_at": "2026-10-29T09:00:00.000Z",
"entitled_until": "2026-10-29T09:00:00.000Z",
"will_renew": true,
"gift_code": null,
"created_at": "2026-09-29T09:00:00.000Z"
}
FieldTypeDescription
purchasestore purchase objectThe claimed purchase
gift_code1?stringGift code minted by a gift purchase, or null

1 The same value as purchase.gift_code. Null for a subscription and for a gift the store has not finished charging

{
"purchase": {
"id": "1501203318237184001",
"provider": "google_play",
"kind": "gift",
"slot": "gift_1_month",
"product_id": "gift_1_month",
"environment": "production",
"state": "fulfilled",
"entitled": true,
"expires_at": null,
"entitled_until": null,
"will_renew": null,
"gift_code": "q7Xr2mPz9LkT4vBn8cWd3HsYf6JaE1Gu",
"created_at": "2026-09-29T09:00:00.000Z"
},
"gift_code": "q7Xr2mPz9LkT4vBn8cWd3HsYf6JaE1Gu"
}
GET/v1/premium/store

Returns the in-app purchase context object for the authenticated account.

The blocked reason is decided in a fixed order. A lifetime Visionary account reports lifetime, then the purchase-disabled premium flag reports purchase_disabled, then an active store subscription reports existing_subscription with that store, and then an active Stripe subscription reports existing_subscription with stripe.

StatusBodyCondition
200in-app purchase context objectThe context was returned

The first read creates the account token.

30 requests per 10 seconds for each authenticated user, on the store:context bucket.

POST/v1/premium/store/app-store/transactions

Verifies a StoreKit 2 signed transaction, links the purchase to the authenticated account, applies it, and returns a store purchase claim object. Repeating the claim for the same purchase returns the same result.

The app finishes the transaction after a 200 response or any 400 or 403 error listed under limitations. It leaves the transaction unfinished after a 429, a 503 STORE_BILLING_UNAVAILABLE, another 5xx or a network failure. StoreKit then delivers the transaction again, so the claim can be retried later.

  • A transaction that fails verification, is for an unknown product or app, or comes from Xcode or local testing returns 400 STORE_PURCHASE_INVALID.
  • A gift bought in a quantity above one mints no gift code and returns 400 STORE_PURCHASE_INVALID.
  • A purchase already linked to another live account returns 403 STORE_PURCHASE_OWNED_BY_OTHER_ACCOUNT. So does a purchase whose account token belongs to another live account.
  • A sandbox purchase on an account that is not allowed test purchases returns 403 STORE_PURCHASE_SANDBOX_NOT_ENTITLED.
  • A subscription claimed by a lifetime Visionary account returns 403 PREMIUM_PURCHASE_BLOCKED with the reason lifetime and the store in the top-level provider member.
  • A deployment without the App Store set up, or an App Store that cannot be reached, returns 503 STORE_BILLING_UNAVAILABLE.

After a test purchase or lifetime refusal the purchase is still linked to the account. It grants nothing.

A purchase shared through Family Sharing is linked only through this route.

FieldTypeDescription
signed_transactionstringThe signed transaction from StoreKit 2 (1-32768 characters)
StatusBodyCondition
200store purchase claim objectThe purchase was verified and linked
400error responseThe transaction was not accepted
403error responseThe purchase belongs to another account, or it cannot grant premium to this account
503error responseThe App Store is unavailable

Fluxer reads the purchase from the App Store. A subscription that grants premium updates the account’s premium state, and a gift purchase mints its gift code once. A changed premium state sends User Update to every account session.

When the purchase has no account token, or has one that belongs to no live account, Fluxer sets the claiming account’s token on it.

20 requests per minute for each authenticated user, on the store:claim:app_store bucket.

POST/v1/premium/store/google-play/purchases

Verifies a Google Play purchase token, links the purchase to the authenticated account, applies it, and returns a store purchase claim object. Repeating the claim for the same purchase returns the same result.

Fluxer acknowledges the purchase with Google Play. The app does not need to acknowledge it.

  • A token that Google Play does not recognise, a product that is not on sale, a product that does not match the token, and a package name that is not accepted all return 400 STORE_PURCHASE_INVALID.
  • The ownership, test purchase and lifetime refusals of claim App Store transaction apply unchanged, with the same codes.
  • A deployment without Google Play set up, or a Google Play that cannot be reached, returns 503 STORE_BILLING_UNAVAILABLE.

A lifetime Visionary account that buys a subscription through Google Play is refunded. So is a gift bought through Google Play in a quantity above one.

FieldTypeDescription
purchase_tokenstringPurchase token from Play Billing (1-1024 characters)
product_idstringGoogle Play product ID of the purchase (1-256 characters)
package_name?1stringPackage name of the app that made the purchase (1-256 characters)

1 One of google_play.package_names from the in-app purchase context. Defaults to the first accepted package

StatusBodyCondition
200store purchase claim objectThe purchase was verified and linked
400error responseThe purchase was not accepted
403error responseThe purchase belongs to another account, or it cannot grant premium to this account
503error responseGoogle Play is unavailable

Fluxer reads the purchase from Google Play. A subscription that grants premium updates the account’s premium state, and a gift purchase mints its gift code once and is then consumed. A changed premium state sends User Update to every account session.

A purchase that replaces an earlier subscription, such as an upgrade, takes over that subscription’s account link. The earlier purchase becomes superseded.

20 requests per minute for each authenticated user, on the store:claim:google_play bucket.

GET/v1/premium/store/purchases

Returns an array of the store purchase objects linked to the authenticated account, newest first.

StatusBodyCondition
200array[store purchase object]The purchases were returned

30 requests per 10 seconds for each authenticated user, on the store:purchases:list bucket.

DELETE/v1/premium/store/purchases/{purchase_id}MFA

Unlinks a store subscription from the authenticated account and returns 204 with an empty body. Requires sudo mode. Another account can then claim the subscription.

Releasing does not cancel the subscription. The subscriber cancels it in the store.

  • A purchase that is not linked to the account returns 404 UNKNOWN_STORE_PURCHASE.
  • A gift purchase cannot be released and returns 400 STORE_PURCHASE_INVALID.
FieldTypeDescription
purchase_idsnowflakeThe ID of the store purchase
FieldTypeDescription
X-Fluxer-Sudo-Mode-JWT?stringExisting sudo mode proof

The body is the sudo verification fields. A request that already has a valid proof can omit it.

FieldTypeDescription
password?stringCurrent account password
mfa_method?stringMFA method, either totp or webauthn
mfa_code?stringAuthenticator code or unconsumed backup code when the method is totp (1-32 characters)
webauthn_response?WebAuthn assertion objectAssertion when the method is webauthn
webauthn_challenge?stringChallenge bound to the WebAuthn assertion
StatusBodyCondition
204emptyThe subscription was released
400error responseThe purchase is a gift, or the sudo proof was rejected
403error responseSudo mode is required and returns SUDO_MODE_REQUIRED
404error responseThe purchase is not linked to the account

The account stops receiving premium from the subscription, and a changed premium state sends User Update to every account session.

5 requests per minute for each authenticated user, on the store:purchases:release bucket.

The stores call these two routes. No client calls them, and they have no OpenAPI entry. Both routes are served only on the hosted deployment, and both answer 503 STORE_BILLING_UNAVAILABLE while their store is not set up.

Fluxer reads the purchase from the store again after each notification, and a redelivered notification is applied once. A Google Play voided purchase is the one notification that changes a purchase by itself. It marks a gift refunded, or a subscription revoked when the voided order is its latest order.

When a gift is refunded, Fluxer revokes its gift code and takes the gift time back from the account that redeemed it. When the store reverses the refund, the gift code and its gift time come back.

Fluxer also reads the App Store notification history and the Google Play voided purchases once a day, so a notification that was missed is still applied.

POST /webhooks/app-store receives App Store Server Notifications V2. The body is {"signedPayload": "..."}, signed by Apple. Fluxer verifies the signature before it answers.

StatusCondition
200The notification was verified and queued
401The body or its signature was rejected and returns STORE_NOTIFICATION_UNAUTHORIZED
503The App Store is not set up

300 requests per minute for each client IP address, on the store:webhook:app_store bucket. The bucket is exempt from the global bucket.

Google Play real-time developer notifications

Section titled “Google Play real-time developer notifications”

POST /webhooks/google-play receives real-time developer notifications as Pub/Sub push messages. Each message is authenticated by a Google-signed token in the Authorization header, whose audience and service account the deployment configures.

StatusCondition
204The message was queued, or it was not a Pub/Sub message and was dropped
401The token was rejected and returns STORE_NOTIFICATION_UNAUTHORIZED
503Google Play is not set up

300 requests per minute for each client IP address, on the store:webhook:google_play bucket. The bucket is exempt from the global bucket.