# What is tiun

## The commercial backend for SaaS, AI and digital products.

One system for authentication, payments, and customer data.\
No backend to build. No payment UI to design. Integrate once and go live.

[Get started](/quickstart) | [How it works](/how-it-works)

***

## Why tiun

* **No backend required** — tiun is the backend. Authentication, billing, and access control are handled for you.
* **No UI to design** — Checkout, login, and paywall overlays are built in and fully managed.
* **One SDK, all product types** — Subscriptions and time-based billing through the same integration.
* **Go live in a day** — Install the SDK, create a product in the dashboard, and you're ready to charge.

***

## Community

Join the conversation, ask questions, and share what you're building.

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Discord</strong></td><td>Ask questions, get help, and connect with other developers building on tiun.</td><td><a href="https://discord.gg/NCggN2ExZ9">https://discord.gg/NCggN2ExZ9</a></td></tr></tbody></table>


# How it works

## The core flow

tiun replaces the commercial backend you'd normally build yourself. Instead of stitching together a payment provider, an auth system, session management, and access control — you create a product in the tiun dashboard, and the SDK handles everything from checkout to access.

The flow is simple: you define a **product** (a subscription plan or a time-based rate), then call **checkout** (subscriptions) or **start** (time-based) from your frontend. tiun takes over — it shows the payment UI, authenticates the user, processes the payment, and manages the session. Your app receives an event when **access** is granted, and you show the content.

There's no backend to build. No webhook endpoints to maintain. No user tables to manage. tiun is the layer between your frontend and all the commercial infrastructure — payments, identity, access control, customer data. You write product logic; tiun handles the commercial stack.

***

## What tiun manages for you

| Feature                 | What you get                                                                                  |
| ----------------------- | --------------------------------------------------------------------------------------------- |
| **Checkout UI**         | Fully hosted payment overlay, no forms to build.                                              |
| **Payment processing**  | Cards, PayPal, Apple Pay, Google Pay, PrePaid (tiun credits), and Twint.                      |
| **Authentication**      | Email + OTP login, no passwords.                                                              |
| **Session persistence** | Returning users are recognized automatically — sessions and payment connections are restored. |
| **Access control**      | Subscription state and paywall events, handled for you.                                       |
| **Customer data**       | Users, subscriptions, and revenue tracked in your dashboard.                                  |

***

<h2 align="center">Next steps</h2>

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Quickstart</strong></td><td>Install the SDK and connect your app to tiun.</td><td><a href="/pages/NzxwLP2NcGpmQpc2wiCX">/pages/NzxwLP2NcGpmQpc2wiCX</a></td></tr><tr><td><strong>Guides</strong></td><td>Step by step instructions for different use-cases</td><td><a href="/spaces/v0tFzjHQUACOiUonWO2S">/spaces/v0tFzjHQUACOiUonWO2S</a></td></tr><tr><td><strong>Reference</strong></td><td>Core concepts and detailed explanations</td><td><a href="/spaces/T5ZmfCMnTzOFQHEyzxIC">/spaces/T5ZmfCMnTzOFQHEyzxIC</a></td></tr></tbody></table>


# Quickstart

Pick how you want to integrate tiun. Both paths end at the same place — a live integration. The full walkthroughs live in the guides; this page is the fast version.

{% hint style="info" %}
In case you have questions, check the [FAQ](/faq) or reach out to <support@tiun.app>
{% endhint %}

***

## Agent

Let your AI agent integrate tiun for you in three steps.

{% hint style="info" %}
**Prerequisites.** Step 1 uses the [GitHub CLI](https://cli.github.com/) to install the tiun-sdk skill. Run `gh --version` — if it returns a version number, you're good to go. Otherwise install it first, then come back here.
{% endhint %}

{% stepper %}
{% step %}

#### Install the tiun-sdk skill

Adds tiun-specific instructions to your codebase so your agent knows how to integrate tiun into your project.

```bash
gh skill install tiun-app/skills tiun-sdk
```

{% endstep %}

{% step %}

#### Connect the MCP server (recommended)

Lets your agent fetch your snippet ID, provider, and product details directly from your dashboard — no copy-paste, no guessing. The tiun-sdk skill detects and uses it automatically.

Add this MCP server in your agent's MCP settings:

```
https://mcp.tiun.business/
```

{% endstep %}

{% step %}

#### Prompt your agent

You're all set. Instruct your agent to integrate tiun. Before writing any code, it'll confirm a few things — subscription vs. time-based, which products, which routes to gate — so nothing gets wired up silently.

[Full agent integration guide](/guides/agent-integration/agent-integration) →
{% endstep %}
{% endstepper %}

***

## Manual

Install and initialize the SDK yourself, then follow a guide for the rest.

{% stepper %}
{% step %}

#### Install the SDK

Add `@tiun/sdk` to your project.

{% tabs %}
{% tab title="npm" %}

```bash
npm install @tiun/sdk
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add @tiun/sdk
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add @tiun/sdk
```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

#### Initialize the SDK

Import tiun and run `init` once at app startup. You can find your snippet ID in the [my.tiun.business dashboard](https://my.tiun.business/).

```javascript
import { tiun } from '@tiun/sdk';

tiun.init({
  snippetId: 'YOUR_SNIPPET_ID',
  language: 'en', // 'en' | 'de' | 'fr'
});
```

{% hint style="info" %}
**Set `language` to match your site.** Supported values are `'en'`, `'de'`, and `'fr'`. If omitted or invalid, the snippet UI falls back to `'en'`. See all options in [Initialization](/sdk/getting-started/initialization).
{% endhint %}
{% endstep %}

{% step %}

#### Follow a guide

Pick the walkthrough that matches what you're building:

* [**Authenticate your user**](/guides/authentication/authenticate-your-user) — Login, logout, and reading the signed-in user.
* [**Monetize with subscriptions**](/guides/subscriptions/monetize-with-subscriptions) — Recurring plans, checkout, and access control.
* [**Charge for time-based sessions**](/guides/time-based-billing/charge-for-time-based-sessions) — Usage-based billing, paywall events, and content metering.
  {% endstep %}
  {% endstepper %}


# FAQ

### What is tiun?

tiun is an end-to-end monetization platform for the web. It handles the parts you'd otherwise stitch together yourself — customer signup and login, checkout and payments, subscriptions, time-based billing, and access control — and exposes them through a single SDK. You drop in a few lines of code; tiun runs the rest.

### How do I get started?

Sign up at [my.tiun.business](https://my.tiun.business/), complete a short onboarding, and you'll land in the dashboard. From there you create a product, grab its product ID, and follow a [guide](https://docs.tiun.io/guides/) for the integration that matches what you're building. The [Quickstart](/quickstart) is the fast version; [Create a profile](/guides/getting-started/create-a-profile) is the longer walkthrough.

### What frameworks are supported?

The tiun SDK works with any JavaScript framework or vanilla JS — React, Vue, Next.js, Nuxt, plain HTML/JS, and more. See the [SDK examples](/sdk/examples/vue) for framework-specific setup.

### Does tiun work for native or mobile apps?

Yes. When you set up live, you choose the platform — **Web app** or **Native app** — and tiun configures the integration accordingly. The current developer documentation covers the web SDK in depth; if you're building a native app, contact <support@tiun.app> and we'll point you at the native integration path.

### Do I need a backend?

No. tiun works entirely client-side — checkout, authentication, and access control all happen in the browser, so you can ship a complete subscription or time-based app from a static site. If your backend serves protected data and you want to verify the user before responding, tiun also provides signed tokens and session-verification endpoints. See [server-side authentication verification](/guides/authentication/verify-server-side), [server-side subscription verification](/guides/subscriptions/verify-server-side), and [server-side session verification](/guides/time-based-billing/verify-server-side).

### What payment methods are supported?

Credit and debit cards, PayPal, Apple Pay, Google Pay, PrePaid (tiun credits), and Twint. Available methods may vary by region.

### What's the difference between subscription and time-based products?

* **Subscription** — the user pays a flat recurring fee (monthly, quarterly, yearly) for ongoing access. Best for SaaS, membership sites, and anything where the value is "access to the product".
* **Time-based** — the user pays for the time they actually spend with your content (per minute or per interval). Best for media-style products like articles, podcasts, or video where engagement varies per visit.

You can also offer both at once — see [Products](/reference) in Reference.

### Can I sell multiple products or tiers?

Yes. You can create as many products as you need — multiple subscription tiers (Light, Pro, Team), time-based products, or a mix of both. Your integration code references each by its product ID, and the user's `productAccess` array tells you which ones they're entitled to. The guides on [monetizing with subscriptions](/guides/subscriptions/monetize-with-subscriptions) and [charging for time-based sessions](/guides/time-based-billing/charge-for-time-based-sessions) show the patterns.

### How do users sign in?

With their email — no passwords. tiun sends a one-time code (OTP) and the user enters it to verify. Once signed in, the session is restored automatically on later visits. The full flow is in [Authentication / How it works](/reference/authentication/how-it-works) in Reference.

### How do I test my integration?

tiun runs **live** and **sandbox** as two **independent parallel environments** — separate domains, snippet IDs, products, product IDs, and API keys. Sandbox uses simulated payments; live handles real billing.

`localhost` is enabled by default on any port in sandbox and blocked in live. Most teams set up sandbox first: register a non-localhost test domain in the dashboard (prefer test/staging; use your live domain as a fallback), create test products, then use `sandbox: true` with your sandbox snippet ID and `p-test-...` product IDs while testing locally. You do not need to add `localhost` explicitly. Keep the dashboard on the same environment as your SDK while you work. See [setting up your environment](/guides/getting-started/set-up-environment), [Sandbox](/reference/generic/sandbox) in Reference, and [test flows](/guides/testing/test-flows) for a verification checklist.

### How do I go from sandbox to live?

Sandbox does not promote into live automatically. You **set up live as its own environment** (production domain, products, snippet ID, API keys), then point production traffic at it:

1. Remove `sandbox: true` from `tiun.init` (or set it to `false`).
2. Replace sandbox-specific values with live ones: snippet ID, product IDs (`p-test-...` → `p-live-...`), and server-side API URL/key if you verify server-side.

The SDK surface is identical — only environment-specific IDs and credentials change. Sandbox stays available for parallel testing. Full walkthrough: [setting up your environment](/guides/getting-started/set-up-environment).

***

Still stuck? Email <support@tiun.app>.


# Guides

End-to-end tutorials for getting tiun integrated. Each section starts with the basics and links to the matching [Reference](https://docs.tiun.io/reference/) pages for the concepts behind them.

## Getting started

* [Create a profile](/guides/getting-started/create-a-profile)
* [Set up your environment](/guides/getting-started/set-up-environment)
* [Create your first product](/guides/getting-started/creating-first-product)

## Authentication

* [Authenticate your user](/guides/authentication/authenticate-your-user)
* [Verify authentication server-side](/guides/authentication/verify-server-side)

## Subscriptions

* [Monetize with subscriptions](/guides/subscriptions/monetize-with-subscriptions)
* [Verify subscriptions server-side](/guides/subscriptions/verify-server-side)

## Time-based billing

* [Charge for time-based sessions](/guides/time-based-billing/charge-for-time-based-sessions)
* [Verify sessions server-side](/guides/time-based-billing/verify-server-side)

## Agent integration

* [Agent integration](/guides/agent-integration/agent-integration)

## Testing

* [Test flows](/guides/testing/test-flows)
* [Debugging](/guides/testing/debugging)

***

For framework-specific code snippets, see [React](/sdk/examples/react), [Vue](/sdk/examples/vue), [Nuxt](/sdk/examples/nuxt), and [Vanilla JS](/sdk/examples/vanilla-js) in the SDK reference.


# Create a profile

Before you can integrate tiun, you need a my.tiun.business account with a configured project. This is a one-time setup.

***

## 1. Sign up

Go to [my.tiun.business](https://my.tiun.business/) and sign up with your email.

***

## 2. Verify your email

Check your inbox for a verification link from tiun and click it to confirm the address.

***

## 3. Complete onboarding

After verifying, you'll be guided through a short onboarding flow that asks for a few details about your project — what you're building, what you'll be selling, and which pricing model fits (subscription, time-based, one time payment). There's no right or wrong here; you can update everything later from the dashboard.

***

## 4. Land in the dashboard

Once onboarding is done, you arrive in the [my.tiun.business dashboard](https://my.tiun.business/). This is where you'll create products, manage customers, and find the snippet ID and product IDs you need for the SDK.


# Set up your environment

tiun runs **two fully independent environments in parallel**: **live** (real customers, real payments) and **sandbox** (simulated payments, separate catalog and credentials). They are not two modes of the same data — each has its own domain, snippet ID, products, product IDs, and API keys.

Most teams **set up sandbox first** while building, test the integration there, then **set up live separately** and swap environment-specific values when they ship. You can configure either environment first; both can be active at the same time.

For the full picture, see [Sandbox](/reference/generic/sandbox) in Reference.

***

## Set up live

Live is set up through the **Get Started** modal in the dashboard. Open the dashboard (with the **Sandbox** toggle **off**) and you'll be prompted to:

1. **Pick a platform** — Web app or Native app. This tells tiun how to deliver the SDK and what kind of integration you're building.
2. **Enter a primary domain** — the domain where your production app is hosted (for example `yoursite.com`). tiun authorizes this domain for secure CORS access so the SDK can talk to the API from your origin. You can add more domains later.

Once that's done, create your **live** products in **Products**. Copy your **live snippet ID** and `p-live-...` product IDs from this dashboard view — they only work with live traffic.

`localhost` is blocked in **live**. Use live only from your registered production domains.

In your production app, initialize the SDK **without** `sandbox` (or with `sandbox: false`) and use those live IDs:

```javascript
import { tiun } from '@tiun/sdk';

tiun.init({
  snippetId: 'YOUR_LIVE_SNIPPET_ID',
  language: 'en',
});
```

***

## Set up sandbox

Sandbox is a **parallel copy** of your tiun setup. Configure it in the dashboard, then point your dev or staging app at it with `sandbox: true`.

### Configure sandbox in the dashboard

In the dashboard, turn on the **Sandbox** toggle in the sidebar. The entire view switches to sandbox data — products, customers, snippet ID, and API keys are all separate from live.

The first time you open sandbox, enter a **test domain** for that environment. Prefer your test/staging domain (for example `staging.yoursite.com`); if you do not have one yet, use your live domain as a fallback. You do not need to add `localhost` explicitly — it is enabled by default in sandbox. You can add more domains later from the same settings area.

Create your **sandbox** products here (mirroring what you plan to sell in live). Copy your **sandbox snippet ID** and `p-test-...` product IDs from this view.

### Point your app at sandbox

In the app you use for development or staging, pass `sandbox: true` and your **sandbox** snippet ID:

```javascript
import { tiun } from '@tiun/sdk';

tiun.init({
  snippetId: 'YOUR_SANDBOX_SNIPPET_ID',
  language: 'en',
  sandbox: true,
});
```

In **sandbox**, `localhost` is enabled by default on any port. To test locally, run with `sandbox: true` and your sandbox snippet ID.

{% hint style="info" %}
**While you're building, keep the dashboard and your app on the same environment.** If the dashboard shows live but your SDK has `sandbox: true` (or the other way around), IDs and sessions won't match what you see in the UI.
{% endhint %}

If you verify purchases or sessions on your server during development, use the **sandbox** API base URL and a **sandbox** API key. Live and sandbox keys are not shared.

***

## Going live from sandbox

When your sandbox integration works and you're ready for production traffic:

1. **Set up live** in the dashboard (if you haven't already) — production domain, products, snippet ID, and API keys.
2. In your **production** deployment, remove `sandbox: true` from `tiun.init` (or set it to `false`).
3. Replace environment-specific values with their **live** counterparts:
   * Snippet ID
   * Product IDs (`p-test-...` → `p-live-...`)
   * Server-side API base URL and API key (if applicable)

The SDK API is identical across environments — only these IDs and credentials change. Your sandbox environment stays available for continued testing in parallel.


# Create your first product

Every tiun integration is wired to one or more **products** you configure in the dashboard. This guide walks through creating a product end-to-end and captures the **product ID** you'll use in code.

***

## 1. Open Products

In the [my.tiun.business dashboard](https://my.tiun.business/), go to **Products**. The catalog belongs to whichever environment the **Sandbox** toggle is showing — live and sandbox each have their own products and IDs. Most teams create products in **sandbox** first while integrating, then recreate them in **live** when shipping.

***

## 2. Pick a product type

Start a new product and choose the type that matches what you're selling. tiun supports two:

* **Subscription** — you charge a flat recurring fee on a schedule (monthly, quarterly, yearly). Best when the value is access to the product itself over time.
* **Time-based** — you charge for the time the user spends with your content. Best for media-style products (articles, audio, video) where engagement varies per visit.

If you're not sure, see [Products](/reference) in Reference for a fuller comparison.

***

## 3. Configure pricing

The configuration depends on the product type. Here's what each field actually represents — you'll be asked for the relevant ones during creation.

**For a subscription product:**

* **Name** — what the user will see (for example "Pro", "Light").
* **Recurring fee** — the amount charged each cycle.
* **Billing interval** — how often the fee is charged (monthly, quarterly, yearly).
* **Trial period** (optional) — a window where the user has access at a reduced price, including `0` for a free trial.

**For a time-based product:**

* **Name** — internal label for the product.
* **Interval** — the smallest billable unit of time (for example one minute).
* **Interval fee** — what one interval costs.
* **Monthly limit** (optional) — caps the total charge per month so heavy users don't run up surprise bills.

Field semantics live in the [Products](/reference) reference if you want the full picture.

***

## 4. Save and copy the product ID

Once the product is saved, copy its **product ID** — it's prefixed `p-test-...` in sandbox and `p-live-...` in live. Use it with the matching SDK environment (`sandbox: true` for `p-test-...`, live default for `p-live-...`). See [Sandbox](/reference/generic/sandbox) if you need both environments.


# Authenticate your user

This guide wires up login, logout, and a signed-in UI in your app. It does **not** cover product gating — for subscriptions, see [monetizing with subscriptions](/guides/subscriptions/monetize-with-subscriptions); for time-based billing, see [charging for time-based sessions](/guides/time-based-billing/charge-for-time-based-sessions).

***

## 1. Install and initialize the SDK

If you haven't yet, install `@tiun/sdk` and initialize it. The [Quickstart](/quickstart) has the full install commands; here's the minimal init:

```javascript
import { tiun } from '@tiun/sdk';

tiun.init({
  snippetId: 'YOUR_SNIPPET_ID',
  language: 'en',
});
```

***

## 2. Add a login button

`tiun.login()` opens the tiun login overlay where the user enters their email and verifies via phone OTP. The full login flow — what the overlay does and how the session is persisted — is covered in [Authentication / How it works](/reference/authentication/how-it-works) in Reference.

```javascript
function onClickLogin() {
  tiun.login();
}
```

After a successful login, `userChange` fires with `event: 'login'`.

***

## 3. Add a logout button

`tiun.logout()` clears the session on the current device.

```javascript
function onClickLogout() {
  tiun.logout();
}
```

After logout, `userChange` fires with `event: 'logout'` and `user` is `null`.

***

## 4. React to user state changes

Listen to `userChange` to keep your UI in sync. The handler fires on session restore, login, logout, and anytime user state updates — so you don't need to poll.

```javascript
tiun.on('userChange', (data) => {
  if (!data.isAuthenticated) {
    showLoginButton();
    return;
  }

  showLogoutButton(data.user.email);
});
```

`data.user` is the **user object** — `null` when nobody is signed in, otherwise it carries `userId`, `email`, and (for subscription users) `productAccess`. See [User object](/reference/authentication/user-object) in Reference for the full shape and when each field is populated.

***

## 5. Session restore is automatic

When a returning user loads your app, tiun restores their session automatically. `userChange` fires with `event: 'init'` and the same payload as above — so the handler from step 4 covers this case too.

***

## Full example

```javascript
import { tiun } from '@tiun/sdk';

tiun.on('userChange', (data) => {
  if (!data.isAuthenticated) {
    showLoginButton();
    return;
  }

  showLogoutButton(data.user.email);
});

tiun.init({
  snippetId: 'YOUR_SNIPPET_ID',
  language: 'en',
});

function login() {
  tiun.login();
}

function logout() {
  tiun.logout();
}
```

***

If your backend serves protected data, you'll also want to verify the user's identity server-side — see [server-side authentication verification](/guides/authentication/verify-server-side).


# Verify authentication server-side

Client-side checks (`isAuthenticated`, `userChange`) are enough for UI gating, but your backend should not trust the browser alone. This guide walks you through verifying a tiun user's identity on your server before serving protected data.

<figure><img src="/files/VBoSsKq6aXmgDzIpGLAa" alt="Verify user flow: frontend gets token, sends to backend, backend exchanges with tiun API for the user object"><figcaption></figcaption></figure>

***

## How it works

1. Your frontend asks tiun for a signed verification token.
2. It sends that token to your backend on protected requests.
3. Your backend exchanges the token with the [tiun UserVerification API](https://app.gitbook.com/s/ZMTdS5A9nvRqJOIo1RJM/userverification) for the user object.
4. If the user object reports `isAuthenticated: true`, you serve the protected data.

The token is a **signed JWT**, valid for **5 minutes**. The verified user object includes `isAuthenticated` plus a `userInfo` payload (`userId`, `email`, `productAccess`) — see [User object](/reference/authentication/user-object) in Reference for the full shape.

***

## Setup: API key

Before you can verify users on your server, create an API key in the dashboard: open **APIs** in the sidebar and click **Create new key**. Store it securely in your backend environment variables.

***

## 1. Get the verification token

Call `getUserVerificationToken()` on the frontend. It returns a token if the user is authenticated, or `null` if they're not. Attach it to your API request — most commonly as a Bearer token in the `Authorization` header.

```javascript
async function fetchProtectedData() {
  const token = await tiun.getUserVerificationToken();

  if (!token) {
    return null;
  }

  const response = await fetch('/api/protected', {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  if (!response.ok) return null;
  return response.json();
}
```

***

## 2. Verify the token on your server

Call the [tiun UserVerification API](https://app.gitbook.com/s/ZMTdS5A9nvRqJOIo1RJM/userverification) to exchange the token for the user object.

**Endpoint:**

`POST /live_api/s2s/v1/users/verification`

**Base URLs:**

| Environment | URL                             |
| ----------- | ------------------------------- |
| Live        | `https://api.tiun.live`         |
| Sandbox     | `https://api-sandbox.tiun.live` |

Use the base URL and API key from the **same environment** as your frontend (`sandbox: true` in the SDK → sandbox URL and sandbox key). API keys are not shared between live and sandbox.

**Header:** `X-TIUN-API-KEY: <your-api-key>`

**Body:** `{ "userVerificationToken": "<token>" }`

**Response codes:**

| Status | Meaning                              |
| ------ | ------------------------------------ |
| `200`  | User object returned — read the body |
| `401`  | API key is invalid                   |

A `200` response carries the user object:

```json
{
  "isAuthenticated": true,
  "userInfo": {
    "userId": "u-...",
    "email": "user@example.com",
    "productAccess": ["p-live-..."]
  }
}
```

***

## 3. Check `isAuthenticated`

Once you have the user object, gate the response on `isAuthenticated`. If it's `false`, treat the request as unauthenticated — the API key was accepted but the underlying user session is no longer active.

For subscription-specific gating on top of identity, see [server-side subscription verification](/guides/subscriptions/verify-server-side) — same endpoint, plus a `productAccess` check.

***

## Full round-trip

**Frontend** — request protected data after auth state is known:

```javascript
tiun.on('userChange', async (data) => {
  if (!data.isAuthenticated) {
    showPublicContent();
    return;
  }

  const result = await fetchProtectedData();

  if (result) {
    showProtectedContent(result);
  }
});

async function fetchProtectedData() {
  const token = await tiun.getUserVerificationToken();

  if (!token) return null;

  const res = await fetch('/api/protected', {
    headers: { Authorization: `Bearer ${token}` },
  });

  if (!res.ok) return null;
  return res.json();
}
```

**Backend** — exchange the token for the user object and serve:

```javascript
import express from 'express';

const app = express();

const BASE_URL = process.env.TIUN_API_BASE || 'https://api-sandbox.tiun.live';
const API_KEY = process.env.TIUN_API_KEY;

app.get('/api/protected', async (req, res) => {
  const auth = req.headers.authorization;
  const token = auth?.startsWith('Bearer ') ? auth.slice(7) : null;

  if (!token) {
    // missing token → reject as unauthenticated
    return;
  }

  const tiunResponse = await fetch(
    `${BASE_URL}/live_api/s2s/v1/users/verification`,
    {
      method: 'POST',
      headers: {
        'X-TIUN-API-KEY': API_KEY,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ userVerificationToken: token }),
    },
  );

  if (tiunResponse.status !== 200) {
    // API key invalid or upstream error → fail closed
    return;
  }

  const user = await tiunResponse.json();

  if (!user.isAuthenticated) {
    // token was exchanged but the user has no active session → reject
    return;
  }

  // user authenticated → serve the protected content
  // user.userInfo carries userId, email, productAccess
});
```

***

{% hint style="info" %}
Live and sandbox are independent environments — each has its own API base URL and API keys. Use sandbox credentials while your app runs with `sandbox: true`; switch URL and key together when you ship live traffic. See [Sandbox](/reference/generic/sandbox).
{% endhint %}


# Monetize with subscriptions

This guide walks through wiring up a subscription product — pricing buttons, checkout, and gating content based on what the user has purchased. Login and logout are not covered here; see [authenticating your user](/guides/authentication/authenticate-your-user) for those.

***

## 1. Create subscription products

Create one product per plan in the dashboard. The full walkthrough is in [creating your first product](/guides/getting-started/creating-first-product) — repeat it for each plan (for example, **Light** and **Pro**) and copy the product IDs.

***

## 2. Install and initialize the SDK

Install `@tiun/sdk` and call `tiun.init` once at app startup. The [Quickstart](/quickstart) has the full install commands.

```javascript
import { tiun } from '@tiun/sdk';

tiun.init({
  snippetId: 'YOUR_SNIPPET_ID',
  language: 'en',
});
```

***

## 3. Build a pricing page

Keep product IDs in one place so checkout buttons, gating logic, and analytics all reference the same source. The JS will throw on typos instead of failing silently.

```javascript
const TIUN_PRODUCTS = {
  light: 'p-live-light',
  pro: 'p-live-pro',
};
```

Wire one button per plan, each calling `tiun.checkout()` with the matching product ID:

```javascript
function onClickLight() {
  tiun.checkout({ productId: TIUN_PRODUCTS.light });
}

function onClickPro() {
  tiun.checkout({ productId: TIUN_PRODUCTS.pro });
}
```

When the user clicks, tiun opens the checkout overlay — it collects their email, handles identity verification, processes payment, and grants access in one flow. For what the overlay does behind the scenes, see [Checkout / How it works](/reference/checkout/how-it-works) in Reference.

***

## 4. Handle checkout success

After a successful purchase, `userChange` fires with `event: 'checkout'`. The user object includes their email and a `productAccess` array containing the product they just bought. `productAccess` is the canonical source of truth for what the user can access — see [Product access](/reference/checkout/product-access) in Reference for how it's populated and updated.

```javascript
tiun.on('userChange', (data) => {
  if (data.event === 'checkout' && data.user) {
    console.log('Subscribed to:', data.user.productAccess);
  }
});
```

For the journey from checkout through renewal and expiration, see [Subscriptions](/reference/checkout/subscriptions) in Reference.

***

## 5. Gate content based on access

Use the `productAccess` array to decide what to show. The same `userChange` handler covers checkout success, login, logout, and the initial session restore on page load — so your gates stay in sync without extra work.

```javascript
tiun.on('userChange', (data) => {
  if (!data.isAuthenticated) {
    showPricingPage();
    return;
  }

  const hasProAccess = data.user.productAccess.includes(TIUN_PRODUCTS.pro);
  const hasLightAccess = data.user.productAccess.includes(TIUN_PRODUCTS.light);

  if (hasProAccess) {
    showProContent();
  } else if (hasLightAccess) {
    showLightContent();
  } else {
    showUpgradePrompt();
  }
});
```

***

## 6. Add login and logout

Returning users can sign in without going through checkout again. The setup is covered in [authenticating your user](/guides/authentication/authenticate-your-user) — once that's wired up, your subscribers can log back in and the same `userChange` handler picks up their existing `productAccess`.

***

## 7. Go live

If you've been testing in sandbox, set up **live as a separate environment** in the dashboard, then for production traffic:

1. Remove `sandbox: true` from your `tiun.init` (or set it to `false`).
2. Use your **live** snippet ID and live product IDs (prefixed `p-live-`).
3. If you verify server-side, switch to the live API base URL and a live API key.

See [setting up your environment](/guides/getting-started/set-up-environment) for the full sandbox / live workflow.

***

## Full example

The subscription-specific integration in one place (login / logout wiring lives in the Auth guide):

```javascript
import { tiun } from '@tiun/sdk';

const TIUN_PRODUCTS = {
  light: 'p-live-light',
  pro: 'p-live-pro',
};

tiun.on('userChange', (data) => {
  if (!data.isAuthenticated) {
    showPricingPage();
    return;
  }

  const hasProAccess = data.user.productAccess.includes(TIUN_PRODUCTS.pro);
  const hasLightAccess = data.user.productAccess.includes(TIUN_PRODUCTS.light);

  if (hasProAccess) {
    showProContent();
  } else if (hasLightAccess) {
    showLightContent();
  } else {
    showUpgradePrompt();
  }
});

tiun.init({
  snippetId: 'YOUR_SNIPPET_ID',
  language: 'en',
});

function checkoutLight() {
  tiun.checkout({ productId: TIUN_PRODUCTS.light });
}

function checkoutPro() {
  tiun.checkout({ productId: TIUN_PRODUCTS.pro });
}
```

***

For framework-specific implementations, see [React](/sdk/examples/react), [Vue](/sdk/examples/vue), and [Nuxt](/sdk/examples/nuxt) in the SDK reference.

If your backend needs to verify the user's subscription before serving protected data, see [server-side subscription verification](/guides/subscriptions/verify-server-side).


# Verify subscriptions server-side

Subscription verification on the backend is **identity verification plus a `productAccess` check**. If your server serves data that should only be returned to subscribers of a specific product, this guide is what you want.

It uses the same endpoint as [server-side authentication verification](/guides/authentication/verify-server-side). The flow is identical up to step 3 — the only difference is what you check on the verified user object.

<figure><img src="/files/CB6kj4XujWcZNwqxgWEL" alt="Verify subscriptions flow: frontend gets token, sends to backend, backend exchanges with tiun API for the user object"><figcaption></figcaption></figure>

***

## How it works

1. Your frontend asks tiun for a signed verification token.
2. It sends that token to your backend on protected requests.
3. Your backend exchanges the token with the [tiun UserVerification API](https://app.gitbook.com/s/ZMTdS5A9nvRqJOIo1RJM/userverification) for the user object.
4. If `isAuthenticated: true` **and** `userInfo.productAccess` contains the required product ID, you serve the protected data.

The verified user object includes `isAuthenticated` plus a `userInfo` payload (`userId`, `email`, `productAccess`) — see [User object](/reference/authentication/user-object) and [Product access](/reference/checkout/product-access) in Reference.

***

## Setup: API key

Create an API key in the dashboard under **APIs**, then store it in your backend environment variables. Same key as the one used for [server-side authentication verification](/guides/authentication/verify-server-side).

***

## 1. Get the verification token

Identical to the auth verification flow — call `tiun.getUserVerificationToken()` on the frontend and send the result to your backend as a Bearer token. See [getting the verification token](/guides/authentication/verify-server-side#1-get-the-verification-token) for the snippet.

***

## 2. Verify the token on your server

Same endpoint as the auth verification flow — POST the token to the [tiun UserVerification API](https://app.gitbook.com/s/ZMTdS5A9nvRqJOIo1RJM/userverification) and read the user object from the response.

**Endpoint:**

`POST /live_api/s2s/v1/users/verification`

**Base URLs:**

| Environment | URL                             |
| ----------- | ------------------------------- |
| Live        | `https://api.tiun.live`         |
| Sandbox     | `https://api-sandbox.tiun.live` |

Use the base URL and API key from the **same environment** as your frontend (`sandbox: true` in the SDK → sandbox URL and sandbox key). API keys are not shared between live and sandbox.

**Header:** `X-TIUN-API-KEY: <your-api-key>`

**Body:** `{ "userVerificationToken": "<token>" }`

A `200` response carries the user object:

```json
{
  "isAuthenticated": true,
  "userInfo": {
    "userId": "u-...",
    "email": "user@example.com",
    "productAccess": ["p-live-pro"]
  }
}
```

***

## 3. Check `productAccess`

Once you have the user object, gate the response on two things:

* `isAuthenticated` must be `true` — the user is signed in.
* `userInfo.productAccess` must include the **product ID** required by this endpoint.

Splitting these two checks lets you distinguish "not signed in" from "signed in but without the subscription" — useful UX feedback (prompt login vs. prompt upgrade).

***

## Full round-trip

**Frontend** — request the protected resource when the user is authenticated:

```javascript
tiun.on('userChange', async (data) => {
  if (!data.isAuthenticated) {
    showPublicContent();
    return;
  }

  const result = await fetchProContent();

  if (result) {
    showProContent(result);
  }
});

async function fetchProContent() {
  const token = await tiun.getUserVerificationToken();

  if (!token) return null;

  const res = await fetch('/api/pro-content', {
    headers: { Authorization: `Bearer ${token}` },
  });

  if (!res.ok) return null;
  return res.json();
}
```

**Backend** — exchange the token, check identity, then check product access:

```javascript
import express from 'express';

const app = express();

const BASE_URL = process.env.TIUN_API_BASE || 'https://api-sandbox.tiun.live';
const API_KEY = process.env.TIUN_API_KEY;
const REQUIRED_PRODUCT = 'p-live-pro';

app.get('/api/pro-content', async (req, res) => {
  const auth = req.headers.authorization;
  const token = auth?.startsWith('Bearer ') ? auth.slice(7) : null;

  if (!token) {
    // missing token → reject as unauthenticated
    return;
  }

  const tiunResponse = await fetch(
    `${BASE_URL}/live_api/s2s/v1/users/verification`,
    {
      method: 'POST',
      headers: {
        'X-TIUN-API-KEY': API_KEY,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ userVerificationToken: token }),
    },
  );

  if (tiunResponse.status !== 200) {
    // API key invalid or upstream error → fail closed
    return;
  }

  const user = await tiunResponse.json();

  if (!user.isAuthenticated) {
    // not signed in → reject
    return;
  }

  if (!user.userInfo?.productAccess?.includes(REQUIRED_PRODUCT)) {
    // signed in but no subscription → reject (prompt upgrade)
    return;
  }

  // user has access → serve the pro content
});
```

***

## Multiple plans

If you offer tiers (for example **Light** unlocks some endpoints, **Pro** unlocks all), check for the highest applicable tier first:

```javascript
const hasPro = user.userInfo?.productAccess?.includes('p-live-pro');
const hasLight = user.userInfo?.productAccess?.includes('p-live-light');

if (hasPro) {
  // serve full Pro content
} else if (hasLight) {
  // serve Light content
} else {
  // no qualifying subscription → reject
}
```

***

{% hint style="info" %}
Live and sandbox are independent environments — each has its own API base URL and API keys. Use sandbox credentials while your app runs with `sandbox: true`; switch URL and key together when you ship live traffic. See [Sandbox](/reference/generic/sandbox).
{% endhint %}


# Charge for time-based sessions

This guide walks you through wiring up a time-based paywall — the user connects a payment method, opens premium content, and is billed for the time they spend with it. By the end, you'll have a paywall, a billing session, and content metering.

***

## 1. Create a time-based product

Create a time-based product in the dashboard and copy its product ID. The full walkthrough is in [creating your first product](/guides/getting-started/creating-first-product) — choose **Time-based** as the product type.

***

## 2. Install and initialize the SDK

Install `@tiun/sdk` and call `tiun.init` once at startup. The [Quickstart](/quickstart) has the full install commands.

```javascript
import { tiun } from '@tiun/sdk';

tiun.init({
  snippetId: 'YOUR_SNIPPET_ID',
  language: 'en',
});
```

***

## 3. Start with the paywall visible

In time-based billing, the default state is **locked** — the user doesn't have access until they connect a payment method. Render your app with the paywall showing and premium content hidden. For the full mental model of how time-based access works, see [Time-based / How it works](/reference/time-based/how-it-works) in Reference.

***

## 4. Listen for paywall events

tiun tells your app when to show or hide the paywall through two events:

```javascript
tiun.on('paywallShow', () => {
  showPaywall();
  hidePremiumContent();
});

tiun.on('paywallHide', (data) => {
  hidePaywall();
  showPremiumContent();
  // data.sessionId is available for server-side verification
});
```

`paywallShow` fires when the user doesn't have access (no payment method yet, or session ended). `paywallHide` fires when access is granted and a [session](/reference/time-based/sessions) is active; the payload includes a `sessionId` you can pass to your backend for [server-side session verification](/guides/time-based-billing/verify-server-side). For the full event contract, see [Access](/reference/time-based/access) in Reference.

***

## 5. Start a billing session

Add a button on your paywall that opens the connect overlay. `tiun.start()` collects the user's payment method and starts their session.

```javascript
function onClickGetAccess() {
  tiun.start();
}
```

After the user connects, `paywallHide` fires and the user has access.

***

## 6. Manage content on route changes

Tell tiun what the user is viewing so it can meter and bill correctly. Call `setContent()` on every route or page change. The full list of content types, media types (audio / video), and metering behavior lives in [Protecting content](/reference/time-based/protecting-content) in Reference.

```javascript
function onRouteChange(path) {
  const isPaid = isPaidContent(path);

  tiun.setContent({
    type: isPaid ? 'active' : 'inactive',
    contentId: path,
  });
}
```

When content is `'inactive'`, the session pauses and the user is not billed. When it switches back to `'active'`, the session resumes.

***

## 7. Go live

If you've been testing in sandbox, set up **live as a separate environment** in the dashboard, then for production traffic:

1. Remove `sandbox: true` from your `tiun.init` (or set it to `false`).
2. Use your **live** snippet ID and live product ID (prefixed `p-live-`).
3. If you verify server-side, switch to the live API base URL and a live API key.

See [setting up your environment](/guides/getting-started/set-up-environment) for the full sandbox / live workflow.

***

## Full example

```javascript
import { tiun } from '@tiun/sdk';

tiun.init({
  snippetId: 'YOUR_SNIPPET_ID',
  language: 'en',
});

tiun.on('paywallShow', () => {
  showPaywall();
  hidePremiumContent();
});

tiun.on('paywallHide', (data) => {
  hidePaywall();
  showPremiumContent();
});

function getAccess() {
  tiun.start();
}

function onRouteChange(path) {
  const isPaid = isPaidContent(path);

  tiun.setContent({
    type: isPaid ? 'active' : 'inactive',
    contentId: path,
  });
}
```

***

For framework-specific implementations, see [React](/sdk/examples/react), [Vue](/sdk/examples/vue), and [Nuxt](/sdk/examples/nuxt) in the SDK reference.

If your backend serves premium content, also see [server-side session verification](/guides/time-based-billing/verify-server-side).


# Verify sessions server-side

For time-based billing, access is controlled through paywall events on the client. But if your backend serves premium content (articles, streams, API data), you need to verify the session on the server before delivering it. This guide walks you through that flow.

<figure><img src="/files/BYRfVYZWXL773vvgpT6r" alt="Verify sessions flow: frontend captures sessionId, sends to backend, backend validates with tiun API"><figcaption></figcaption></figure>

***

## How it works

1. The `paywallHide` event includes a `sessionId` when the user has access.
2. Your frontend sends that session ID to your backend.
3. Your backend validates it against the [tiun Session API](https://docs.tiun.io/api-reference/).
4. If valid, you serve the premium content.

For the `sessionId` concept itself and its lifecycle, see [Sessions](/reference/time-based/sessions) in Reference.

***

## Setup: API key

Before you can verify sessions, create an API key in the dashboard: open **APIs** in the sidebar and click **Create new key**. Store it securely in your backend environment variables.

***

## 1. Capture the session ID

When `paywallHide` fires, its payload includes a `sessionId`. Pass it to your backend on the protected request.

```javascript
tiun.on('paywallHide', async (data) => {
  const content = await fetchPremiumContent(data.sessionId);

  if (content) {
    renderContent(content);
  }
});

async function fetchPremiumContent(sessionId) {
  const response = await fetch('/api/premium-content', {
    headers: {
      'X-Session-Id': sessionId,
    },
  });

  if (!response.ok) return null;
  return response.json();
}
```

***

## 2. Validate the session on your server

Call the [tiun Session API](https://docs.tiun.io/api-reference/) to confirm the session is valid before serving content.

**Endpoint:**

`PATCH /live_api/s2s/v1/sessions/{sessionId}/status`

**Base URLs:**

| Environment | URL                             |
| ----------- | ------------------------------- |
| Live        | `https://api.tiun.live`         |
| Sandbox     | `https://api-sandbox.tiun.live` |

Use the base URL and API key from the **same environment** as your frontend (`sandbox: true` in the SDK → sandbox URL and sandbox key). API keys are not shared between live and sandbox.

**Header:** `X-TIUN-API-KEY: <your-api-key>`

**Response codes:**

| Status | Meaning                                           |
| ------ | ------------------------------------------------- |
| `200`  | Session is valid — serve the content              |
| `404`  | Session is invalid, expired, or user has no funds |
| `401`  | API key is incorrect                              |

***

## 3. Backend implementation

```javascript
import express from 'express';

const app = express();

const BASE_URL = process.env.TIUN_API_BASE || 'https://api-sandbox.tiun.live';
const API_KEY = process.env.TIUN_API_KEY;

app.get('/api/premium-content', async (req, res) => {
  const sessionId = req.headers['x-session-id'];

  if (!sessionId) {
    // missing session id → reject as no access
    return;
  }

  const tiunResponse = await fetch(
    `${BASE_URL}/live_api/s2s/v1/sessions/${sessionId}/status`,
    {
      method: 'PATCH',
      headers: { 'X-TIUN-API-KEY': API_KEY },
    },
  );

  if (tiunResponse.status === 200) {
    // session valid → serve the premium content
  } else if (tiunResponse.status === 404) {
    // session invalid, expired, or user out of funds → deny
  } else {
    // unexpected error from tiun → fail closed
  }
});
```

***

## Full round-trip

**Frontend** — capture the session ID and fetch content:

```javascript
import { tiun } from '@tiun/sdk';

tiun.init({
  snippetId: 'YOUR_SNIPPET_ID',
  language: 'en',
});

tiun.on('paywallHide', async (data) => {
  const response = await fetch('/api/premium-content', {
    headers: { 'X-Session-Id': data.sessionId },
  });

  if (response.ok) {
    const content = await response.json();
    renderContent(content);
  }
});

tiun.on('paywallShow', () => {
  showPaywall();
});
```

**Backend** — validate and serve:

```javascript
import express from 'express';

const app = express();

const BASE_URL = process.env.TIUN_API_BASE || 'https://api-sandbox.tiun.live';
const API_KEY = process.env.TIUN_API_KEY;

app.get('/api/premium-content', async (req, res) => {
  const sessionId = req.headers['x-session-id'];

  if (!sessionId) {
    // missing session id → reject as no access
    return;
  }

  const upstream = await fetch(
    `${BASE_URL}/live_api/s2s/v1/sessions/${sessionId}/status`,
    {
      method: 'PATCH',
      headers: { 'X-TIUN-API-KEY': API_KEY },
    },
  );

  if (upstream.status === 200) {
    // session valid → serve the premium content
  } else {
    // session invalid or upstream error → deny
  }
});
```

***

{% hint style="info" %}
Live and sandbox are independent environments — each has its own API base URL and API keys. Use sandbox credentials while your app runs with `sandbox: true`; switch URL and key together when you ship live traffic. See [Sandbox](/reference/generic/sandbox).
{% endhint %}


# Agent integration

Instead of wiring tiun by hand, you can let your AI coding agent do it. Two pieces make this work well:

* A **skill** — a curated, agent-readable instruction pack that teaches the agent the patterns it should follow when integrating tiun.
* An **MCP server** — a live read-only connection to your my.tiun.business account, so the agent fetches your snippet ID, provider, and product list directly instead of asking you to copy-paste them.

With both in place, you can prompt your agent with something as short as "integrate tiun" and it'll discover the right products, confirm the integration mode, and generate the code.

***

## 1. Prerequisites

Install the [GitHub CLI](https://cli.github.com/). It's how you install the tiun-sdk skill in the next step.

```bash
gh --version
```

If the command prints a version, you're set. Otherwise install it and re-check.

***

## 2. Install the tiun-sdk skill

A skill adds tiun-specific instructions to your codebase so your agent knows how to integrate tiun correctly — which APIs to call, how to handle login, when to use `userChange` vs. `paywallShow`, etc.

```bash
gh skill install tiun-app/skills tiun-sdk
```

This installs the skill into your project where your agent can pick it up automatically.

***

## 3. Connect the MCP server

Add the tiun MCP server to your agent's MCP settings:

```
https://mcp.tiun.business/
```

How you register an MCP server **depends on the agent** — Cursor, Claude Code, and other clients all wire MCP servers in slightly different ways (a config file, a settings panel, a CLI command, etc.). Check your agent's documentation for "MCP" or "Model Context Protocol" to find the exact steps.

Once connected, the agent can list your providers, snippet IDs, and products in real time. The tiun-sdk skill detects the MCP and uses it automatically — so when you ask the agent to integrate, it pulls data from your account instead of relying on what you paste in.

**Sandbox and live are separate environments** with their own snippet IDs and product IDs. MCP returns both (each provider is tagged sandbox or live). If you have products set up in sandbox and in live, the agent can pick the right IDs for the environment you're building against — sandbox while you're developing (`sandbox: true`), live when you're shipping production traffic.

This step is optional, but recommended: without MCP the agent has no view of your dashboard and you'll have to hand it the snippet ID and product IDs yourself.

See [Sandbox](/reference/generic/sandbox) for how the two environments relate.

***

## 4. Prompt your agent

You're ready. Open your agent in your project and ask it to integrate tiun. Examples:

* **General** — "Integrate tiun." The agent will ask which integration mode (subscription, time-based, or both), which products to wire, and any routes to gate.
* **Specific** — "Set up a subscription paywall on the `/premium` route using my Pro plan." The agent will confirm the product ID via the MCP and write the integration.
* **Time-based** — "Add a time-based paywall to the article view and meter content on route changes."

Before writing code, the agent will confirm the integration mode, the product(s) to use, and the gating points — so nothing is wired silently.

***

## Available skills

The tiun-sdk skill is one of several maintained skills. The full list lives at [tiun-app/skills](https://github.com/tiun-app/skills) on GitHub.


# Test flows

Use this checklist while validating a tiun integration in **sandbox** — the parallel test environment with its own domain, snippet ID, product IDs (`p-test-...`), and API keys. Items are grouped by product type so you can focus on what applies to your integration.

## General (all product types)

* Overlay opens when triggered (`tiun.checkout()` for subscriptions, `tiun.start()` for time-based).
* Test payment methods complete successfully.
* Overlay closes cleanly after success or cancel.
* Failures surface through the SDK **`error`** event so you can log or show UI.
* If you verify purchases or sessions on your server, requests use the **sandbox** base URL and a **sandbox** API key (not shared with live). Replace both with live URL and key when you ship.

## Subscriptions

* `userChange` fires after checkout with `event: 'checkout'` and updated `productAccess`.
* Login and logout behave correctly (see [authenticating your user](/guides/authentication/authenticate-your-user)).
* After a successful purchase, `productAccess` includes the expected product ID.
* Returning to the app restores the user session without forcing login (`userChange` with `event: 'init'`).
* Removing or expiring access (where testable) updates `productAccess` and triggers `userChange`.

## Time-based billing

* `paywallShow` fires when the user doesn't have access.
* `paywallHide` fires when access is granted and a session is active — payload includes a `sessionId`.
* `setContent()` pauses and resumes sessions correctly across content types.
* Switching between paid and free content updates the session state as expected.

***

For sandbox vs. live setup, see [setting up your environment](/guides/getting-started/set-up-environment). For how the two environments differ, see [Sandbox](/reference/generic/sandbox) in Reference.


# Debugging

When something does not behave as expected, start with SDK logging and a short checklist of common causes before escalating.

## Debug mode

Pass **`debug: true`** in the `tiun.init` configuration to turn on **console logging** from the SDK. That helps you see initialization, event flow, and errors during development.

```javascript
tiun.init({
  snippetId: 'YOUR_SNIPPET_ID',
  language: 'en', // 'en' | 'de' | 'fr'
  debug: true,
});
```

Disable or omit `debug` in your live builds if you prefer minimal client noise.

## Common issues

| Issue                        | Likely cause                                | What to try                                                                                                                                                  |
| ---------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Events not firing            | SDK not initialized                         | Call `tiun.init()` before subscribing to events.                                                                                                             |
| Checkout not opening         | Wrong or stale product ID                   | Live and sandbox have separate catalogs — compare the ID to the dashboard view for the same environment your SDK uses (`sandbox: true` → sandbox toggle on). |
| Connect overlay not opening  | SDK not initialized or wrong environment    | Ensure `tiun.init()` has been called and the snippet is configured for a time-based product.                                                                 |
| Sandbox payments not working | App and dashboard on different environments | Use sandbox snippet ID and `p-test-...` IDs with `sandbox: true`; confirm the dashboard **Sandbox** toggle is on while configuring test data.                |
| User state not updating      | Relying only on snapshot properties         | Listen for **`userChange`** and update UI from the event payload.                                                                                            |
| Session not restoring        | Different browser or cleared storage        | Sessions are per browser; clearing cookies or site data clears the session.                                                                                  |

## Event reference

For a full list of events and payloads, see [SDK events](/sdk/reference/events).

## Support

If logs and the table above do not resolve the issue, contact <support@tiun.app> with steps to reproduce and, when possible, snippet ID and environment (sandbox or live).


# Products

Everything you sell on tiun is modeled as a **product**. tiun has two product types, and each matches a different pricing and access pattern.

***

## Subscription

Subscriptions charge users on a **fixed schedule** — monthly, quarterly, or yearly. You configure the **interval**, the **recurring fee**, and optionally a **trial period** with a **trial amount** (use a trial amount of zero for a free trial).

Subscriptions fit SaaS, membership sites, and any product where access is continuous and priced like a membership.

<figure><img src="/files/adr4KK338e8oNP1Xczqq" alt="Create product form for subscription with interval, fee, and trial settings" width="340"><figcaption><p>Creating a subscription product.</p></figcaption></figure>

Subscription products integrate through `tiun.checkout({ productId })`. See [Checkout / How it works](/reference/checkout/how-it-works) and [Subscriptions](/reference/checkout/subscriptions) for the flow.

***

## Time-based billing

Time-based billing charges for **time spent** with your content rather than a flat membership. You define how often tiun charges (the **interval**), how much each interval costs (**interval fee**), and optionally a **monthly limit** that caps total spending.

Time-based products fit news, podcasts, video streaming, magazines — anywhere value tracks engagement.

<figure><img src="/files/y7CprLT3MA61sbEKv0X9" alt="Create product form for time-based billing with interval, fee, and monthly limit" width="340"><figcaption><p>Creating a time-based product.</p></figcaption></figure>

Time-based products integrate through `tiun.start()`. See [Time-based / How it works](/reference/time-based/how-it-works) for the flow.

***

## Product IDs

Every product gets a **unique product ID** like `p-live-pro`. The ID is stable and used everywhere you reference the product: `tiun.checkout()`, entitlement checks, analytics correlation.

Live and sandbox are **independent parallel catalogs** — you create products in each environment separately, and IDs never cross over. Product IDs are prefixed by environment: `p-live-...` for live and `p-test-...` for sandbox. See [Sandbox](/reference/generic/sandbox) for how the two environments work.

Find a product's ID on its detail or edit page in the [my.tiun.business dashboard](https://my.tiun.business/).

***

## Mix and match

A single tiun account can include **multiple product types** at once — for example a subscription for your core SaaS and a time-based offering for premium media. Each product is configured and called independently.


# Sandbox

tiun gives you **two fully independent environments** that run **in parallel**: **live** and **sandbox**.

Think of sandbox as a separate copy of your tiun setup — same SDK and checkout behavior, but its own catalog, snippet ID, domains, API keys, customers, and analytics. Nothing syncs or carries over automatically between the two.

**Live** is the default: real customers, real payments, your production domain. `localhost` is blocked in live.

**Sandbox** is for building and testing: simulated payments, test customers, and your development flow. `localhost` is enabled by default on any port in sandbox when you use sandbox settings (`sandbox: true` plus a sandbox snippet ID).

***

## What is independent in each environment

Everything you configure in tiun exists **twice** — once per environment. A sandbox product ID never works in live, and a live API key never works against the sandbox API.

| Resource                      | Live                                                          | Sandbox                                                                                |
| ----------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Authorized domains            | Production domains only (e.g. `yoursite.com`), no `localhost` | Dev / staging domains (e.g. `staging.yoursite.com`); `localhost` is enabled by default |
| Snippet ID                    | Live snippet                                                  | Sandbox snippet                                                                        |
| Products & pricing            | Your live catalog                                             | Separate test catalog                                                                  |
| Product IDs                   | `p-live-...`                                                  | `p-test-...`                                                                           |
| API keys                      | Live keys only                                                | Sandbox keys only (not shared)                                                         |
| Customers, sessions, payments | Real                                                          | Test / simulated                                                                       |
| Dashboard data                | Live analytics                                                | Sandbox analytics                                                                      |

Checkout, events, paywalls, and access control work the same way in both environments. Only the data, domains, credentials, and payment processing differ.

***

## Selecting an environment in your app

In the browser SDK, pass `sandbox: true` to target sandbox. Omit `sandbox` or set `sandbox: false` to target live (the default).

```javascript
tiun.init({
  snippetId: 'YOUR_SNIPPET_ID', // must match the environment you're targeting
  language: 'en',
  sandbox: true, // remove this line to target your live environment
});
```

Use the **snippet ID** from the environment you're building against. Sandbox and live each have their own snippet ID in the dashboard.

***

## Dashboard: two parallel setups

In the [my.tiun.business dashboard](https://my.tiun.business/), the **Sandbox** toggle in the sidebar switches which environment you're viewing and editing — products, customers, snippet ID, and API keys all belong to that view.

You set up **live** and **sandbox** separately:

* **Live** — register your production domain and create your real product catalog.
* **Sandbox** — the first time you open the sandbox view, register your test domain (prefer test/staging; use your live domain if needed as a fallback), then create test products with the same structure you plan to use in live.

Both environments can be fully configured at the same time and run in parallel.

For a step-by-step walkthrough, see [setting up your environment](/guides/getting-started/set-up-environment).

{% hint style="info" %}
**Your app and the dashboard should target the same environment while you're working.** If the dashboard is on live but your SDK has `sandbox: true` (or the other way around), snippet IDs, product IDs, and sessions won't line up with what you see in the UI.
{% endhint %}

***

## Recommended workflow

Most teams build in sandbox first, then stand up live when they're ready to ship:

1. **Set up sandbox** — domain, products, snippet ID, and (if you verify server-side) a sandbox API key.
2. **Integrate and test** — `sandbox: true`, your sandbox snippet ID, and `p-test-...` product IDs.
3. **Set up live** — production domain, recreate products and pricing, note your live snippet ID and `p-live-...` IDs, and create live API keys.
4. **Ship** — point production traffic at live: remove `sandbox: true` (or set it to `false`), swap in the live snippet ID and product IDs, and use live server credentials.

There is no one-click switch from sandbox to live. Going live means **replacing environment-specific values**; the integration patterns stay the same.

***

## Server-side verification

If your backend calls the tiun API, use the **matching base URL and API key** for the environment your app is using:

| Environment | API base URL                    |
| ----------- | ------------------------------- |
| Live        | `https://api.tiun.live`         |
| Sandbox     | `https://api-sandbox.tiun.live` |

API keys are **not shared** between environments. See the server verification guides for examples.

***

## Agent integration (MCP)

If you use the [tiun MCP server](/guides/agent-integration/agent-integration), the agent can read **both** environments from your account — sandbox and live providers are listed separately. With products set up in each environment, the agent can use the correct snippet ID and product IDs for sandbox development versus production, without you copying IDs by hand.


# How it works

tiun identifies users without passwords or OAuth. Identity is established through **email** plus **one-time passcode (OTP)** verification, all handled by the SDK.

***

## Two entry points

There are two ways a user can become authenticated:

1. **Through checkout** — authentication is built into the payment flow. The user enters their email (and completes OTP where needed) as part of checkout, so they're signed in the moment the purchase completes.
2. **Through dedicated login** — call `tiun.login()` to open the login overlay without starting a purchase. Useful for returning users who already have a subscription.

***

## Login flow

* **Returning user.** They enter their email. Because a phone number is already linked to the account, the OTP is sent to their **phone via SMS**.
* **New user.** They enter their **email and phone number**. The OTP is sent to the **phone** to verify and link it.
* **Fallback.** If the SMS doesn't arrive, the overlay offers to resend the code to the **email** instead.

After a successful flow, the user is available via `tiun.getUser()` — see [User object](/reference/authentication/user-object) for the shape and what each field means.

***

## Session persistence

tiun manages sessions for you. On the same browser, returning visitors are recognized automatically. When the page loads and a session is restored, the `userChange` event fires with `event: 'init'` so your UI can align without extra calls. See [Subscriptions](/reference/checkout/subscriptions) for the full lifecycle.

***

## Logout

Call `tiun.logout()` to clear the session on the current device. After logout, treat the user as unauthenticated until they sign in or complete checkout again.


# User object

When a user is signed in, tiun exposes them to your app as a **user object**. It's the canonical "who is this person and what can they use" record on the client.

***

## Reading the user

Call `tiun.getUser()` to read the current state. It returns an object shaped like:

```javascript
{ isAuthenticated, user }
```

When someone is signed in, `user` is the user object. When they're not, `user` is `null` and `isAuthenticated` is `false`.

The same payload is also available on every [`userChange`](/sdk/reference/events) event, so you rarely call `getUser()` directly — listening to `userChange` keeps your UI in sync. For one-off reads, the quick-access properties `tiun.user` and `tiun.isAuthenticated` are also available. See [SDK properties](/sdk/reference/properties).

***

## Shape

| Field           | Type       | Description                                                                                                    |
| --------------- | ---------- | -------------------------------------------------------------------------------------------------------------- |
| `userId`        | `string`   | Stable identifier for this user in tiun.                                                                       |
| `email`         | `string`   | Email on the account; used for receipts and identification.                                                    |
| `productAccess` | `string[]` | Product IDs the user currently has active access to. See [Product access](/reference/checkout/product-access). |

***

## Typical payloads

**Unauthenticated:**

```javascript
{ isAuthenticated: false, user: null }
```

**Authenticated, no purchases:**

```javascript
{
  isAuthenticated: true,
  user: {
    userId: 'u-abc123',
    email: 'user@example.com',
    productAccess: []
  }
}
```

**Authenticated with active subscriptions:**

```javascript
{
  isAuthenticated: true,
  user: {
    userId: 'u-abc123',
    email: 'user@example.com',
    productAccess: ['p-live-light', 'p-live-pro']
  }
}
```

***

## When the user object updates

The user object is populated and refreshed at several points:

* After a successful **checkout** (`event: 'checkout'`)
* After a successful **login** (`event: 'login'`)
* After **session restore** on page load (`event: 'init'`)
* After **logout**, where `user` becomes `null` (`event: 'logout'`)
* When **entitlements change** — for example a subscription expires, removing its product ID from `productAccess`

Listen to `userChange` so your UI reflects each of these moments. For UI gating patterns built on top of this state, see [Protecting content](/reference/authentication/protecting-content).


# Protecting content

Content gating is driven by two pieces of state on the user object — **`isAuthenticated`** and **`productAccess`** — kept in sync by the **`userChange`** event.

***

## Reading access

Combine `isAuthenticated` with `productAccess` to decide what to show:

* If `isAuthenticated` is `false`, show the public experience or your sign-in / pricing UI.
* If `isAuthenticated` is `true`, check whether the relevant product ID is in `productAccess`. If yes, unlock the gated experience; if not, offer an upgrade.

The shape of the user object is documented in [User object](/reference/authentication/user-object). The semantics of the `productAccess` array — what it contains, when it changes — are documented in [Product access](/reference/checkout/product-access).

***

## Reacting to changes

Subscribe to `userChange` and update your UI from the event payload. It fires on session init, after login, after checkout, after logout, and any time entitlements change — so your gates stay aligned without polling.

| Trigger                | Method                         | Resulting `userChange` event                                                      |
| ---------------------- | ------------------------------ | --------------------------------------------------------------------------------- |
| User subscribes        | `tiun.checkout({ productId })` | `event: 'checkout'`, user authenticated, `productAccess` reflects the new product |
| Returning visitor      | Automatic on page load         | `event: 'init'`, session restored                                                 |
| Existing user signs in | `tiun.login()`                 | `event: 'login'`, user authenticated                                              |
| User signs out         | `tiun.logout()`                | `event: 'logout'`, `user` is `null`                                               |

***

## Example

```javascript
tiun.on('userChange', (data) => {
  const { isAuthenticated, user } = data;

  if (!isAuthenticated || !user) {
    showPublicExperience();
    return;
  }

  const hasPremium = user.productAccess.includes('YOUR_PRODUCT_ID');

  if (hasPremium) {
    showPremiumContent();
  } else {
    showUpgradePrompt();
  }
});
```

For all event names and full payloads, see [SDK events](/sdk/reference/events).


# How it works

Call `tiun.checkout({ productId })` and tiun opens a **full-screen overlay** that runs the entire purchase and sign-in experience. You don't build forms, validate inputs, or host payment fields yourself.

<figure><img src="/files/7U2Et9hCtquJR23IC92c" alt="Checkout overlay with email input, payment methods, and plan details" width="340"><figcaption></figcaption></figure>

***

## What the overlay contains

For a subscription product, the overlay walks the user through:

* **Plan details** — product name, pricing, billing interval, and any trial info.
* **Email entry** — the user is identified by email as part of checkout.
* **Payment method selection** — credit/debit cards, PayPal, Apple Pay, Google Pay, PrePaid (tiun credits), and Twint, where enabled and regionally available.
* **Terms acceptance** and a clear pay action.

A **Login** affordance also appears at the bottom of the overlay so a visitor who already has a tiun account can sign in instead of entering email as a new customer.

***

## Identity verification

* If the email is **recognized** (returning user), tiun sends an OTP code to the **phone number** already linked to the account.
* If the email is **not recognized**, an additional overlay asks for the user's phone number, then sends an OTP to validate and link it before payment continues.

Checkout authenticates as part of the same flow — there's no separate sign-up step. See [Authentication / How it works](/reference/authentication/how-it-works) for the broader identity model.

***

## Managed by tiun

The overlay is owned and updated by tiun: layout, validation, compliance copy, and payment UI stay consistent without changes in your codebase. The flow also surfaces in-overlay help — including an explainer video link and "More info" affordances where appropriate — so common questions are handled inside checkout.

For what happens after the user completes checkout (the journey, session restore, logout), see [Subscriptions](/reference/checkout/subscriptions). For the resulting entitlements on the user, see [Product access](/reference/checkout/product-access).


# Product access

For subscriptions, the **`productAccess`** array on the user object is the source of truth for **what a user can use right now**. Your application gates features and content by checking whether a product ID appears in that array.

***

## What it contains

`productAccess` is an array of **product IDs** — strings like `p-live-pro` for live products or `p-test-abc123` for sandbox products. IDs always belong to the environment your SDK is using; live and sandbox catalogs are independent. Each ID corresponds to a subscription the user **currently has active access to**. The array can hold zero, one, or many IDs depending on what the user has purchased.

The full user object that contains this array is documented in [User object](/reference/authentication/user-object).

***

## When it's populated and updated

| When                                        | What happens                                                                                                               |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| After successful checkout                   | The purchased product ID is added to `productAccess`. `userChange` fires with `event: 'checkout'`.                         |
| On session restore                          | tiun rebuilds `productAccess` for the returning user. `userChange` fires with `event: 'init'`.                             |
| After login                                 | `productAccess` reflects whatever subscriptions are active for that user. `userChange` fires with `event: 'login'`.        |
| When a subscription expires or is cancelled | The product ID is **removed** from `productAccess` and `userChange` fires. The customer should lose access at that moment. |
| After logout                                | The user object becomes `null`; there is no `productAccess` to check.                                                      |

You don't manage renewals yourself — tiun handles billing cycles and keeps `productAccess` consistent with what the customer has paid for. See [Subscriptions](/reference/checkout/subscriptions) for the full lifecycle.

***

## Checking access

Read `productAccess` from the `userChange` payload (preferred) or via `tiun.user`:

```javascript
tiun.on('userChange', (data) => {
  if (!data.isAuthenticated) return;

  const hasPro = data.user.productAccess.includes('p-live-pro');
  const hasLight = data.user.productAccess.includes('p-live-light');

  if (hasPro) {
    renderProExperience();
  } else if (hasLight) {
    renderLightExperience();
  }
});
```

Replace the example IDs with the product IDs from your [my.tiun.business dashboard](https://my.tiun.business/). For the broader UI gating pattern that combines this with `isAuthenticated`, see [Protecting content](/reference/authentication/protecting-content).

{% hint style="info" %}
Prefer `userChange` over polling `tiun.user`. The event fires precisely when access changes, so your gates stay in sync without manual refresh logic.
{% endhint %}


# Subscriptions

A subscription in tiun ties an **identified user** to a **product** on a recurring schedule. This page covers the lifecycle from anonymous visitor to paying customer to expiration — and what your app should expect at each step.

***

## The journey

1. A **visitor** arrives unauthenticated — no tiun session yet.
2. They open **checkout** directly. The overlay collects email and payment; completing it **authenticates the user** and **grants access** to the product in one step.
3. Alternatively they may **log in first** with `tiun.login()`, then subscribe later via checkout when ready.
4. A **returning user** on the same browser gets **session restoration** automatically; `userChange` fires with `event: 'init'` so your UI can sync.
5. The user may **log out**; the session is cleared and they're anonymous again until they sign in or check out again.

{% hint style="info" %}
Checkout authenticates the user automatically. If a visitor goes directly to checkout without logging in first, they provide their email during the payment flow and are authenticated as part of completing it.
{% endhint %}

***

## Flow diagram

<figure><img src="/files/5rKvjiYOpFVG6faJPJsA" alt="Subscription flow: Visitor to Authenticated via login or checkout, with logout path"><figcaption></figcaption></figure>

The top branch shows **login first**, then optional checkout or logout. The bottom branch shows **checkout from cold start**, which both subscribes and establishes identity.

***

## Renewal and expiration

Subscription renewal is **automatic**. tiun handles the billing cycle, payment retries, and keeps subscription state consistent with what the customer has paid for — you don't run cron jobs or webhook listeners for renewals.

While a subscription is active, the corresponding product ID appears in the user's `productAccess`. When access ends — after a successful cancellation or a permanent payment failure — tiun removes the product ID and emits `userChange` so your UI immediately reflects the new state. See [Product access](/reference/checkout/product-access) for how that array is the source of truth and how to check it in code.

You do not need to implement any manual renewal management for the standard subscription lifecycle.

***

## Where to go next

* [Checkout / How it works](/reference/checkout/how-it-works) — what the overlay does and how identity is verified.
* [Product access](/reference/checkout/product-access) — how `productAccess` is populated and how to gate features.
* [Authentication / How it works](/reference/authentication/how-it-works) — the broader login and session model.
* For an end-to-end walkthrough, see [monetizing with subscriptions](/guides/subscriptions/monetize-with-subscriptions).


# How it works

Time-based billing charges customers for **actual time** they spend with billable content rather than a flat membership fee. It fits streaming, reading, and listening experiences where value scales with engagement.

***

## The core flow

The mental model is four steps:

1. The user **connects a payment method** through `tiun.start()` on your paywall.
2. tiun starts a **billing session** and begins **metering time** against your configured interval and price.
3. The user is **billed for time spent** according to the product's interval fee and optional monthly cap.
4. The session **ends** when the user leaves, your app stops billable content, or payment fails — access returns to the locked state.

You configure the product itself in the dashboard (interval, fee, monthly limit — see [Products](/reference)). The SDK and events connect that configuration to your app's UI and routing.

***

## Where the details live

The rest of the time-based reference is split across three pages, each owning one piece of the model:

* [Sessions](/reference/time-based/sessions) — the session lifecycle (Locked / Active / Ended), what tiun tracks behind the scenes, and the `sessionId` you can verify server-side.
* [Access](/reference/time-based/access) — the `paywallShow` and `paywallHide` events that tell your app when the user has access.
* [Protecting content](/reference/time-based/protecting-content) — how to gate the UI with paywall events and how to use `tiun.setContent()` to control metering as the user navigates.

For an end-to-end walkthrough, see [charging for time-based sessions](/guides/time-based-billing/charge-for-time-based-sessions).


# Sessions

Time-based billing is built around **billing sessions**, not identified users. When the customer connects a payment method, tiun opens a session and starts metering against your product's interval and fee. The session — not an email or `userId` — is the unit of "is this person currently allowed to use paid content?"

***

## What tiun captures

When you call `tiun.start()`, the customer connects a payment method — credit/debit cards, PayPal, Apple Pay, Google Pay, PrePaid (tiun credits), or Twint — and tiun creates a billing session tied to that connection.

Behind the scenes, tiun extracts and stores details about the customer and their session. You can review this data under **User Management** in the [my.tiun.business dashboard](https://my.tiun.business/), but it is **not exposed to the SDK or your snippet**. From your app's perspective, sessions are anonymous.

That's a deliberate design choice: in time-based experiences (news, podcasts, streaming), the integration only needs to know whether the customer currently has access — not who they are. Identity-bound flows like login/logout and `productAccess` do not apply here.

***

## Session lifecycle

A session moves through three states:

<figure><img src="/files/WgWCwm3tDN3RubD4bGkM" alt="Session lifecycle: Locked, Active, Ended/Invalid"><figcaption></figcaption></figure>

| State               | Meaning                                                                                                                              |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Locked**          | No active billing session, or the session is not granting access. The user should see a paywall.                                     |
| **Active**          | Billing session is in progress; eligible content is billable and time accrues per your product rules.                                |
| **Ended / Invalid** | The session was closed or payment failed. No further billing for this visit; the user returns to Locked and may need to start again. |

Transitions are signaled to your app via [paywall events](/reference/time-based/access): `paywallHide` corresponds to entering Active, `paywallShow` corresponds to leaving it.

***

## Session ID

When `paywallHide` fires (the session is Active and the user has access), the payload includes a **`sessionId`**. The session ID is what your backend uses to **verify the session server-side** before serving premium content — protecting your APIs against clients that fake or spoof access on the front end.

For the verification flow including the API endpoint and example backend code, see [server-side session verification](/guides/time-based-billing/verify-server-side).


# Access

Time-based access is **binary** at the session level — at any moment a user either has access or they don't. tiun signals this to your app through two events: **`paywallShow`** and **`paywallHide`**.

***

## Paywall events

| Event             | Meaning                                                                                                                                                                                              |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`paywallShow`** | The user should **not** see paid content. Either there's no active session, the session ended, payment failed, or your integration has explicitly paused access. Render the paywall or locked state. |
| **`paywallHide`** | The user **has** access. The billing session is Active. Reveal premium UI.                                                                                                                           |

These map directly to session state — see [Sessions](/reference/time-based/sessions) for the Locked / Active / Ended lifecycle. `paywallHide` corresponds to entering Active; `paywallShow` corresponds to leaving it.

***

## What the `paywallHide` payload includes

When access is granted, the `paywallHide` event payload includes a **`sessionId`**. Use it when you need to verify the session on your backend before serving premium content — see [server-side session verification](/guides/time-based-billing/verify-server-side).

```javascript
tiun.on('paywallHide', (data) => {
  console.log('Session is active:', data.sessionId);
  showPremiumContent();
});

tiun.on('paywallShow', () => {
  hidePremiumContent();
  showPaywall();
});
```

***

## Where to go next

* For UI gating patterns and metering with `setContent()`, see [Protecting content](/reference/time-based/protecting-content).
* For the full event payload spec, see [SDK events](/sdk/reference/events).


# Protecting content

Protecting time-based content has two parts: **gating the UI** with paywall events, and **telling tiun what the user is viewing** so metering and billing stay accurate.

***

## Gating the UI

Wire your screens to the [paywall events](/reference/time-based/access): show the premium UI when `paywallHide` fires, replace it with a paywall when `paywallShow` fires.

```javascript
tiun.on('paywallShow', () => {
  hidePremiumContent();
  showPaywall();
});

tiun.on('paywallHide', () => {
  hidePaywall();
  showPremiumContent();
});
```

Start your app with the paywall visible — the default state is locked until the user connects a payment method via `tiun.start()`.

***

## Telling tiun what the user is viewing

The SDK needs to know **what** the user is on so it can meter correctly. Call **`tiun.setContent()`** on every route or section change that affects what counts as billable.

You pass a **content type** describing how tiun should treat the current view:

| Type         | When to use                                                                                      |
| ------------ | ------------------------------------------------------------------------------------------------ |
| `'active'`   | Paid, billable content. Time accrues per your product rules.                                     |
| `'inactive'` | Free or non-billable content. Metering pauses and the user is not billed.                        |
| `'paused'`   | Temporarily not accruing — for example an interstitial, an ad break, or a deliberate user pause. |

Keeping `setContent()` accurate across navigation prevents incorrect charges and keeps paywall state trustworthy.

```javascript
function onRouteChange(path) {
  tiun.setContent({
    type: isPaidContent(path) ? 'active' : 'inactive',
    contentId: path,
  });
}
```

***

## Media types

How tiun interprets engagement depends on the **media type** of the content:

| Media type           | Behavior                                                                                                                       |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `'text'` *(default)* | Tied to **visibility** and **scroll** — reading in view counts as engagement; leaving the viewport can pause or stop metering. |
| `'audio'`            | Stays **active** during background or locked-screen playback — suited to podcasts and music.                                   |
| `'video'`            | Follows **play and pause** — billing tracks actual playback rather than page presence.                                         |

Pick the media type that matches the asset so sessions reflect real usage:

```javascript
tiun.setContent({
  type: 'active',
  contentId: 'episode-42',
  mediaType: 'audio',
});
```

***

## Content ID

The optional `contentId` you pass to `setContent()` is a stable identifier — an article slug, an episode ID, a video asset key. It surfaces in dashboard analytics so you can tie charges and sessions to specific pieces of content.

***

## Server-side verification

The paywall events and `setContent()` calls cover the client side. If your backend serves the premium payload itself (an article body, a stream URL, an API response), you should also **verify the session on the server** before responding — clients can lie about whether `paywallHide` fired.

See [server-side session verification](/guides/time-based-billing/verify-server-side) for the flow using the `sessionId` from `paywallHide`.


# Overview

The tiun SDK is a lightweight JavaScript package that lets you integrate payments, subscriptions, and access control into any website or app.

{% hint style="info" %}
**NPM:** [@tiun/sdk](https://www.npmjs.com/package/@tiun/sdk)
{% endhint %}

## When to use the SDK

Use the tiun SDK when you need to:

* **Accept payments** — Open a checkout flow and let users purchase products or subscriptions directly from your site.
* **Control access** — Gate content behind a paywall, whether time-based or subscription-based.
* **Manage user sessions** — Let users log in, log out, and persist their session across visits.

The SDK runs entirely on the client side. Install it via npm, call `init()` once, and you're ready to go — no server-side code or manual script tags required.

{% hint style="info" %}
Ready to get started? Head to [Installation](/sdk/getting-started/installation) to add the SDK to your project.
{% endhint %}


# Installation

## Setup

Install the SDK from npm using your preferred package manager:

{% tabs %}
{% tab title="npm" %}

```bash
npm install @tiun/sdk
```

{% endtab %}

{% tab title="pnpm" %}

```bash
pnpm add @tiun/sdk
```

{% endtab %}

{% tab title="yarn" %}

```bash
yarn add @tiun/sdk
```

{% endtab %}
{% endtabs %}

## Configuration

After installing, import the SDK and call `init()` once at app startup:

```javascript
import { tiun } from '@tiun/sdk';

tiun.init({
  snippetId: 'YOUR_SNIPPET_ID',
  language: 'en', // 'en' | 'de' | 'fr'
});
```

You'll need your **Snippet ID** from the tiun dashboard. That's the only required option — see [Initialization](/sdk/getting-started/initialization) for the full list of configuration options.

## Use without a bundler

If you don't have a build step, load the SDK as an ES module from [unpkg](https://unpkg.com/@tiun/sdk/):

```html
<script type="module">
  import { tiun } from 'https://unpkg.com/@tiun/sdk/tiun.js';

  tiun.init({
    snippetId: 'YOUR_SNIPPET_ID',
    language: 'en', // 'en' | 'de' | 'fr'
  });
</script>
```

See the [Vanilla JS examples](/sdk/examples/vanilla-js) for the full set of patterns.


# Initialization

## Creating the client

Import the SDK and call `init()` once at app startup. The SDK automatically loads the tiun snippet from the backend — no script tags or extra HTML required.

```javascript
import { tiun } from '@tiun/sdk';

tiun.init({
  snippetId: 'YOUR_SNIPPET_ID',
  language: 'en', // 'en' | 'de' | 'fr'
});
```

{% hint style="info" %}
**Set `language` to match your site.** Supported values are `'en'`, `'de'`, and `'fr'`. If omitted or invalid, the snippet UI falls back to `'en'`.
{% endhint %}

## Configuration options

| Option      | Type                     | Default    | Description                                                                                                                                                                    |
| ----------- | ------------------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `snippetId` | `string`                 | —          | **Required.** Your snippet ID from the dashboard.                                                                                                                              |
| `language`  | `'en' \| 'de' \| 'fr'`   | `'en'`     | Language for the snippet UI. The value is case-insensitive (`'En'`, `'DE'`, `' fr '` all work). Unsupported values trigger a one-time console warning and fall back to `'en'`. |
| `tone`      | `'formal' \| 'informal'` | `'formal'` | Tone of the snippet copy.                                                                                                                                                      |
| `debug`     | `boolean`                | `false`    | Enable debug logging to the console.                                                                                                                                           |
| `sandbox`   | `boolean`                | `false`    | Target the sandbox environment (`true`) or live (`false` / omitted). Selects the API host automatically; use the snippet ID from the matching dashboard view.                  |

You can also pass event callbacks directly in the config. See [Events](/sdk/reference/events) for the full list.

```javascript
tiun.init({
  snippetId: 'YOUR_SNIPPET_ID',
  language: 'en',
  onReady: () => console.log('ready')
});
```

## Environment setup

tiun has two **independent parallel environments** — live and sandbox — each with its own snippet ID, product IDs, and catalog. Set `sandbox: true` while developing against sandbox; omit it or set `false` for live (the default).

The SDK routes to the correct API host for you. Use the snippet ID from the environment you're targeting (sandbox vs live in the dashboard).

```javascript
tiun.init({
  snippetId: 'YOUR_SANDBOX_SNIPPET_ID',
  language: 'en',
  sandbox: true
});
```

See [Sandbox](/reference/generic/sandbox) for how the two environments relate and [set up your environment](/guides/getting-started/set-up-environment) for dashboard setup.

## Full example

```javascript
import { tiun } from '@tiun/sdk';

tiun.init({
  snippetId: 'YOUR_SNIPPET_ID',
  language: 'en',
  sandbox: true,
  onReady: () => {
    console.log('tiun is ready');
  },
  onError: (error) => {
    console.error('tiun error:', error.code, error.message);
  }
});
```

## Lifecycle and `destroy()`

`tiun.init()` is **idempotent**: calling it again on an initialized instance merges config rather than re-initializing. Calling `tiun.destroy()` clears listeners, runtime config, and cached user state.

The question is *can the SDK subtree remount in this host?* If it can, you want a `destroy()` on teardown so listeners and state don't accumulate. If it can't, `destroy()` is unnecessary.

| Situation                                                      | Call `destroy()`?            | Why                                                  |
| -------------------------------------------------------------- | ---------------------------- | ---------------------------------------------------- |
| SPA root (Vue `App.vue`, single React root, Svelte/Solid root) | No                           | The root only unmounts when the document is gone     |
| React under StrictMode                                         | Yes (in `useEffect` cleanup) | StrictMode double-invokes effects in dev             |
| Component mid-tree that mounts/unmounts repeatedly             | Yes                          | Stale listeners would accumulate otherwise           |
| Micro-frontend inside a long-lived host                        | Yes                          | Host remounts the subtree without reloading the page |
| Astro island, Qwik resumability                                | Yes                          | Each island is a remount-capable subtree             |
| Server-rendered page (vanilla HTML, classic PHP)               | No                           | Page reload tears down everything                    |
| Mobile WebView swapping page URL                               | No                           | The whole document is replaced                       |
| Mobile WebView swapping DOM content without reload             | Yes                          | The same JS context survives and accumulates state   |
| Tests                                                          | Yes (per test)               | Each test wants a clean instance                     |

For framework-specific wiring, see the [SDK examples](/sdk/examples/vue).


# Core Methods

## Authentication

| Method                            | Description                                           |
| --------------------------------- | ----------------------------------------------------- |
| `tiun.login()`                    | Open the login overlay.                               |
| `tiun.logout()`                   | Clear the user session.                               |
| `tiun.getUser()`                  | Get the current user state.                           |
| `tiun.getUserVerificationToken()` | Get a signed token for server-side user verification. |

***

## Checkout (Subscriptions)

| Method                         | Description                                        |
| ------------------------------ | -------------------------------------------------- |
| `tiun.checkout({ productId })` | Open the checkout flow for a subscription product. |

***

## Time-based

| Method         | Description                                      |
| -------------- | ------------------------------------------------ |
| `tiun.start()` | Open the connect overlay for time-based billing. |

***

## Content & Access

| Method                     | Description                         |
| -------------------------- | ----------------------------------- |
| `tiun.setContent(options)` | Update the current content context. |

***

## Lifecycle

| Method                       | Description                                                |
| ---------------------------- | ---------------------------------------------------------- |
| `tiun.init(config)`          | Initialize the SDK. Call once at startup.                  |
| `tiun.destroy()`             | Destroy the SDK instance and clean up all listeners.       |
| `tiun.waitForReady()`        | Returns a Promise that resolves when the snippet is ready. |
| `tiun.on(event, callback)`   | Subscribe to an event. Returns an unsubscribe function.    |
| `tiun.once(event, callback)` | Subscribe to an event once.                                |


# Properties

Properties give you a quick read of the current SDK state. Use `userChange` events to keep your UI in sync; use these properties when you need a one-off check of what tiun knows right now.

```javascript
// Quick read of current state
console.log(tiun.isAuthenticated);
console.log(tiun.user);

// Keep your UI in sync with events
tiun.on('userChange', (data) => {
  myState.isAuthenticated = data.isAuthenticated;
  myState.user = data.user;
});
```

***

## Reference

| Property               | Type               | Description                                    |
| ---------------------- | ------------------ | ---------------------------------------------- |
| `tiun.version`         | `string`           | The SDK version.                               |
| `tiun.isInitialized`   | `boolean`          | Whether `init()` has been called.              |
| `tiun.isReady`         | `boolean`          | Whether the snippet is ready to use.           |
| `tiun.isAuthenticated` | `boolean`          | Whether the user has a valid session.          |
| `tiun.user`            | `UserInfo \| null` | Current user info, or `null` if not logged in. |


# Events

## Usage

Subscribe to events with `tiun.on()` to react when something happens in the SDK.

```javascript
tiun.on('ready', () => {
  console.log('tiun is ready');
});

tiun.on('userChange', (data) => {
  console.log('user state changed:', data.event);
});
```

***

## Event reference

| Event         | Payload                                                               | Description                                         |
| ------------- | --------------------------------------------------------------------- | --------------------------------------------------- |
| `ready`       | —                                                                     | Snippet has initialized and is ready to use.        |
| `userChange`  | `{ event: string, isAuthenticated: boolean, user: UserInfo \| null }` | User state changed (init, login, checkout, logout). |
| `login`       | `{ user: UserInfo }`                                                  | User has logged in.                                 |
| `logout`      | —                                                                     | User has logged out.                                |
| `paywallShow` | `{ isConnected: boolean }`                                            | Paywall should be shown (user doesn't have access). |
| `paywallHide` | `{ sessionId: string, isConnected: boolean }`                         | User has access, hide the paywall.                  |
| `error`       | `{ code: string, message: string, details?: any }`                    | An error occurred.                                  |

{% hint style="info" %}
`userChange`, `login`, and `logout` are specific to subscriptions. `paywallShow` and `paywallHide` are specific to time-based billing.
{% endhint %}

### `ready`

Fires once when the snippet has fully loaded and is ready to use. Use this to delay any SDK calls that depend on the snippet being available.

```javascript
tiun.on('ready', () => {
  // Safe to call checkout, login, start, etc.
});
```

### `userChange`

Fires on every user state change — login, logout, checkout, or session restore. This is the main event for keeping your app in sync with the user's state.

```javascript
tiun.on('userChange', (data) => {
  // data.event — what triggered the change: 'init', 'login', 'checkout', 'logout', 'update'
  // data.isAuthenticated — whether the user has a valid session
  // data.user — { userId, email, productAccess } or null
});
```

{% hint style="info" %}
**`userChange` fires once with `event: 'init'` after the snippet is ready.** As long as your handler is registered **before** (or synchronously after) `tiun.init`, you'll receive the initial state automatically — no need to manually read `tiun.user` from a `waitForReady()` callback.

```javascript
// Correct — initial state arrives via the listener
tiun.on('userChange', syncStateFromTiun);
tiun.init({ snippetId });
```

{% endhint %}

### `login`

Fires when the user has successfully logged in.

```javascript
tiun.on('login', (data) => {
  // data.user — the authenticated user's info
});
```

### `logout`

Fires when the user session has been cleared.

```javascript
tiun.on('logout', () => {
  // User is logged out
});
```

### `paywallShow`

Fires when the user does not have access and the paywall should be displayed. Use this to show your paywall UI or gate content.

```javascript
tiun.on('paywallShow', (data) => {
  // data.isConnected — whether the user has a payment method connected
  showPaywall();
});
```

### `paywallHide`

Fires when the user has access and the paywall should be hidden. Use this to reveal content.

```javascript
tiun.on('paywallHide', (data) => {
  // data.sessionId — the active session ID
  // data.isConnected — whether the user has a payment method connected
  hidePaywall();
});
```

### `error`

Fires when an error occurs in the SDK.

```javascript
tiun.on('error', (err) => {
  // err.code — error code
  // err.message — human-readable message
  // err.details — optional additional info
});
```

{% hint style="info" %}
**`err.code` is a string, but there is no published enum of values.** Codes can change between SDK versions — display `err.message` to users and log `err.code` for support, rather than branching application logic off specific codes.
{% endhint %}

***

## Unsubscribing

`tiun.on()` returns an unsubscribe function. Call it if you need to remove a specific listener — for example, inside a component that mounts and unmounts.

```javascript
const unsubscribe = tiun.on('userChange', handler);

// Later, to stop listening:
unsubscribe();
```

For most integrations where listeners are set up once at app startup, you don't need to unsubscribe manually — `tiun.destroy()` cleans up everything.

***

## Listening once

Use `tiun.once()` to listen for an event a single time. The listener is automatically removed after it fires.

```javascript
tiun.once('ready', () => {
  console.log('First ready event');
});
```


# Vue

Examples of common tiun SDK patterns in Vue 3. Each section is standalone — use what applies to your integration.

For Nuxt (SSR), see the [Nuxt example](/sdk/examples/nuxt).

***

## Initialize

Initialize the SDK in `onMounted` of your root component (typically `App.vue`).

```vue
<script setup>
import { tiun } from '@tiun/sdk';
import { onMounted } from 'vue';

onMounted(() => {
  tiun.init({
    snippetId: 'YOUR_SNIPPET_ID',
    language: 'en', // 'en' | 'de' | 'fr'
  });
});
</script>

<template>
  <!-- your app -->
</template>
```

{% hint style="info" %}
You don't need to call `tiun.destroy()` from the root — the root only unmounts when the page is gone. See [Lifecycle and `destroy()`](/sdk/getting-started/initialization#lifecycle-and-destroy) for when teardown is needed.
{% endhint %}

***

## Checkout

Trigger checkout on a button click. Pass the product ID from your dashboard.

```vue
<script setup>
import { tiun } from '@tiun/sdk';
</script>

<template>
  <div>
    <button @click="tiun.checkout({ productId: 'p-live-light' })">
      Light — EUR 19/mo
    </button>
    <button @click="tiun.checkout({ productId: 'p-live-pro' })">
      Pro — EUR 199/mo
    </button>
  </div>
</template>
```

***

## Listen for user changes

Track authentication and access state with `userChange`. Store it in reactive refs so your components update automatically.

```vue
<script setup>
import { tiun } from '@tiun/sdk';
import { ref, onMounted } from 'vue';

const isAuthenticated = ref(false);
const user = ref(null);

onMounted(() => {
  // Register before init so the initial userChange ('init') fires into your handler.
  tiun.on('userChange', (data) => {
    isAuthenticated.value = data.isAuthenticated;
    user.value = data.user;
  });

  tiun.init({ snippetId: 'YOUR_SNIPPET_ID', language: 'en' });
});
</script>

<template>
  <p v-if="isAuthenticated">Logged in as {{ user.email }}</p>
  <p v-else>Not logged in</p>
</template>
```

***

## Gate content

Show or hide content based on `productAccess`.

```vue
<script setup>
import { tiun } from '@tiun/sdk';
import { ref, computed, onMounted } from 'vue';

const isAuthenticated = ref(false);
const user = ref(null);

onMounted(() => {
  tiun.on('userChange', (data) => {
    isAuthenticated.value = data.isAuthenticated;
    user.value = data.user;
  });
});

const hasPro = computed(() =>
  user.value?.productAccess?.includes('p-live-pro')
);
</script>

<template>
  <div v-if="!isAuthenticated">
    <p>Please log in or subscribe.</p>
  </div>

  <div v-else-if="hasPro">
    <!-- Pro content -->
  </div>

  <div v-else>
    <button @click="tiun.checkout({ productId: 'p-live-pro' })">
      Upgrade to Pro
    </button>
  </div>
</template>
```

***

## Login and logout

Add authentication buttons to your navigation.

```vue
<script setup>
import { tiun } from '@tiun/sdk';
import { ref, onMounted } from 'vue';

const isAuthenticated = ref(false);
const user = ref(null);

onMounted(() => {
  tiun.on('userChange', (data) => {
    isAuthenticated.value = data.isAuthenticated;
    user.value = data.user;
  });
});
</script>

<template>
  <nav v-if="isAuthenticated">
    <span>{{ user.email }}</span>
    <button @click="tiun.logout()">Log out</button>
  </nav>

  <nav v-else>
    <button @click="tiun.login()">Log in</button>
  </nav>
</template>
```

***

## Paywall events (time-based)

For time-based billing, use `paywallShow` and `paywallHide` to control access.

```vue
<script setup>
import { tiun } from '@tiun/sdk';
import { ref, onMounted } from 'vue';

const hasAccess = ref(false);

onMounted(() => {
  tiun.on('paywallHide', () => { hasAccess.value = true; });
  tiun.on('paywallShow', () => { hasAccess.value = false; });
});
</script>

<template>
  <div v-if="!hasAccess">
    <h2>Premium content</h2>
    <button @click="tiun.start()">
      Get access
    </button>
  </div>

  <slot v-else />
</template>
```

***

## Content management (time-based)

Call `setContent()` on route changes to keep billing accurate.

```vue
<script setup>
import { tiun } from '@tiun/sdk';
import { watch } from 'vue';
import { useRoute } from 'vue-router';

const route = useRoute();

watch(
  () => route.path,
  (path) => {
    const isPaid = isPaidContent(path);
    tiun.setContent({
      type: isPaid ? 'active' : 'inactive',
      contentId: path
    });
  },
  { immediate: true }
);
</script>
```


# React

Examples of common tiun SDK patterns in React. Each section is standalone — use what applies to your integration.

***

## Initialize

Initialize the SDK inside a top-level `useEffect` with an empty dependency array. Return `tiun.destroy()` as the cleanup so dev-mode StrictMode (which double-invokes effects) tears down cleanly.

```tsx
import { tiun } from '@tiun/sdk';
import { useEffect } from 'react';

function App() {
  useEffect(() => {
    tiun.init({
      snippetId: 'YOUR_SNIPPET_ID',
      language: 'en', // 'en' | 'de' | 'fr'
    });
    return () => tiun.destroy();
  }, []);

  return <>{/* your app */}</>;
}
```

See [Lifecycle and `destroy()`](/sdk/getting-started/initialization#lifecycle-and-destroy) for when teardown is and isn't needed in other hosts.

***

## Checkout

Trigger checkout on a button click. Pass the product ID from your dashboard.

```tsx
import { tiun } from '@tiun/sdk';

function PricingPage() {
  return (
    <div>
      <button onClick={() => tiun.checkout({ productId: 'p-live-light' })}>
        Light — EUR 19/mo
      </button>
      <button onClick={() => tiun.checkout({ productId: 'p-live-pro' })}>
        Pro — EUR 199/mo
      </button>
    </div>
  );
}
```

***

## Listen for user changes

Track authentication and access state with `userChange`. Store it in React state so your components re-render when the user changes.

```tsx
import { tiun } from '@tiun/sdk';
import { useEffect, useState } from 'react';

function App() {
  const [user, setUser] = useState(tiun.getUser());

  useEffect(() => {
    tiun.init({ snippetId: 'YOUR_SNIPPET_ID', language: 'en' });

    tiun.on('userChange', (data) => {
      setUser({ isAuthenticated: data.isAuthenticated, user: data.user });
    });

    return () => tiun.destroy();
  }, []);

  return <>{/* use user.isAuthenticated and user.user */}</>;
}
```

***

## Gate content

Show or hide content based on `productAccess`.

```tsx
function PremiumSection({ user }) {
  if (!user.isAuthenticated) {
    return <p>Please log in or subscribe.</p>;
  }

  const hasPro = user.user?.productAccess.includes('p-live-pro');

  if (hasPro) {
    return <div>{/* Pro content */}</div>;
  }

  return (
    <button onClick={() => tiun.checkout({ productId: 'p-live-pro' })}>
      Upgrade to Pro
    </button>
  );
}
```

***

## Login and logout

Add authentication buttons to your navigation.

```tsx
import { tiun } from '@tiun/sdk';

function Nav({ user }) {
  if (user.isAuthenticated) {
    return (
      <nav>
        <span>{user.user.email}</span>
        <button onClick={() => tiun.logout()}>Log out</button>
      </nav>
    );
  }

  return (
    <nav>
      <button onClick={() => tiun.login()}>Log in</button>
    </nav>
  );
}
```

***

## Paywall events (time-based)

For time-based billing, use `paywallShow` and `paywallHide` to control access.

```tsx
import { tiun } from '@tiun/sdk';
import { useEffect, useState } from 'react';

function PaywallGate({ children }) {
  const [hasAccess, setHasAccess] = useState(false);

  useEffect(() => {
    tiun.on('paywallHide', () => setHasAccess(true));
    tiun.on('paywallShow', () => setHasAccess(false));
  }, []);

  if (!hasAccess) {
    return (
      <div>
        <h2>Premium content</h2>
        <button onClick={() => tiun.start()}>
          Get access
        </button>
      </div>
    );
  }

  return <>{children}</>;
}
```

***

## Content management (time-based)

Call `setContent()` on route changes to keep billing accurate.

```tsx
import { tiun } from '@tiun/sdk';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function ContentTracker() {
  const location = useLocation();

  useEffect(() => {
    const isPaid = isPaidContent(location.pathname);
    tiun.setContent({
      type: isPaid ? 'active' : 'inactive',
      contentId: location.pathname
    });
  }, [location.pathname]);

  return null;
}
```


# Vanilla JS

The tiun SDK can be loaded directly in a browser using a module script tag — no build tools needed. Import from unpkg (or any CDN that serves the package).

***

## Initialize

Load the SDK as an ES module and call `init()`.

```html
<script type="module">
  import { tiun } from 'https://unpkg.com/@tiun/sdk/tiun.js';

  tiun.init({
    snippetId: 'YOUR_SNIPPET_ID',
    language: 'en', // 'en' | 'de' | 'fr'
  });
</script>
```

***

## Checkout

Attach checkout to a button click.

```html
<button id="btn-checkout">Subscribe</button>

<script type="module">
  import { tiun } from 'https://unpkg.com/@tiun/sdk/tiun.js';

  tiun.init({ snippetId: 'YOUR_SNIPPET_ID', language: 'en' });

  document.getElementById('btn-checkout').onclick = () => {
    tiun.checkout({ productId: 'YOUR_PRODUCT_ID' });
  };
</script>
```

***

## Listen for user changes

Update the page when authentication or access changes.

```html
<div id="status">Not logged in</div>

<script type="module">
  import { tiun } from 'https://unpkg.com/@tiun/sdk/tiun.js';

  tiun.init({ snippetId: 'YOUR_SNIPPET_ID', language: 'en' });

  tiun.on('userChange', (data) => {
    const el = document.getElementById('status');

    if (data.isAuthenticated) {
      el.textContent = 'Logged in as ' + data.user.email;
    } else {
      el.textContent = 'Not logged in';
    }
  });
</script>
```

***

## Login and logout

```html
<button id="btn-login">Log in</button>
<button id="btn-logout">Log out</button>

<script type="module">
  import { tiun } from 'https://unpkg.com/@tiun/sdk/tiun.js';

  tiun.init({ snippetId: 'YOUR_SNIPPET_ID', language: 'en' });

  document.getElementById('btn-login').onclick = () => {
    tiun.login();
  };

  document.getElementById('btn-logout').onclick = () => {
    tiun.logout();
  };
</script>
```

***

## Gate content

Show or hide elements based on `productAccess`.

```html
<div id="premium" style="display:none">Premium content here</div>
<div id="upgrade">
  <p>Subscribe to access premium content.</p>
  <button id="btn-upgrade">Upgrade</button>
</div>

<script type="module">
  import { tiun } from 'https://unpkg.com/@tiun/sdk/tiun.js';

  tiun.init({ snippetId: 'YOUR_SNIPPET_ID', language: 'en' });

  document.getElementById('btn-upgrade').onclick = () => {
    tiun.checkout({ productId: 'YOUR_PRODUCT_ID' });
  };

  tiun.on('userChange', (data) => {
    const hasAccess = data.isAuthenticated &&
      data.user.productAccess.includes('YOUR_PRODUCT_ID');

    document.getElementById('premium').style.display = hasAccess ? 'block' : 'none';
    document.getElementById('upgrade').style.display = hasAccess ? 'none' : 'block';
  });
</script>
```

***

## Read properties

Check SDK state at any point using properties.

```html
<button id="btn-status">Show status</button>
<pre id="output"></pre>

<script type="module">
  import { tiun } from 'https://unpkg.com/@tiun/sdk/tiun.js';

  tiun.init({ snippetId: 'YOUR_SNIPPET_ID', language: 'en' });

  document.getElementById('btn-status').onclick = () => {
    const output = document.getElementById('output');
    output.textContent = JSON.stringify({
      version: tiun.version,
      isReady: tiun.isReady,
      isAuthenticated: tiun.isAuthenticated,
      user: tiun.user
    }, null, 2);
  };
</script>
```

***

## Paywall events (time-based)

For time-based billing, use paywall events to show or hide content.

```html
<div id="paywall">
  <h2>Premium content</h2>
  <button id="btn-access">Get access</button>
</div>
<div id="content" style="display:none">Your premium content</div>

<script type="module">
  import { tiun } from 'https://unpkg.com/@tiun/sdk/tiun.js';

  tiun.init({ snippetId: 'YOUR_SNIPPET_ID', language: 'en' });

  document.getElementById('btn-access').onclick = () => {
    tiun.start();
  };

  tiun.on('paywallHide', () => {
    document.getElementById('paywall').style.display = 'none';
    document.getElementById('content').style.display = 'block';
  });

  tiun.on('paywallShow', () => {
    document.getElementById('paywall').style.display = 'block';
    document.getElementById('content').style.display = 'none';
  });
</script>
```


# Nuxt (SSR)

Since Nuxt renders pages on the server first, the tiun SDK must only initialize on the client. The recommended approach is to set up everything in your `app.vue` using `onMounted`.

For plain Vue 3 (no SSR), see the [Vue example](/sdk/examples/vue).

***

## Initialize

Initialize tiun inside `onMounted` in your `app.vue`. The SDK is idempotent — calling `init` again on an initialized instance merges config rather than re-initializing — so no guard is needed.

```vue
<!-- app.vue -->
<script setup>
import { tiun } from '@tiun/sdk';
import { onMounted } from 'vue';

onMounted(() => {
  tiun.init({
    snippetId: 'YOUR_SNIPPET_ID',
    language: 'en', // 'en' | 'de' | 'fr'
  });
});
</script>

<template>
  <NuxtPage />
</template>
```

{% hint style="info" %}
You don't need to call `tiun.destroy()` from `app.vue` — the root only unmounts when the page is gone. See [Lifecycle and `destroy()`](/sdk/getting-started/initialization#lifecycle-and-destroy) for the full matrix.
{% endhint %}

***

## Checkout

Trigger checkout from any component. The SDK is already initialized in `app.vue`, so you can call methods directly.

```vue
<script setup>
import { tiun } from '@tiun/sdk';
</script>

<template>
  <div>
    <button @click="tiun.checkout({ productId: 'p-live-light' })">
      Light — EUR 19/mo
    </button>
    <button @click="tiun.checkout({ productId: 'p-live-pro' })">
      Pro — EUR 199/mo
    </button>
  </div>
</template>
```

***

## Shared state with `useState`

Use Nuxt's `useState` to create reactive state that any component can read. This is SSR-safe and works across the app without prop drilling.

```vue
<!-- app.vue -->
<script setup>
import { tiun } from '@tiun/sdk';
import { onMounted } from 'vue';

const isAuthenticated = useState('isAuthenticated', () => false);
const user = useState('user', () => null);

onMounted(() => {
  tiun.on('userChange', (data) => {
    isAuthenticated.value = data.isAuthenticated;
    user.value = data.user;
  });

  tiun.init({
    snippetId: 'YOUR_SNIPPET_ID',
    language: 'en',
  });
});
</script>
```

Any component can then read the shared state:

```vue
<script setup>
const isAuthenticated = useState('isAuthenticated');
const user = useState('user');
</script>

<template>
  <p v-if="isAuthenticated">Logged in as {{ user.email }}</p>
  <p v-else>Not logged in</p>
</template>
```

***

## Gate content

Use `useState` values in any page or component to show or hide content based on `productAccess`.

```vue
<script setup>
import { tiun } from '@tiun/sdk';

const isAuthenticated = useState('isAuthenticated');
const user = useState('user');
</script>

<template>
  <div v-if="!isAuthenticated">
    <p>Please log in or subscribe.</p>
  </div>

  <div v-else-if="user?.productAccess?.includes('p-live-pro')">
    <!-- Pro content -->
  </div>

  <div v-else>
    <button @click="tiun.checkout({ productId: 'p-live-pro' })">
      Upgrade to Pro
    </button>
  </div>
</template>
```

***

## Paywall events (time-based)

For time-based billing, use `paywallShow` and `paywallHide` to control access. Wire them to shared state in `app.vue`.

```vue
<!-- app.vue -->
<script setup>
import { tiun } from '@tiun/sdk';
import { onMounted } from 'vue';

const showPaywall = useState('showPaywall', () => true);

onMounted(() => {
  tiun.on('paywallShow', () => {
    showPaywall.value = true;
  });

  tiun.on('paywallHide', () => {
    showPaywall.value = false;
  });

  tiun.init({
    snippetId: 'YOUR_SNIPPET_ID',
    language: 'en',
  });
});
</script>
```

Then in any component:

```vue
<script setup>
import { tiun } from '@tiun/sdk';

const showPaywall = useState('showPaywall');
</script>

<template>
  <div v-if="showPaywall">
    <h2>Premium content</h2>
    <button @click="tiun.start()">
      Get access
    </button>
  </div>

  <article v-else>
    <!-- premium content -->
  </article>
</template>
```

***

## Content management (time-based)

Track route changes to keep billing accurate. Use `router.afterEach` with an `import.meta.client` guard so it only runs in the browser. Send the initial content state when the SDK is ready.

```vue
<!-- app.vue -->
<script setup>
import { tiun } from '@tiun/sdk';
import { onMounted } from 'vue';

const router = useRouter();

onMounted(() => {
  tiun.on('ready', () => {
    sendContentState(router.currentRoute.value);
  });

  tiun.init({
    snippetId: 'YOUR_SNIPPET_ID',
    language: 'en',
  });
});

function sendContentState(route) {
  const isPaid = route.path !== '/';
  tiun.setContent({
    type: isPaid ? 'active' : 'inactive',
    contentId: route.params?.slug || route.path
  });
}

if (import.meta.client) {
  router.afterEach((to) => {
    if (!tiun.isReady) return;
    sendContentState(to);
  });
}
</script>
```

***

## Alternative: plugin approach

Instead of `app.vue`, you can initialize tiun in a Nuxt plugin. The `.client.ts` suffix ensures it only runs in the browser.

```ts
// plugins/tiun.client.ts
import { tiun } from '@tiun/sdk';

export default defineNuxtPlugin(() => {
  tiun.init({
    snippetId: 'YOUR_SNIPPET_ID',
    language: 'en',
  });
});
```

This runs before any component mounts. Either approach works — pick whichever fits your project layout. Neither needs an explicit teardown at the app root.


# Mobile and Native

There are two ways to use tiun in a mobile app today: **Mobile WebView** (supported now) and **first-class native SDKs** (on the roadmap).

***

## Mobile WebView (available today)

The tiun SDK runs **inside** a WebView the same way it runs in any browser. The native shell does not call the SDK directly — it loads your web app in a `WKWebView` (iOS) or `WebView` (Android) and lets the JS SDK handle checkout, login, and paywall events.

For entitlement checks that need to flow back to native code, expose a small JS bridge that returns a verification token from `getUserVerificationToken()`.

```javascript
import { tiun } from '@tiun/sdk';

tiun.init({
  snippetId: window.TIUN_SNIPPET_ID,
  language: 'en', // 'en' | 'de' | 'fr'
});

window.getTiunVerification = async () => {
  const token = await tiun.getUserVerificationToken();
  return JSON.stringify({ token });
};
```

The native host then calls `getTiunVerification()` via its JS bridge — `evaluateJavascript` on Android, `WKWebView.evaluateJavaScript` on iOS — and forwards the token to its own backend, which verifies it server-side. See [server-side subscription verification](/guides/subscriptions/verify-server-side) for the verification round-trip.

### Injecting the snippet ID from the native host

The native shell should inject `window.TIUN_SNIPPET_ID` before page load (via `WKUserScript` on iOS, or `addJavascriptInterface` / `evaluateJavascript` on Android) so the same JS bundle works across environments without hardcoded IDs.

### Lifecycle

* If the native host swaps the WebView's URL (full reload), the page tears down automatically — no `destroy()` needed.
* If the native host swaps DOM content **without** reload, call `tiun.destroy()` before loading new content so listeners and runtime config are cleaned up.

See [Lifecycle and `destroy()`](/sdk/getting-started/initialization#lifecycle-and-destroy) for the full matrix.

***

## Native SDKs (roadmap)

First-class native SDKs — Swift (iOS), Kotlin (Android), React Native, and Flutter — are on the roadmap.

{% hint style="info" %}
Native SDKs are coming soon. For updates, join the [Discord](https://discord.gg/NCggN2ExZ9) or reach out to <support@tiun.app>.
{% endhint %}


# Session

## PATCH /live\_api/s2s/v1/sessions/{sessionId}/status

> Retrieve the current status of a tiun session

```json
{"openapi":"3.0.1","info":{"title":"Public tiun.live API documentation","version":"v1"},"servers":[{"url":"https://api.tiun.live","description":"Production"},{"url":"https://api-sandbox.tiun.live","description":"Sandbox"}],"paths":{"/live_api/s2s/v1/sessions/{sessionId}/status":{"patch":{"tags":["Session"],"summary":"Retrieve the current status of a tiun session","parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string"}},{"name":"X-TIUN-API-KEY","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The session is valid and the content can be served."},"401":{"description":"The API key or providerId is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"The session is invalid and the content should not be served.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}}},"components":{"schemas":{"ProblemDetails":{"type":"object","properties":{"type":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"status":{"type":"integer","format":"int32","nullable":true},"detail":{"type":"string","nullable":true},"instance":{"type":"string","nullable":true}},"additionalProperties":{}}}}}
```


# UserVerification

## POST /live\_api/s2s/v1/users/verification

> Verify the authentication status of a tiun user

```json
{"openapi":"3.0.1","info":{"title":"Public tiun.live API documentation","version":"v1"},"servers":[{"url":"https://api.tiun.live","description":"Production"},{"url":"https://api-sandbox.tiun.live","description":"Sandbox"}],"paths":{"/live_api/s2s/v1/users/verification":{"post":{"tags":["UserVerification"],"summary":"Verify the authentication status of a tiun user","parameters":[{"name":"X-TIUN-API-KEY","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserVerificationRequestV1"}},"application/*+json":{"schema":{"$ref":"#/components/schemas/UserVerificationRequestV1"}}},"required":true},"responses":{"200":{"description":"The request was successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseV1"}}}},"401":{"description":"The API key or providerId is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}}},"components":{"schemas":{"UserVerificationRequestV1":{"required":["userVerificationToken"],"type":"object","properties":{"userVerificationToken":{"minLength":1,"type":"string"}},"additionalProperties":false},"UserResponseV1":{"required":["isAuthenticated"],"type":"object","properties":{"isAuthenticated":{"type":"boolean"},"userInfo":{"$ref":"#/components/schemas/UserInfoResponseV1"}},"additionalProperties":false},"UserInfoResponseV1":{"required":["email","productAccess","userId"],"type":"object","properties":{"userId":{"minLength":1,"type":"string"},"email":{"minLength":1,"type":"string"},"productAccess":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"ProblemDetails":{"type":"object","properties":{"type":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"status":{"type":"integer","format":"int32","nullable":true},"detail":{"type":"string","nullable":true},"instance":{"type":"string","nullable":true}},"additionalProperties":{}}}}}
```


