These endpoints are live in production today, served by the FloKit payments gateway. They are separate from the v1 REST API, which is still in design-partner preview.
https://payments-gateway.flokitai.com
Authentication
These endpoints use request-scoped credentials instead of server API keys:x-app-keyheader — your app’s publishable key (pk_...), created per app in the FloKit dashboard. It scopes the request to your app and is safe to embed in your app build (it is not a secret and does not authenticate the user). Required on entitlement reads and funnel event writes; not needed forGET /api/paywall/config.x-app-tokenheader — an optional short-lived app-session token minted from the publishable key viaPOST /api/paywall/token. Anywherex-app-keyis accepted,x-app-tokenworks instead — send one or the other.x-user-idheader — the user’s identity.userIdoranonymousIdquery parameters are accepted as a fallback where custom headers are inconvenient (the header takes precedence).
x-app-key enforcement is being phased in ahead of GA. Integrations should send it on every request today; once enforcement is on, requests without a valid key receive 401 on the endpoints marked as requiring it. @flokit/subscriptions-sdk v0.2.0+ sends it automatically via the appKey option.GET /api/paywall/config
Returns the published paywall configuration for an app, with a single variant selected for the requesting user. Variant selection is deterministic — the same user always receives the same variant for a given paywall, and traffic splits respect each variant’straffic_weight when an experiment is active.
Query parameters
Your application identifier.
The paywall to fetch, e.g.
onboarding.User identity fallback if the
x-user-id header is not set.Anonymous identity fallback for pre-login users.
Example
Response
| Field | Type | Description |
|---|---|---|
paywall_id | string | The requested paywall identifier |
version | number | Published configuration version |
variant.variant_id | string | The variant selected for this user |
variant.layout | string | Layout hint, e.g. single_offer or standard |
variant.offers[] | array | Offers to render (see below) |
variant.cta_copy | string | Optional call-to-action copy |
fallbackUsed | boolean | true when no published config was found and a safe empty fallback was returned |
| Field | Type | Description |
|---|---|---|
product_id | string | Store product identifier |
name | string | Display name |
price_usd_cents | number | Price in minor units |
currency | string | ISO 4217 currency code |
billing_period | string | weekly, monthly, quarterly, yearly, or lifetime |
trial_days | number | Free trial length; 0 means no trial |
200 with a fallback: version: 0, variant control with an empty offers array, and fallbackUsed: true. Your app should treat an empty offer list as “do not show the paywall.”
Responses are cacheable: Cache-Control: public, max-age=60, stale-while-revalidate=300.
Errors
| Status | Body | Condition |
|---|---|---|
400 | { "error": "app_id and paywall_id are required." } | Missing query parameter |
401 | { "error": "x-user-id header or userId/anonymousId query param is required." } | No identity provided |
POST /api/paywall/receipt
Submits an in-app purchase receipt for server-side verification. Send the Apple StoreKit 2 signed transaction (JWS) or the Google Play package name + purchase token; the gateway forwards it to the verification service and returns the verification result.Headers
| Header | Value |
|---|---|
x-user-id | Your authenticated user ID (required) |
x-app-key | Your publishable app key (recommended) |
Content-Type | application/json |
x-app-key is not enforced here — but send it anyway for uniformity.
Request body
apple or google.Apple StoreKit 2 signed transaction (JWS). Required for
apple.Android package name. Required for
google.Google Play purchase token. Required for
google.Example
Response
The verification result is returned as produced by the verification service, for example:Errors
| Status | Body | Condition |
|---|---|---|
400 | { "error": "Invalid receipt request." } | Body fails validation |
401 | { "error": "x-user-id is required." } | Missing user identity |
422 | { "error": "..." } | Verification rejected upstream, e.g. no tenant provisioned for this user |
POST /api/paywall/events
Logs paywall funnel events — impressions, offer taps, purchases, conversions, cancellations — for paywall analytics. Accepts 1–50 events per request. The gateway resolves the request’s country at the edge; a server-resolved country takes precedence over any client-suppliedcountry value.
Headers
| Header | Value |
|---|---|
x-app-key | Your publishable app key (required) |
x-user-id | Your authenticated user ID (required) |
Content-Type | application/json |
app_id must belong to the app the key was issued for — mismatches are rejected with 403.
Request body
1–50 event objects.
One of
impression, offer_tap, purchase_start, trial_start, convert, cancel.Your application identifier.
Must include
paywall_id and variant_id. Additional properties are passed through.ISO 3166-1 alpha-2 country code. Used only if server-side resolution fails.
Client platform, e.g.
ios or android.ISO 8601 timestamp of when the event occurred.
Example
Response
Errors
| Status | Body | Condition |
|---|---|---|
400 | { "error": "Invalid paywall events request." } | Bad event_type, empty array, more than 50 events, or missing required fields |
401 | { "error": "x-app-key is required." } | Missing app key (once enforcement is on) |
401 | { "error": "Invalid app key." } | Unknown or revoked app key |
401 | { "error": "x-user-id is required." } | Missing user identity |
403 | { "error": "event app_id does not match the app key." } | An event claims an app_id the key was not issued for |
422 | { "error": "..." } | Rejected upstream, e.g. no tenant attributed for this user |
POST /api/paywall/token
Exchanges the publishable key for a short-lived app-session token bound to the requesting app and user. Use it when you’d rather not attach the rawpk_... key to every request: mint once at SDK init, send the token as x-app-token on subsequent calls, and re-mint when it expires.
Headers
| Header | Value |
|---|---|
x-app-key | Your publishable app key (required — always enforced on this endpoint) |
x-user-id | Your authenticated user ID (required) |
Example
Response
x-user-id it was minted for — a token leaked from one user cannot read another user’s entitlements.
Errors
| Status | Body | Condition |
|---|---|---|
401 | { "error": "x-app-key is required." } / { "error": "Invalid app key." } | Missing or invalid key |
401 | { "error": "x-user-id is required." } | Missing user identity |
503 | { "error": "App session tokens are not enabled." } | Token minting not configured on this environment |
Provider webhook passthrough
The gateway also exposes webhook receiver endpoints for subscription and attribution providers. Bodies are forwarded as raw bytes — provider signature headers are preserved and verified downstream, so configure these URLs directly in each provider’s dashboard.| Endpoint | Provider |
|---|---|
POST /webhooks/stripe | Stripe (signature in stripe-signature header) |
POST /webhooks/apple | Apple App Store Server Notifications V2 |
POST /webhooks/google | Google Play RTDN (Pub/Sub push) |
POST /api/providers/revenuecat/webhook | RevenueCat |
POST /api/providers/adapty/webhook | Adapty |
POST /api/providers/generic-subscription/webhook | Generic subscription provider |
POST /api/integrations/mmp/generic-attribution | Generic MMP attribution callback |
502; if it times out (10 seconds), 504 — providers treat both as delivery failures and retry.