# 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, one-time purchases, 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, a one-time fixed fee, or a time-based rate), then call **checkout** (subscriptions and one-time purchases) 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 (Visa, Mastercard, Amex), Apple Pay, Google Pay, PayPal and TWINT (region-dependent).   |
| **Authentication**      | Email + OTP login, no passwords.                                                              |
| **Session persistence** | Returning users are recognized automatically — sessions and payment connections are restored. |
| **Access control**      | Entitlement state and paywall events, handled for you.                                        |
| **Customer data**       | Users, purchases, 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.io>
{% endhint %}

***

## Agent

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

{% 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
npx skills add tiun-app/skills
```

{% 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, one-time purchase, or 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.
* [**Sell one-time products**](/guides/one-time-purchases/sell-one-time-products) — A single fixed fee, checkout, and permanent access.
* [**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

## General

### What is tiun?

tiun is an end-to-end monetization platform — the commercial backend for the AI engineering era. Instead of stitching together an auth provider, a payments processor, a customer database, and an analytics tool, you get one integrated ecosystem that covers authentication, payments, customer data, and product analytics, exposed through a single SDK. tiun is also your Merchant of Record, so it handles tax, payouts, and chargebacks. You drop in a few lines of code; tiun runs the rest.

### Who is tiun for?

SaaS and AI companies selling digital products globally — from solo builders launching their first product, to small teams that want to avoid a multi-tool stack, to enterprises. It is not built for physical goods, marketplaces with third-party sellers, in-person retail, or complex wire-transfer enterprise contracts.

### How am I faster with tiun?

Three reasons: you integrate one system instead of four; you don't write webhooks or glue logic between disconnected tools; and because user, payment, and product data live in one place, tiun can surface improvement opportunities that siloed tools can't.

### What does tiun replace?

The usual four-tool stack — an auth provider, a payments processor, a customer/CRM database, and an analytics tool — collapses into a single integrated system, with no data silos or middleware to maintain.

### 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.

***

## How tiun Works

### What does Merchant of Record mean for me?

As Merchant of Record, tiun is the legal seller of your product to the end customer. That moves tax, compliance, and payment liability from you to tiun — you receive payouts and focus on your product instead of billing infrastructure.

### Why one system instead of multiple tools?

Because users, payments, and usage data are connected by default, setup is faster and there's no synchronization logic to maintain. It also lets tiun generate recommendations from cross-data correlation that separate tools simply can't produce.

### How does tiun turn my data into recommendations?

tiun correlates user, payment, and usage data and lets you ask plain-language questions about it. Instead of piecing together scattered metrics, you get answers that point at improvement opportunities and development priorities.

### 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, one-time purchase, 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).

### 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.

***

## Products & Billing

### What product types can I sell?

Three, each matching a different pricing and access pattern:

* **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".
* **One-time purchase** — the user pays a single fixed fee and access never expires. Best for consulting engagements, lifetime licenses, or a single course, report, or deliverable.
* **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 offer any combination 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), one-time purchases, time-based products, or any mix. 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), [selling one-time products](/guides/one-time-purchases/sell-one-time-products), and [charging for time-based sessions](/guides/time-based-billing/charge-for-time-based-sessions) show the patterns.

***

## Speed and Integration

### How long does it take to integrate?

The agent setup path is designed to get a basic backend running in about five minutes. Full production readiness depends on the specifics of your product.

### Can my AI agent integrate tiun?

Yes. tiun exposes an MCP server and skills, so AI agents (in Cursor or similar) can read the setup and configure your backend directly instead of you wiring it up by hand.

### 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.io> and we'll point you at the native integration path.

### 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).

***

## Payments, Payout and Invoicing

### What payment methods does tiun support?

All major credit and debit cards (Visa, Mastercard, American Express) plus the most-used digital wallets: Apple Pay, Google Pay, PayPal and TWINT. Available methods may vary by region.

### How are payouts made?

Payouts are processed directly through tiun. You receive net revenue after fees and taxes, sent to your bank account via the most efficient local rails in supported countries. The default frequency is monthly with a $200 minimum; enterprise plans support custom schedules and multiple IBANs.

### Who handles refunds, chargebacks, and fraud?

As Merchant of Record, tiun manages chargebacks and the associated risk and liability. Refunds are issued through tiun, and fraud screening runs before a transaction completes.

### Can my customers access their invoices and receipts?

Yes. Both business and private customers can view or download their receipts and invoices themselves — no support tickets required. tiun generates compliant invoices automatically for every transaction type.

### Does tiun support enterprise invoicing?

Not currently. Under the Merchant of Record model, transactions must run through tiun's checkout to be properly invoiced, so external wire-transfer invoicing workflows aren't supported.

***

## Tax and Compliance

### Do you handle sales tax and VAT for me?

Yes. As Merchant of Record, tiun calculates taxes in real time at checkout and collects and remits VAT, GST, and sales tax across global jurisdictions, assuming the tax liability itself.

### Do you handle EU VAT reverse charge for B2B transactions?

Yes. tiun validates the customer's VAT ID at checkout, applies the reverse charge automatically when the conditions are met, and issues a compliant invoice that includes the required reverse charge notation.

### Is tiun GDPR compliant?

Yes. tiun is hosted in Europe and GDPR compliant by default, with customer data stored in EU data centers. We maintain a public sub-processor list and honor customer data rights including access, deletion, and portability.

***

## Pricing

### What does tiun cost?

Sign up is free. See the [pricing page](https://tiun.io/pricing) for current plans and the comparison table for details.

***

## Comparison

### Do I still need Stripe if I use tiun?

No. tiun includes payment processing and Merchant of Record services, so you don't manage Stripe separately.

### Why not just use Stripe directly?

Stripe handles payments only. On top of it you'd still build authentication, manage entitlements, keep billing and access in sync, and create a self-serve portal. tiun provides that complete system out of the box.

### How does tiun compare to Polar, Paddle, Lemon Squeezy, and Stripe Managed Payments?

Those are billing platforms with Merchant of Record. tiun is broader — it adds authentication, a customer database, and analytics alongside billing, and offers lower all-in transaction rates.

***

## Account and Leaving

### How do I migrate to tiun?

Start by joining the Discord community or emailing <support@tiun.io> for guidance on your specific setup. Enterprise plans include a dedicated technical team to help plan and execute the switch for more complex migrations.

***

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


# 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)
* [Get user info server-side](/guides/authentication/get-user-info)

## Subscriptions

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

## One-time purchases

* [Sell one-time products](/guides/one-time-purchases/sell-one-time-products)

## 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)
* [Simulate payments](/guides/testing/simulate-payments)
* [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, one-time purchase, time-based). 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 three:

* **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.
* **One-time purchase** — you charge a single fixed fee and access never expires. Best for consulting engagements, lifetime licenses, or a single course, report, or deliverable.
* **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 one-time product:**

* **Name** — what the user will see (for example "Full course", "Lifetime license").
* **Fixed fee** — the single amount charged at checkout. The fee incurs only once; there's no interval and no 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** — 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. Pick a tax category

Whatever the type, you also file the product under a **tax category**. It's required and there's no default. tiun is the Merchant of Record for your sales, so this is what determines how the product is taxed.

The form lists the available categories with a short description, guidance on where each one ends, and examples. Pick the one your product **genuinely** belongs to — not the closest-looking match.

If none of them fit what you sell, choose **Something else** and describe your product. tiun follows up with you instead of filing it under an approximate category, so it's always better than picking a near-match.

{% hint style="info" %}
**Something else lives in this form only.** It's part of creating a product in the dashboard at [my.tiun.business](https://my.tiun.business/) — it is not available over the [MCP server](/guides/agent-integration/agent-integration), so an agent can't pick it for you. If you're setting products up through an agent and none of the categories fit, create that product here in the dashboard instead.
{% endhint %}

{% hint style="warning" %}
**Choose carefully — this one is permanent.** Unlike the name and price, a product's tax category cannot be changed after the product is created. Correcting it means archiving the product and creating a new one, and archiving cannot be undone. If you're unsure which category applies, use **Something else** rather than guessing.
{% endhint %}

***

## 5. 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. To look up a user later by ID without a token — for example from a background job — see [getting user info server-side](/guides/authentication/get-user-info).

***

## 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 %}


# Get user info server-side

Sometimes your backend needs to know a user's current state while the user isn't active in your app — a scheduled job checking whether a subscription is still active, an external service acting on the user's behalf, or any async flow that runs without a browser session. The UserInfo API covers this: your server fetches a tiun user's current `email` and `productAccess` using only their **`userId`** and your API key. No token, no user present.

***

## User info vs. verification

[Server-side authentication verification](/guides/authentication/verify-server-side) answers "**is this request really coming from a signed-in user?**" — it exchanges a short-lived token and returns `isAuthenticated`.

The UserInfo API answers "**what is the current state of a user I already know?**" — a plain lookup by ID. It doesn't authenticate anyone: anyone with your API key and a userId gets the data. Use it for server-initiated flows, and keep using verification to gate incoming requests from your frontend.

***

## How it works

1. Capture the tiun **`userId`** while the user is in your app — from the [user object](/reference/authentication/user-object) in a `userChange` payload, or from `userInfo.userId` on a verified request — and store it with your own user record.
2. Later, your server calls the [tiun UserInfo API](/api-reference/user-info) with that ID and your API key.
3. The response carries the user's current `userId`, `email`, and `productAccess` — check `productAccess` for the product you care about.

***

## Setup: API key

The endpoint is protected by an API key — the same one used for server-side verification. If you don't have one yet, create it in the dashboard: open **APIs** in the sidebar and click **Create new key**. Store it securely in your backend environment variables.

***

## Call the endpoint

**Endpoint:**

`GET /live_api/s2s/v1/users/{userId}/info`

**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** the userId belongs to — live and sandbox users are separate, and API keys are not shared between environments.

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

No request body — the userId travels in the path.

**Response codes:**

| Status | Meaning                              |
| ------ | ------------------------------------ |
| `200`  | User object returned — read the body |
| `400`  | The request is invalid               |
| `401`  | API key is invalid                   |
| `404`  | The user does not exist              |

A `200` response carries the user object:

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

Unlike the verification response, there is no `isAuthenticated` wrapper — the lookup isn't tied to a session, so the body is just the user's current state.

***

## Full example

A reusable check your backend can run from anywhere — a scheduled job, a queue worker, or a service handling a user who isn't in the app right now:

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

async function hasActiveSubscription(userId, productId) {
  const response = await fetch(
    `${BASE_URL}/live_api/s2s/v1/users/${userId}/info`,
    {
      headers: { 'X-TIUN-API-KEY': API_KEY },
    },
  );

  if (response.status === 404) {
    // unknown user → no access
    return false;
  }

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

  const user = await response.json();
  return user.productAccess.includes(productId);
}

// e.g. in a background job:
const isActive = await hasActiveSubscription('u-abc123', 'p-live-pro');
```

`productAccess` reflects what the user has paid for **right now** — tiun keeps it consistent through renewals, cancellations, and payment failures, so there is nothing to cache or reconcile on your side. See [Product access](/reference/checkout/product-access) in Reference.

***

{% 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.

{% hint style="info" %}
This page covers **one-time purchases** too. Verification is a `productAccess` check either way, so the code is the same — a one-time product ID simply never disappears from the array.
{% endhint %}

<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).

This flow assumes the user is in your app to produce a token. To check a subscription while the user is **not** active in your app — a scheduled job, an external service — look the user up by ID instead: see [getting user info server-side](/guides/authentication/get-user-info).

***

## 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 %}


# Sell one-time products

This guide walks through wiring up a one-time purchase — a buy button, checkout, and gating content once the user has paid. Login and logout are not covered here; see [authenticating your user](/guides/authentication/authenticate-your-user) for those.

One-time products use the **same SDK call as subscriptions**. The difference is what happens afterwards: the customer pays a single fixed fee and keeps access permanently, so there's no renewal or expiration for your app to handle.

***

## 1. Create a one-time product

In the dashboard, create a product with the **One-time Purchase** pricing model and set its **Fixed fee** — the single amount charged at checkout. The full walkthrough is in [creating your first product](/guides/getting-started/creating-first-product); copy the product ID when you're done.

***

## 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. Add a buy button

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

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

Wire the button to `tiun.checkout()` with the matching product ID:

```javascript
function onClickBuy() {
  tiun.checkout({ productId: TIUN_PRODUCTS.lifetime });
}
```

When the user clicks, tiun opens the checkout overlay — it shows the fixed fee, collects their email, handles identity verification, processes the 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.

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

That product ID **stays in `productAccess` permanently** — one-time purchases don't renew and don't expire, so your gate never has to handle revocation for them. See [One-time purchases](/reference/checkout/one-time-purchases) in Reference for the full lifecycle.

***

## 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) {
    showSalesPage();
    return;
  }

  if (data.user.productAccess.includes(TIUN_PRODUCTS.lifetime)) {
    showPurchasedContent();
  } else {
    showSalesPage();
  }
});
```

Use the same check to **hide or disable the buy button** once the customer owns the product. They can't buy it a second time — checkout links their existing entitlement and shows an "already purchased" screen instead of charging — so a live buy button only sends a paying customer somewhere that can't do anything for them. See [One-time purchases](/reference/checkout/one-time-purchases) in Reference.

***

## 6. Add login and logout

Returning customers need to sign in to get their purchase back on a new browser or after logging out. The setup is covered in [authenticating your user](/guides/authentication/authenticate-your-user) — once that's wired up, the same `userChange` handler picks up their existing `productAccess`.

{% hint style="info" %}
Because a one-time purchase never expires, login is the only thing standing between a returning customer and the content they paid for. Make sure a **Log in** affordance is visible on your sales page, not just a buy button — otherwise a returning customer's only visible option is to pay again.
{% endhint %}

***

## 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 one-time purchase integration in one place (login / logout wiring lives in the Auth guide):

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

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

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

  if (data.user.productAccess.includes(TIUN_PRODUCTS.lifetime)) {
    showPurchasedContent();
  } else {
    showSalesPage();
  }
});

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

function buyLifetime() {
  tiun.checkout({ productId: TIUN_PRODUCTS.lifetime });
}
```

***

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 purchase before serving protected data, use the same `productAccess`-based check documented in [verifying purchases server-side](/guides/subscriptions/verify-server-side).


# 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 connection to your my.tiun.business account, so the agent reads your snippet ID, provider, and product list directly instead of asking you to copy-paste them, and can create or edit products for you when you ask it to.

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. 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
npx skills add tiun-app/skills
```

This installs the skill into your project where your agent can pick it up automatically. It needs [Node.js](https://nodejs.org/) 18+ for `npx`; the CLI works out which agents you have installed and puts the skill where each one looks for it.

Prefer a different installer? The [tiun-app/skills README](https://github.com/tiun-app/skills#installing) covers the alternatives — `gh skill install tiun-app/skills` via the GitHub CLI, the Claude Code plugin marketplace, a Cursor remote rule, or cloning the repo and copying the skill in by hand.

***

## 2. 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.

### If your agent can't complete browser authentication

Connecting to the tiun MCP requires a browser-based sign-in. Some agents don't support that flow for remote MCP servers — the connection stalls or fails at the login step. If that happens, register the server through [mcp-remote](https://www.npmjs.com/package/mcp-remote) instead: a small local proxy that runs as a command-based (stdio) MCP server, performs the browser sign-in itself, and forwards the connection to `mcp.tiun.business`.

```json
{
  "mcpServers": {
    "tiun": {
      "command": "npx",
      "args": ["mcp-remote", "https://mcp.tiun.business/"]
    }
  }
}
```

This needs Node.js on your machine. On first connect a browser window opens for the tiun sign-in; after that, tokens are cached and refreshed automatically. Nearly every agent supports command-based MCP servers, so this works even where native remote authentication doesn't — as long as the machine has a browser. If your agent connects to `https://mcp.tiun.business/` directly without trouble (Claude Code and Cursor do), you don't need this.

Once connected, the agent can read 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.

### What the agent can do through MCP

**Read** — your providers and their snippet IDs, the product catalog for either environment, and the tax categories a product can be filed under.

**Write** — create and edit one-time and subscription products, so you can say "add a Pro plan at €9.90 a month" instead of switching to the dashboard mid-integration.

Writes are constrained on purpose:

* The agent must show you the exact change — name, price, interval, tax category, environment — and get your explicit go-ahead before it calls anything.
* It defaults to **sandbox**. Writing to live is a deliberate choice you have to make, and the agent is instructed to say so in its confirmation.
* Prices and names come from you or from values already written down in your project. The agent is instructed to ask rather than guess an amount.
* **Time-based products can't be created or edited through MCP** — those stay in the dashboard.
* **Nothing can be deleted, archived, or deactivated through MCP.** There's no undo for a write, so read the confirmation before you approve it.
* **A product whose tax category is "Something else" has to be created in the dashboard.** That option is part of the dashboard's new-product form and is not offered over MCP, so an agent cannot select it. When none of the real categories fit your product, the agent should stop and hand the step back to you.

{% hint style="info" %}
A product's **tax category is permanent** — no tool can change it after creation. If the agent proposes one that looks wrong, correct it before you approve the create. See [Products](/reference).
{% endhint %}

**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.

***

## 3. 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, one-time purchase, time-based, or a combination), 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.
* **One-time** — "Add a buy button for my lifetime license and unlock the `/pro-tools` route once it's purchased."
* **Time-based** — "Add a time-based paywall to the article view and meter content on route changes."
* **Create a product** — "Create a Pro subscription at €9.90 a month in sandbox, then wire it up." The agent will confirm the name, price, interval, tax category, and environment with you before it creates anything.

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 and one-time purchases, `tiun.start()` for time-based).
* Test payment methods complete successfully — see [Simulate payments](/guides/testing/simulate-payments) for test cards and the PayPal simulator.
* A failed payment (declining test card or **Fail test setup** in the PayPal flow) grants no access and surfaces through the SDK **`error`** event so you can log or show UI.
* Overlay closes cleanly after success or cancel.
* 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'`).
* After cancelling the test subscription in the [account portal](/guides/testing/simulate-payments#cancellation-and-expiration), the product ID is removed from `productAccess` and `userChange` fires.

## One-time purchases

* The checkout overlay shows the **fixed fee** with no billing interval or trial.
* `userChange` fires after checkout with `event: 'checkout'` and the product ID in `productAccess`.
* Returning to the app restores the purchase without forcing login (`userChange` with `event: 'init'`).
* After logging out and back in with the same email, the product ID is **still** in `productAccess` — one-time purchases don't expire, so there's no cancellation path to test.
* Your sales page offers a **log in** option, not only a buy button, so a returning customer isn't pushed to pay twice.

## 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.


# Simulate payments

In **sandbox**, checkout runs on Stripe's test mode — no payment method is ever charged, no matter which one you pick. Use this page to complete test purchases, force failures, cancel test subscriptions, and reset state between runs.

Everything here assumes your app is targeting sandbox: `sandbox: true`, your sandbox snippet ID, and `p-test-...` product IDs. See [Sandbox](/reference/generic/sandbox) in Reference for how the two environments differ.

***

## Test cards

In the card form, use one of Stripe's test card numbers with **any future expiry date**, **any CVC**, and **any name and postal code**:

| Card number           | Result           |
| --------------------- | ---------------- |
| `4242 4242 4242 4242` | Payment succeeds |
| `4000 0000 0000 0002` | Card is declined |

These cover the common cases; Stripe offers many more numbers for specific brands, countries, and error codes — see the [Stripe testing docs](https://docs.stripe.com/testing) for the full list.

***

## PayPal

Selecting PayPal in sandbox does not open the real PayPal — it redirects to Stripe's **PayPal test setup page**:

1. Choose **PayPal** in the checkout overlay and enter any email and name (they don't need to exist).
2. Continue, and you're redirected to the test setup page.
3. Click **Authorize test setup** to simulate a successful payment, or **Fail test setup** to simulate a failure.

You're then redirected back to your app, and the flow continues exactly like a real PayPal payment would. This is also the easiest way to simulate a failure for a redirect-based payment method.

***

## Apple Pay and Google Pay

Apple Pay and Google Pay appear in sandbox checkout whenever your browser and device support them. You can use your **real wallet** — in test mode the stored card is never charged, so it's safe to complete the flow end to end.

***

## What happens after a successful payment

After any successful test payment the sequence is the same:

1. The overlay shows the success screen.
2. **`userChange`** fires with `event: 'checkout'` — this can happen while the success screen is still visible, before the overlay closes.
3. The event payload's `productAccess` **already contains** the purchased `p-test-...` product ID — access is granted by the time the event fires, for every payment method.

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

{% hint style="info" %}
Rely on **`userChange`** rather than reading `tiun.user` right after the overlay closes. The event fires exactly when access is in place, so there is nothing to poll or retry. See [Product access](/reference/checkout/product-access) for how to gate your UI on it.
{% endhint %}

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

***

## Simulating failed payments

Use a declining test card (`4000 0000 0000 0002`) or **Fail test setup** in the PayPal flow. On failure:

* No access is granted — `productAccess` stays unchanged.
* The failure surfaces through the SDK **`error`** event, so you can verify your logging or error UI.
* The user can pick another payment method and retry inside the overlay.

***

## Cancellation and expiration

To test what your app does when access is removed, cancel the test subscription in the **tiun account portal** — the page where end users manage their subscriptions, purchases, transactions, and payment methods:

| Environment | Account portal                                             |
| ----------- | ---------------------------------------------------------- |
| Sandbox     | [account-staging.tiun.io](https://account-staging.tiun.io) |
| Live        | [account.tiun.io](https://account.tiun.io)                 |

Sign in with the same email you used at checkout, find the subscription, and cancel it. Once the cancellation takes effect, the product ID is **removed** from `productAccess` and `userChange` fires — your app should revoke access at that moment, with no extra handling for the expiration itself.

{% hint style="info" %}
This applies to **subscriptions only**. A one-time purchase can't be cancelled and never leaves `productAccess`, so there's no removal to test. To re-run a first-purchase flow for a one-time product, use a fresh test customer — see [Resetting sandbox state](#resetting-sandbox-state) below.
{% endhint %}

***

## Resetting sandbox state

There is no reset button — instead, start over with a **fresh test customer**:

1. Log out in your app (or clear the site data for your dev domain — sessions are per browser).
2. Run checkout again with a **new email address**. Every email is its own test customer, so the new one starts with no purchases and no `productAccess`.

Old test customers remain visible in the sandbox view of the [my.tiun.business dashboard](https://my.tiun.business/); they don't interfere with new ones. This is the way to re-test a first-purchase flow after a previous test email already has access.

***

While testing, keep the [Test flows](/guides/testing/test-flows) checklist at hand, and see [Debugging](/guides/testing/debugging) if something doesn't behave as expected.


# 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.                                                                                                                           |
| `productAccess` missing a purchase after checkout | Reading `tiun.user` too early, or wrong environment | Listen for **`userChange`** with `event: 'checkout'` — the payload already contains the updated `productAccess`. Confirm you're checking a `p-test-...` ID against the sandbox environment. |
| Can't re-test a first purchase                    | The test customer already has access                | Log out and check out with a fresh email — each email is a separate test customer. See [resetting sandbox state](/guides/testing/simulate-payments#resetting-sandbox-state).                |
| 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.io> 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 three 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.

***

## One-time purchase

One-time purchases charge a **single fixed fee**. You configure one field — the **fixed fee** — and the customer pays it once at checkout. There is no interval, no trial, and no renewal, and access does not expire.

One-time products fit consulting engagements, lifetime licenses, a single course or report, and any deliverable that is bought once rather than rented.

One-time products integrate through `tiun.checkout({ productId })` — the same call as subscriptions. See [Checkout / How it works](/reference/checkout/how-it-works) and [One-time purchases](/reference/checkout/one-time-purchases) 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 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.

***

## Tax category

Every product is filed under a **tax category** when you create it. tiun is the Merchant of Record for your sales, so the category is what determines how the product is taxed in each jurisdiction — see [Tax and Compliance](/faq) in the FAQ for what that means for you.

The dashboard shows the available categories with a short description, guidance on where each one ends, and examples. Pick the one your product **genuinely** belongs to rather than the closest-looking match.

The list is **closed** — it is not free text, and there is no default.

If nothing on it fits what you sell, choose **Something else** in the new-product form at [my.tiun.business](https://my.tiun.business/) and describe your product; tiun follows up with you rather than filing it under an approximate category. **Something else is a dashboard-only option.** It exists in the new-product form — it is not one of the categories offered over the [MCP server](/guides/agent-integration/agent-integration), so an agent cannot select it on your behalf. When no category fits, the product has to be created in the dashboard; an agent should stop and hand that step back to you rather than settling for the nearest match.

{% hint style="warning" %}
**A tax category is permanent.** It is fixed when the product is created and cannot be edited afterwards — not in the dashboard, and not through the [MCP server](/guides/agent-integration/agent-integration). The only way to correct one is to archive the product and create a new one, and archiving cannot be undone: there is no restore. Get it right at creation.
{% endhint %}

***

## 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, a one-time purchase for a lifetime license, 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.

Payments in sandbox are simulated through Stripe's test mode — nothing is ever charged. For test card numbers, the PayPal simulator, forcing failures, and resetting test state, see the [Simulate payments](/guides/testing/simulate-payments) guide.

***

## 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 is entitled to — active subscriptions and completed one-time purchases. 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 purchases:**

```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`. One-time purchases don't expire, so their IDs stay put.

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 purchases — subscription or one-time | `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 or one-time product, the overlay walks the user through:

* **Plan details** — product name and pricing. A subscription also shows the billing interval and any trial info; a one-time product shows the fixed fee and neither.
* **Email entry** — the user is identified by email as part of checkout.
* **Payment method selection** — credit/debit cards (Visa, Mastercard, American Express), PayPal, Apple Pay, Google Pay, 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) or [One-time purchases](/reference/checkout/one-time-purchases). For the resulting entitlements on the user, see [Product access](/reference/checkout/product-access).


# Product access

For subscriptions and one-time purchases alike, 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 product the user **currently has access to** — an active subscription, or a one-time purchase they've completed. 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 the user is entitled to. `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. |
| For a **one-time purchase**                     | The product ID is added at checkout and **never removed** — one-time purchases don't renew and don't expire.               |
| After logout                                    | The user object becomes `null`; there is no `productAccess` to check.                                                      |

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

***

## 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).

Your backend can also read `productAccess` for any user by their userId — without the user being present — via the UserInfo API. See [getting user info server-side](/guides/authentication/get-user-info).

{% 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.
* [One-time purchases](/reference/checkout/one-time-purchases) — the same checkout flow for products bought once instead of on a schedule.
* [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).


# One-time purchases

A one-time purchase in tiun ties an **identified user** to a **product** through a **single payment**. This page covers the lifecycle from anonymous visitor to paying customer — and why, unlike a subscription, there is nothing after that to manage.

***

## 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 buy 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 again. Logging back in restores the purchase.

{% 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 %}

***

## Access is permanent

A one-time purchase is charged **once**. There is no billing cycle, no renewal, no cancellation flow, and no expiration.

Once checkout completes, the product ID is added to the user's `productAccess` and **stays there** — across sessions, logouts, and new devices, for as long as the account exists. Your gating code never has to handle revocation for a one-time product: if the ID is in the array, the customer has paid for it.

See [Product access](/reference/checkout/product-access) for how that array is populated and how to check it in code.

{% hint style="info" %}
Because access can't be cancelled, you can't reset a one-time purchase for a test customer. To re-test a first-purchase flow in sandbox, run checkout with a **new email address** — see [simulating payments](/guides/testing/simulate-payments).
{% endhint %}

***

## Bought once per customer

The same customer can't buy the same one-time product twice. tiun stops the repeat purchase before any money moves, in both directions it can be attempted:

* **Signed in and already entitled** — checkout links the existing entitlement instead of charging again, and the customer sees an "already purchased, no new charge was made" screen.
* **Signed out, signing up again with the same email** — the sign-up is rejected because the customer already exists. They have to log in to reach what they own.

So a one-time product is a **perpetual licence, not a consumable**. It isn't a shop item that can be re-ordered: there are no quantities, carts, credit packs, or "buy another" flows. If you need customers to buy the same thing repeatedly, a one-time product is not the right model for it.

Two consequences for your UI:

1. **Hide or disable the buy button once the customer owns the product.** Gate it on `productAccess.includes(productId)`. Leaving it live sends a paying customer into a checkout that cannot charge them.
2. **Put a visible Log in option on your sales page**, not just a buy button. Login is the only route back for a returning customer — signing up again with their email is rejected, so a buy-only page leaves them stuck.

Copy should match the model too: "Buy", "Purchased", "You own this" — not "Subscribe", "Manage your plan", "Renews on", or "Cancel". None of those states exist for a one-time purchase.

***

## 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.
* [Products](/reference) — how the one-time product type compares to subscriptions and time-based billing.
* [Authentication / How it works](/reference/authentication/how-it-works) — the broader login and session model.
* For an end-to-end walkthrough, see [selling one-time products](/guides/one-time-purchases/sell-one-time-products).


# 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 (Visa, Mastercard, American Express), Apple Pay, Google Pay, PayPal or TWINT, subject to regional availability — 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 the product is a subscription, a one-time purchase, or time-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

| Method                         | Description                                                    |
| ------------------------------ | -------------------------------------------------------------- |
| `tiun.checkout({ productId })` | Open the checkout flow for a subscription or one-time 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` apply to checkout-based products — subscriptions and one-time purchases. `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.io>.
{% 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":{}}}}}
```


# User Info

## GET /live\_api/s2s/v1/users/{userId}/info

> Retrieve a tiun user by ID

```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/{userId}/info":{"get":{"tags":["UserInfo"],"summary":"Retrieve a tiun user by ID","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}},{"name":"X-TIUN-API-KEY","in":"header","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The request was successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfoResponseV1"}}}},"400":{"description":"The request is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"description":"The API key is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"The user does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}}},"components":{"schemas":{"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":{}}}}}
```


# User Verification

## 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 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":{}}}}}
```


