> For the complete documentation index, see [llms.txt](https://docs.tiun.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.tiun.io/guides/time-based-billing/verify-server-side.md).

# 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.md) 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.md).
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.tiun.io/guides/time-based-billing/verify-server-side.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
