> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flokitai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# SDKs

> The FloKit subscriptions SDK for React Native purchases, entitlements, and app measurement, plus planned server SDKs.

## @flokitai/subscriptions-sdk (shipping)

<Warning>
  **The package was renamed in v1.2.0.** `@flokit/subscriptions-sdk` is no longer published anywhere — re-pin to `@flokitai/subscriptions-sdk`. Distribution moved to GitHub Packages, whose registry requires the package scope to match the repository owner. See [Install](#install).
</Warning>

`@flokitai/subscriptions-sdk` **v2.0** is FloKit's React Native SDK for purchases, entitlements, and app measurement. You initialize it once; the SDK then runs the native purchase → receipt-verification → entitlement flow, tracks sessions and app lifecycle, and reports funnel events through a persisted, offline-safe queue. Funnel events carry an optional **placement** — a stable name for a moment in your app (`onboarding_complete`, `settings_upgrade`) — so reporting can be sliced by where in the app something happened.

* **Platform:** React Native (TypeScript). Peer dependencies: `react >= 18`, `react-native >= 0.70`.
* **No native dependency:** the native store call (StoreKit 2 / Play Billing / expo-iap) is injected by your app, so the SDK stays pure TypeScript.
* **Context-first:** call `initSubscriptionsSdk()` once. Every other function resolves `baseUrl`, `appId`, the user reference, and credentials from that context — you never thread them through call sites.
* **Tenant-safe by design:** the SDK sends only the user reference (`x-user-id`) and your app's publishable key (`x-app-key`); your workspace/tenant is always derived server-side. The context mints and refreshes a short-lived app-session token (`x-app-token`) automatically.
* **Anonymous-first identity:** a persisted `anonymous_ref` (a UUID) is the user reference until you call `identify(userId)` after login.

```mermaid theme={null}
sequenceDiagram
    participant App as Your app
    participant SDK as subscriptions-sdk
    participant Gateway as Payments gateway
    App->>SDK: getEntitlement()
    SDK->>Gateway: GET /api/entitlements/current (x-app-key, x-user-id)
    Gateway-->>SDK: Entitlement { active, state, entitlement_key }
    App->>SDK: purchase({ productId, placement })
    SDK->>Gateway: native purchase + POST /api/iap/receipts
    Gateway-->>SDK: Entitlement { active, state, entitlement_key }
    SDK->>Gateway: POST /api/paywall/events (purchase_start, convert)
    SDK-->>App: Entitlement { active, state, entitlement_key }
```

### Install

The package is published to **GitHub Packages** under the `@flokitai` scope. Point the scope at the GitHub registry in your `.npmrc`, then install:

```ini .npmrc theme={null}
@flokitai:registry=https://npm.pkg.github.com
```

```sh theme={null}
npm install @flokitai/subscriptions-sdk
```

The package is private — authenticate to GitHub Packages with a token that has `read:packages` on the `flokitai` org. Contact your FloKit team for access.

<Note>
  Migrating from `@flokit/subscriptions-sdk` v1.0.x? Re-pin to the new scope, delete any host-side `purchase_start` / `trial_start` / `convert` / `cancel` emission around `purchase()` (the SDK emits them now), and pass `appState` at init. See [Migrating to v1.2](#migrating-to-v1-2).
</Note>

### Initialize once

```tsx theme={null}
import AsyncStorage from '@react-native-async-storage/async-storage';
import { AppState, Linking, Platform } from 'react-native';
import { initSubscriptionsSdk } from '@flokitai/subscriptions-sdk';

await initSubscriptionsSdk({
  baseUrl: process.env.EXPO_PUBLIC_GATEWAY_URL!, // your FloKit gateway URL
  appId: process.env.EXPO_PUBLIC_APP_ID!,
  appKey: process.env.EXPO_PUBLIC_APP_KEY!,      // publishable pk_... key
  storage: AsyncStorage,        // persists the queue, identity, and entitlement cache
  purchaseNative,               // your native store adapter (see below)
  openUrl: Linking.openURL,     // for the Paddle web-checkout bridge
  platform: Platform.OS,
  appState: AppState,           // enables sessions, lifecycle events, background flush
  appVersion: '1.4.2',          // optional device/app context, stamped on every event
  buildNumber: '482',
  osVersion: '17.5',
  deviceModel: 'iPhone15,3',
  locale: 'en-US',
});
```

`appState` is optional but strongly recommended: without it the SDK cannot observe
foreground/background transitions, so sessions never roll over, the lifecycle events never
fire, and the queue is not flushed when the app backgrounds. All device/app context fields
are host-supplied, which is what keeps the SDK dependency-free.

`userId` is optional at init — identity is anonymous-first. Call `identify(userId)` when the user
authenticates and `resetIdentity()` on logout.

### Remote paywalls are retired

<Warning>
  `RemotePaywall`, `Paywall`, and `getPaywall()` are still exported by v2.0, but the endpoint they
  call — `GET /api/paywall/config` — now answers `410` with `{"code": "PAYWALL_CONFIG_REMOVED"}`.
  Remote paywall configuration no longer exists: paywalls are **Flow-owned** and served with the
  Flow page. Render your paywall in your own UI and drive purchases with `purchase()`, or send the
  user to the app's published FloKit Flow page.
</Warning>

### Purchase and entitlement

`purchase()` runs the store adapter you injected at init, verifies the receipt through the gateway
(`POST /api/iap/receipts`), updates the entitlement cache, and auto-tracks `purchase_start` and
(on an active entitlement) `convert`:

```tsx theme={null}
import { purchase, getEntitlement, onEntitlementChange } from '@flokitai/subscriptions-sdk';

const entitlement = await purchase({ productId: 'com.yourapp.annual', placement: 'onboarding_complete' });
if (entitlement.active) {
  // unlock premium
}

// The entitlement gate (stale-while-revalidate, 5-min TTL):
const current = await getEntitlement();

// Live updates — purchase and restore both fire this:
const unsubscribe = onEntitlementChange((e) => setIsPro(e.active));
```

Your native store adapter is one function — `PurchaseNative` — that runs the store purchase and
returns the receipt the gateway verifies. See the README for a complete `expo-iap` example. Keeping
it injected is what lets the SDK stay free of any native dependency.

### Identity, handoff, and web checkout

* **Identity** — `identify(userId)` links the persisted `anonymous_ref` to a known user (queues a
  `user_alias` event and posts the server-side alias edge); `resetIdentity()` severs it on logout.
* **Web→app handoff** — `exchangeHandoffCode({ code })` redeems the single-use `flokit_code`
  (`hc_…`) from a FloKit web-funnel deep link (`POST /api/paywall/handoff/exchange`), adopts the web
  `anonymous_ref`, and enqueues an `app_activation` event; expired/used codes throw `HandoffCodeError`.
* **Attribution** — `submitInstallReferrer()` (Android Play Install Referrer) and
  `submitAdServicesToken()` (Apple Search Ads) feed install attribution.
* **Web checkout** — `startWebCheckout()` **always** throws `CheckoutUnavailableError` with
  `code: 'WEB_CHECKOUT_MIGRATED'`. It is retained only so old call sites fail loudly. Web checkout
  is created from a published Flow's paywall and checkout configuration and driven same-origin by
  `@flokitai/checkout-js` on the Flow page.
* **Restore** — `restore()` re-mints the token and fetches a fresh entitlement, bypassing the cache.
* **Diagnostics** — `getDiagnostics()` returns `{ sdkVersion, queueDepth, lastFlushAt,
  lastFlushError, droppedEvents, sessionId, tokenExpiresAt, entitlementCacheAgeMs, anonymousRef }`;
  `setDebugLogger(fn)` streams queue/token/checkout debug lines. `droppedEvents` counts events this
  process gave up on (queue overflow or a non-retriable reject) — both are silent by design, so a
  non-zero value is the only signal that telemetry is being lost.

### Sessions, lifecycle, and general events

* **`track(eventName, properties?, opts?)`** — general-purpose funnel events beyond the paywall. Names come from a canonical vocabulary shared verbatim with the hosted FloKit web funnel (`quiz_start`, `quiz_answered`, `offer_view`, `lead_captured`, …), so a web session and an app session describe one journey in one language. **Unknown names are refused on-device** — the gateway answers an unknown name with a non-retriable `400` that discards the whole batch, so failing fast keeps one typo from costing up to 49 unrelated events.
* **Sessions** — every event carries a `session_id`. A session survives a background trip shorter than 30 minutes (`sessionTimeoutMs` to override).
* **Lifecycle events** — `first_open` (once per install), `app_open`, `session_start`, `session_end`, and `day_active` (once per **local** calendar day, on that day's first foreground — the DAU spine).
* **Entitlement transitions** — `entitlement_granted` / `entitlement_revoked` fire when the cached entitlement flips. These are access events, not commercial ones: `convert` means they paid, `entitlement_granted` means they can use it, and a restore or a web checkout completing produces one without the other. The first observation carries `first_observation: true`. `entitlement_revoked` is the only on-device signal that a trial lapsed or a cancellation took effect — the entitlement is revalidated on foreground (60s TTL) to catch it.
* **Revenue** — `purchase({ priceUsdCents, currency })` stamps `revenue` and `currency` on the terminal event only, never on `purchase_start`, so abandoned attempts are not booked as income.

### Migrating to v2.0

1. **Entitlement reads moved.** `GET /api/paywall/entitlement` was deleted; the SDK now reads
   `GET /api/entitlements/current`. If you call the gateway directly, re-point it.
2. **Receipts moved.** `POST /api/paywall/receipt` became `POST /api/iap/receipts`. The body schema
   is unchanged.
3. **`product_id` → `entitlement_key`** on `Entitlement`, and it is now **opaque** — the store
   product id for native purchases, the Package key for web. Do not parse it. `expires_at` is now
   `string | null`, and `state`, `source_event_id`, and `updated_at` are new.
4. **`startWebCheckout()` always throws** `CheckoutUnavailableError` (`code: 'WEB_CHECKOUT_MIGRATED'`).
   Delete the call site and send the user to the app's published FloKit Flow page instead.
5. **`previewOfferPrice()` and `PricePreview` are gone.** So is `trackPaywallEvent({ offerId })`.
6. **Remote paywall config is gone.** `getPaywall()` / `RemotePaywall` hit an endpoint that now
   returns `410` — render your paywall yourself or use a Flow page.

### Migrating to v1.2

1. Re-pin to `@flokitai/subscriptions-sdk` with the `.npmrc` scope line above.
2. **Delete any host-side `purchase_start` / `trial_start` / `convert` / `cancel` emission around `purchase()`** — the SDK owns that funnel now. Hand-rolling them is what produced double-counted `purchase_start`, `trial_start` recorded against failed purchases, and every error filed as a user cancellation.
3. Pass `appState: AppState` (and ideally `appVersion`) to `initSubscriptionsSdk()`.
4. Expect roughly three extra events per launch (`first_open` once, then `session_start` and `app_open`).

### Reliable events

Funnel events ride a persisted FIFO queue: each event is stamped with a client-minted `event_id`,
`occurred_at`, `session_id`, `sdk_version`, and its `placement_id`; the queue survives app kills via
the injected `StorageAdapter` and is batch-POSTed to `POST /api/paywall/events` (≤50 per batch) with
capped exponential backoff. With `appState` wired, the queue also flushes when the app backgrounds.
Tracking **never throws**.

`purchase()` owns its whole funnel — it emits `purchase_start` on entry, then exactly **one**
terminal event: `convert`, `trial_start` (when `trialDays > 0`), `cancel` (user backed out), or
`purchase_failed` (with a `reason`). Do not emit these yourself. `impression`, `offer_tap`,
`checkout_*`, `user_alias`, `app_activation`, and the lifecycle events are auto-tracked too. For
anything else, use `track(eventName, properties?)` for vocabulary events or
`trackPaywallEvent({ eventType, placement, paywallId, variantId })` for paywall-scoped ones.

### Export surface

| Export                                                                      | Kind             | What it does                                                                                                                  |
| --------------------------------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `initSubscriptionsSdk` / `getSubscriptionsSdk` / `shutdownSubscriptionsSdk` | function         | Wire / read / tear down the SDK context                                                                                       |
| `getPaywall`                                                                | function         | Legacy paywall-config fetch — the endpoint now returns `410`. See [Remote paywalls are retired](#remote-paywalls-are-retired) |
| `RemotePaywall`                                                             | component        | Legacy fetch + render by placement — non-functional, the config endpoint returns `410`                                        |
| `Paywall`                                                                   | component        | Legacy renderer for an already-fetched `PaywallVariant` (no network)                                                          |
| `purchase`                                                                  | function         | Native purchase (injected adapter) → receipt verify → `Entitlement`; auto-tracks `purchase_start` / `convert`                 |
| `submitReceipt` / `fetchEntitlement`                                        | function         | Low-level receipt verify (`POST /api/iap/receipts`) / entitlement read (`GET /api/entitlements/current`)                      |
| `getEntitlement` / `restore` / `onEntitlementChange`                        | function         | Entitlement cache (stale-while-revalidate), forced restore, change listener                                                   |
| `trackPaywallEvent`                                                         | function         | Paywall-scoped funnel event through the queue; never throws                                                                   |
| `track`                                                                     | function         | General-purpose vocabulary event (`quiz_start`, `offer_view`, …); unknown names refused on-device                             |
| `EVENT_NAMES` / `PAYWALL_EVENT_NAMES`                                       | const            | The canonical event vocabulary, shared with the hosted web funnel                                                             |
| `startWebCheckout` / `CheckoutUnavailableError`                             | function / error | Retired bridge — always throws `CheckoutUnavailableError` (`code: 'WEB_CHECKOUT_MIGRATED'`)                                   |
| `SessionTracker` / `SESSION_TIMEOUT_MS`                                     | class / const    | Session + lifecycle tracking (advanced — `initSubscriptionsSdk({ appState })` wires it for you)                               |
| `identify` / `resetIdentity` / `Identity`                                   | function / class | Anonymous-first identity + `anonymous_ref` management                                                                         |
| `exchangeHandoffCode` / `HandoffCodeError`                                  | function / error | Web→app handoff-code redemption                                                                                               |
| `aliasUser` / `exchangeHandoff`                                             | function         | Lower-level identity alias + signed-handoff helpers                                                                           |
| `submitInstallReferrer` / `submitAdServicesToken`                           | function         | Play install-referrer / Apple AdServices attribution signals                                                                  |
| `getDiagnostics` / `setDebugLogger` / `SDK_VERSION`                         | function / const | Diagnostics snapshot, debug logger hook, version constant                                                                     |
| `EventQueue` / `createInMemoryStorage`                                      | class / function | Persisted retrying event queue + default `StorageAdapter` (advanced)                                                          |
| `fetchAppToken`                                                             | function         | Mint an app-session token (advanced — the context auto-mints and refreshes)                                                   |

Exported types include `Offer`, `PaywallVariant`, `PaywallConfigResponse`, `Entitlement`, `Provider`,
`EntitlementProvider`, `StoreReceipt`, `PaywallEventType`, `PurchaseNative`, `AppToken`,
`StorageAdapter`, `InitSubscriptionsSdkOptions`, `SubscriptionsSdkContext`, `IdentityState`,
`GetEntitlementOptions`, `RestoreResult`, `EntitlementListener`, `HandoffSession`,
`SdkDiagnostics`, `EventName`, `PaywallEventName`, `TrackOptions`, `JourneyRefs`,
`AppStateSource`, `SessionTrackerOptions`, plus the option/prop types for each export.

See the [Paywall API](/api-reference/paywall) and [Entitlements API](/api-reference/entitlements) for the underlying endpoints.

***

## Server SDKs (roadmap)

<Info>
  The packages below are planned, not shipping. Use the [v1 REST API preview](/api-reference/authentication) docs to shape requirements — package names are placeholders until publishing is ready.
</Info>

Server-side SDKs are planned as thin integration layers around the FloKit API: event ingestion, workspace and integration metadata, growth action approval workflows, and reporting reads for CAC, ROAS, payback, and LTV.

* `@flokit/node` — Node.js / TypeScript (planned)
* `flokit-python` — Python (planned)
* `flokit-go` — Go (planned)
