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

# Инструкция по интеграции

> Это руководство предназначено для технической реализации интеграции Holders WebView в продукт партнера.

# Схема архитектуры интеграции

<img src="https://mintcdn.com/whalescorp/sYcwF1WqD1mDpark/images/Screenshot2026-01-26at18.50.25(1).png?fit=max&auto=format&n=sYcwF1WqD1mDpark&q=85&s=361edaa262f2422e975ef7fc85303181" alt="Screenshot2026 01 26at18 50 25(1)" width="673" height="775" data-path="images/Screenshot2026-01-26at18.50.25(1).png" />

# API-эндпоинты

### Backend API

| Environment | Endpoint |
| :- | :- |
| **Production** | `https://card-prod.whales-api.com` |
| **Staging/Testnet** | `https://card-staging.whales-api.com` |

### URL веб-приложения

| Environment | URL\* |
| :- | :- |
| **Production** | `https://yourwallet.holders.io` |
| **Staging/Testnet** | `https://yourwallet-stage.holders.io` |

\*Вместо `yourwallet` будут использоваться URL с названием вашего кошелька.

# Процесс аутентификации

## Общий поток аутентификации

<img src="https://mintcdn.com/whalescorp/sYcwF1WqD1mDpark/images/Screenshot2026-01-26at19.10.25.png?fit=max&auto=format&n=sYcwF1WqD1mDpark&q=85&s=9aefade0a7d013625b9e87e9ed01cbda" alt="Screenshot2026 01 26at19 10 25" width="629" height="903" data-path="images/Screenshot2026-01-26at19.10.25.png" />

## Структура proof

Для аутентификации используется протокол [TonConnect v2](https://github.com/ton-connect). Необходимо сформировать proof, то есть доказательство владения кошельком, для TON и, при необходимости, для Solana.

### Структура TON Proof

```
interface TonProof {
  timestamp: number;           // Unix timestamp in seconds
  domain: {
    lengthBytes: number;       // Domain length in bytes
    value: string;             // Domain (for example, "holders.io")
  };
  signature: string;           // Base64 signature
  payload: string;             // Arbitrary payload from the server
  walletStateInit: string;     // Wallet StateInit in base64
  publicKey: string;           // Public key in hex
}
```

### Алгоритм создания TON Proof

```
// 1. Construct the message to sign
const messageBuffer = Buffer.concat([
  Buffer.from('ton-proof-item-v2/'),
  addressBuffer,           // workchain (4 bytes BE) + hash (32 bytes)
  domainLengthBuffer,      // 4 bytes LE
  domainBuffer,            // domain string
  timestampBuffer,         // 8 bytes LE (Int64)
  Buffer.from(payload)     // payload string
]);

// 2. Hash it
const message = sha256(messageBuffer);

// 3. Add prefix
const bufferToSign = Buffer.concat([
  Buffer.from('ffff', 'hex'),
  Buffer.from('ton-connect'),
  message
]);

// 4. Sign
const signature = nacl.sign.detached(sha256(bufferToSign), secretKey);
```

**Структура Solana Proof**

```
interface SolanaProof {
  timestamp: number;
  domain: {
    lengthBytes: number;
    value: string;
  };
  signature: string;        // Base58 encoded signature
  payload: string;
  publicKey: string;        // Base58 public key
}
```

### Формат запроса на аутентификацию

```
// Request for TON + Solana
type TonSolanaAuthRequest = [
  {
    stack: 'ton';
    network: 'ton-mainnet' | 'ton-testnet';
    key: {
      kind: 'tonconnect-v2';
      wallet: 'yourwallet';
      config: {
        address: string;          // Raw address format
        proof: TonProof;
      };
    };
    inviteId?: string;            // Optional invite code
    invitationId?: string;        // Optional invitation ID
    appsflyerId?: string;         // AppsFlyer UID for analytics
  },
  {
    stack: 'solana';
    network: 'solana-mainnet' | 'solana-devnet';
    key: {
      kind: 'tonconnect-v2';
      wallet: 'yourwallet';
      config: {
        address: string;          // Base58 address
        proof: SolanaProof;
      };
    };
  }
];
```

# API-методы

## Подключение кошелька, Enrollment

**Endpoint:**`POST /v2/user/wallet/connect`\
**Запрос:**

```
[
  {
    "stack": "ton",
    "network": "ton-mainnet",
    "key": {
      "kind": "tonconnect-v2",
      "wallet": "yourwallet",
      "config": {
        "address": "0:...",
        "proof": {
          "timestamp": 1706284800,
          "domain": { "lengthBytes": 10, "value": "holders.io" },
          "signature": "base64...",
          "payload": "ton-proof-any",
          "walletStateInit": "base64...",
          "publicKey": "hex..."
        }
      }
    },
    "inviteId": "optional-invite-code"
  },
  {
    "stack": "solana",
    "network": "solana-mainnet",
    "key": {
      "kind": "tonconnect-v2",
      "wallet": "yourwallet",
      "config": {
        "address": "base58...",
        "proof": {
          "timestamp": 1706284800,
          "domain": { "lengthBytes": 10, "value": "holders.io" },
          "signature": "base58...",
          "payload": "solana-proof-any",
          "publicKey": "base58..."
        }
      }
    }
  }
]
```

**Ответ:**

```
{
  "ok": true,
  "token": "jwt-token-string"
}
```

## Получение состояния пользователя

**Endpoint:**`POST /account/state`\
**Запрос:**

```
{
  "token": "jwt-token"
}
```

**Ответ:**

```
{
  "ok": true,
  "state": {
    "state": "ok",
    "notificationSettings": { "enabled": true },
    "suspended": false
  }
}
```

## Список аккаунтов, приватный метод

**Endpoint:**`POST /v2/account/list`\
**Запрос:**

```
{
  "token": "jwt-token"
}
```

**Ответ:**

```
{
  "ok": true,
  "list": [
    {
      "id": "account-id",
      "type": "crypto",
      "accountIndex": 0,
      "address": "EQ...",
      "state": "ACTIVE",
      "balance": "1000000000",
      "network": "ton-mainnet",
      "cards": [
        {
          "id": "card-id",
          "status": "ACTIVE",
          "lastFourDigits": "1234",
          "productId": "product-id"
        }
      ],
      "limits": {
        "daily": "100000000",
        "monthly": "1000000000",
        "onetime": "50000000"
      }
    }
  ],
  "prepaidCards": []
}
```

## Публичные данные аккаунта, авторизация не требуется

**Endpoint:**`POST /v2/public/accounts`\
**Запрос:**

```
{
  "walletKind": "yourwallet",
  "wallets": [
    {
      "network": "ton-mainnet",
      "address": "EQ..."
    },
    {
      "network": "solana",
      "address": "base58..."
    }
  ]
}
```

**Ответ:**

```
{
  "ok": true,
  "accounts": [
    {
      "id": "account-id",
      "accountIndex": 0,
      "address": "EQ...",
      "state": "ACTIVE",
      "balance": "1000000000",
      "network": "ton-mainnet",
      "cards": [
        {
          "lastFourDigits": "1234",
          "productId": "product-id"
        }
      ]
    }
  ]
}
```

## Проверка доступа, Invite Check

**Endpoint:**`POST /v2/invite/wallet/check/v2`\
**Запрос:**

```
{
  "wallet": "EQ...",
  "network": "ton-mainnet",
  "region": {
    "countryCode": "US",
    "storeFrontCode": "usa"
  },
  "campaignId": "optional-campaign",
  "campaignParams": {}
}
```

**Ответ:**

```
{
  "allowed": true,
  "banner": {
    "id": 1,
    "content": {
      "action": { "en": "Get Card", "ru": "Получить карту" },
      "title": { "en": "Welcome!", "ru": "Добро пожаловать!" },
      "subtitle": { "en": "...", "ru": "..." }
    }
  }
}
```

## Профиль пользователя

**Endpoint:**`POST /v2/profile/get`\
**Запрос:**

```
{
  "token": "jwt-token"
}
```

**Ответ:**

```
{
  "ok": true,
  "data": {
    "userId": "user-id",
    "email": "user@example.com",
    "emailVerified": true,
    "phone": "+1234567890",
    "country": "US",
    "firstName": "John",
    "lastName": "Doe"
  }
}
```

## Список карточных транзакций

**Endpoint:**`POST /v2/card/events`\
**Запрос:**

```
{
  "token": "jwt-token",
  "cardId": "card-id",
  "limit": 40,
  "cursor": "optional-cursor",
  "order": "desc"
}
```

**Ответ:**

```
{
  "ok": true,
  "data": {
    "events": [
      {
        "type": "charge",
        "id": "event-id",
        "cardId": "card-id",
        "time": 1706284800,
        "data": {
          "accountId": "account-id",
          "amount": "1000000000",
          "currency": "USD",
          "currencyAmount": "10.00"
        }
      }
    ],
    "cursor": "next-cursor",
    "more": true
  }
}
```

## Данные для Apple Pay

**Endpoint:**`POST /v2/card/provisioning/list`\
**Запрос:**

```
{
  "token": "jwt-token"
}
```

**Ответ:**

```
{
  "ok": true,
  "cards": [
    {
      "id": "card-id",
      "lastFourDigits": "1234",
      "title": "Holders Card",
      "cardholderName": "JOHN DOE",
      "assetName": "card-asset",
      "assetUrl": "https://..."
    }
  ]
}
```

# Интеграция WebView

## Схема архитектуры WebView

<img src="https://mintcdn.com/whalescorp/sYcwF1WqD1mDpark/images/Screenshot2026-01-26at19.22.37.png?fit=max&auto=format&n=sYcwF1WqD1mDpark&q=85&s=57888614dcabe98ae550222eb7c7acac" alt="Screenshot2026 01 26at19 22 37" width="561" height="863" data-path="images/Screenshot2026-01-26at19.22.37.png" />

Подробное описание встроенного bridge и всех его API можно найти на странице [WebView Bridge APIs](/ru/integration-gides/technical-integration/webviewbridgeapis).

# URL-параметры

При загрузке Holders WebApp нужно передать следующие query-параметры:

```
const queryParams = new URLSearchParams({
  lang: 'en',                    // Language: 'en' | 'ru'
  currency: 'USD',               // Display currency
  theme: 'holders',              // Theme
  'theme-style': 'dark',         // 'dark' | 'light'
  bounceable: 'false',           // Address format
  'initial-route': '/accounts'   // Initial route
});

const url = `https://tonhub.holders.io?${queryParams.toString()}`;
```

### Типы

| **Имя параметра** | **Значение** |
| :- | :- |
| lang | en |
| currency | код валюты |
| theme | код темы |
| theme-style | dark |

**Поддерживаемые валюты:**

```
const supportedCurrencies = [
  { code: 'USD', symbol: '$', label: 'U.S. Dollar' },
  { code: 'EUR', symbol: '€', label: 'Euro' },
  { code: 'RUB', symbol: '₽', label: 'Russian Ruble' },
  { code: 'GBP', symbol: '£', label: 'British Pound' },
  { code: 'CHF', symbol: '₣', label: 'Swiss Franc' },
  { code: 'CNY', symbol: '¥', label: 'Chinese Yuan' },
  { code: 'KRW', symbol: '₩', label: 'South Korean Won' },
  { code: 'IDR', symbol: 'Rp', label: 'Indonesian Rupiah' },
  { code: 'INR', symbol: '₹', label: 'Indian Rupee' },
  { code: 'JPY', symbol: '¥', label: 'Japanese Yen' }
];
```

## Инъекция начального состояния

```
const initialState = {
  user: {
    status: {
      state: 'ok',
      kycStatus: null,
      suspended: false
    },
    token: 'jwt-token'
  },
  accountsList: [...],
  prepaidCards: [...]
};

const injectedJS = `
  window.initialState = ${JSON.stringify(initialState)};
`;
```

### Мост TonConnect для WebView

```
interface TonConnectInjectedBridge {
  deviceInfo: DeviceInfo;
  protocolVersion: number;
  isWalletBrowser: boolean;
  
  // Connect a new app
  connect(
    protocolVersion: number,
    request: ConnectRequest
  ): Promise<ConnectEvent>;
  
  // Restore an existing connection
  restoreConnection(): Promise<ConnectEvent>;
  
  // Disconnect
  disconnect(): Promise<void>;
  
  // Send requests (transactions, data signing)
  send<T extends RpcMethod>(request: AppRequest<T>): Promise<WalletResponse<T>>;
}
```

## Query API

WebView может управлять нативным приложением через URL-параметры:

| **Параметр** | **Значения** | **Описание** |
| :- | :- | :- |
| `closeApp` | `true` | Закрыть WebView |
| `openEnrollment` | `true` | Запустить процесс регистрации |
| `openUrl` | URL | Открыть URL в браузере |
| `backPolicy` | `back` / `close` / `lock` | Поведение кнопки «Назад» |
| `lockScroll` | `true` / `false` | Блокировка прокрутки |
| `showKAV` | `true` / `false` | Показать дополнительную панель клавиатуры |

# Состояния пользователя

### Схема переходов между состояниями пользователя

<img src="https://mintcdn.com/whalescorp/sYcwF1WqD1mDpark/images/Screenshot2026-01-27at12.43.50.png?fit=max&auto=format&n=sYcwF1WqD1mDpark&q=85&s=ac17b8c8bf0237cdadf0d51bb51ff4e3" alt="Screenshot2026 01 27at12 43 50" width="701" height="885" data-path="images/Screenshot2026-01-27at12.43.50.png" />

## Обработка состояний

```
enum HoldersUserState {
  NeedEnrollment = 'need-enrollment',
  NeedPhone = 'need-phone',
  NoRef = 'no-ref',
  NeedKyc = 'need-kyc',
  NeedEmail = 'need-email',
  Ok = 'ok',
}

async function handleUserState(token: string): Promise<void> {
  const state = await fetchUserState(token);
  
  switch (state.state) {
    case HoldersUserState.NeedEnrollment:
      // Show the registration screen
      navigateToEnrollment();
      break;
      
    case HoldersUserState.NoRef:
      // Show the waiting/waitlist screen
      showWaitlistScreen();
      break;
      
    case HoldersUserState.NeedPhone:
      // Redirect to phone verification in WebView
      openHoldersWebView('/phone-verification');
      break;
      
    case HoldersUserState.NeedKyc:
      // Redirect to KYC in WebView
      openHoldersWebView('/kyc');
      break;
      
    case HoldersUserState.NeedEmail:
      // Redirect to email verification in WebView
      openHoldersWebView('/email-verification');
      break;
      
    case HoldersUserState.Ok:
      // Full access
      openHoldersWebView('/accounts');
      break;
  }
}
```

# Примеры кода

## TypeScript: полный поток аутентификации

```
import { Address, beginCell, storeStateInit } from '@ton/core';
import { sha256_sync } from '@ton/crypto';
import nacl from 'tweetnacl';
import { Int64LE } from 'int64-buffer';

interface WalletKeys {
  publicKey: Buffer;
  secretKey: Buffer;
}

interface EnrollmentResult {
  success: boolean;
  token?: string;
  error?: string;
}

class HoldersIntegration {
  private endpoint: string;
  private isTestnet: boolean;

  constructor(isTestnet: boolean = false) {
    this.isTestnet = isTestnet;
    this.endpoint = isTestnet 
      ? 'https://card-staging.whales-api.com'
      : 'https://card-prod.whales-api.com';
  }

  /**
   * TON Proof creating
   */
  private createTonProof(
    address: Address,
    keys: WalletKeys,
    domain: string,
    payload: string,
    walletStateInit: string
  ) {
    const timestamp = Math.floor(Date.now() / 1000);
    const timestampBuffer = new Int64LE(timestamp).toBuffer();
    
    const domainBuffer = Buffer.from(domain);
    const domainLengthBuffer = Buffer.allocUnsafe(4);
    domainLengthBuffer.writeInt32LE(domainBuffer.byteLength);

    const rawAddress = address.toRawString();
    const [workchain, addrHash] = rawAddress.split(':');

    const addressWorkchainBuffer = Buffer.allocUnsafe(4);
    addressWorkchainBuffer.writeInt32BE(Number(workchain));

    const addressBuffer = Buffer.concat([
      addressWorkchainBuffer,
      Buffer.from(addrHash, 'hex'),
    ]);

    const messageBuffer = Buffer.concat([
      Buffer.from('ton-proof-item-v2/'),
      addressBuffer,
      domainLengthBuffer,
      domainBuffer,
      timestampBuffer,
      Buffer.from(payload),
    ]);

    const message = sha256_sync(messageBuffer);

    const bufferToSign = Buffer.concat([
      Buffer.from('ffff', 'hex'),
      Buffer.from('ton-connect'),
      message,
    ]);

    const signed = nacl.sign.detached(
      sha256_sync(bufferToSign),
      Uint8Array.from(keys.secretKey)
    );

    return {
      timestamp,
      domain: {
        lengthBytes: domainBuffer.byteLength,
        value: domain,
      },
      signature: Buffer.from(signed).toString('base64'),
      payload,
      walletStateInit,
      publicKey: keys.publicKey.toString('hex'),
    };
  }

  /**
   * Fetching the application manifest
   */
  async getManifest() {
    const manifestUrl = this.isTestnet
      ? 'https://yourwallet-stage.holders.io/jsons/tonconnect-manifest.json'
      : 'https://yourwallet.holders.io/jsons/tonconnect-manifest.json';
    
    const response = await fetch(manifestUrl);
    return response.json();
  }

  /**
   * Wallet connect (enrollment)
   */
  async enrollWallet(
    address: Address,
    keys: WalletKeys,
    walletStateInit: string,
    inviteId?: string
  ): Promise<EnrollmentResult> {
    try {
      const manifest = await this.getManifest();
      const domain = new URL(manifest.url).hostname;
      
      const tonProof = this.createTonProof(
        address,
        keys,
        domain,
        'ton-proof-any',
        walletStateInit
      );

      const requestBody = [
        {
          stack: 'ton',
          network: this.isTestnet ? 'ton-testnet' : 'ton-mainnet',
          key: {
            kind: 'tonconnect-v2',
            wallet: 'yourwallet',
            config: {
              address: address.toRawString(),
              proof: tonProof,
            },
          },
          inviteId,
        },
      ];

      const response = await fetch(`${this.endpoint}/v2/user/wallet/connect`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(requestBody),
      });

      const data = await response.json();

      if (!data.ok) {
        return { success: false, error: 'Failed to fetch user token' };
      }

      return { success: true, token: data.token };
    } catch (error) {
      return { success: false, error: String(error) };
    }
  }

  /**
   * Fetching the user state
   */
  async getUserState(token: string) {
    const response = await fetch(`${this.endpoint}/account/state`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token }),
    });

    if (response.status === 401) {
      return null;
    }

    const data = await response.json();
    
    if (!data.ok) {
      throw new Error('Failed to fetch account state');
    }

    return data.state;
  }

  /**
   * Fetching the account list
   */
  async getAccounts(token: string) {
    const response = await fetch(`${this.endpoint}/v2/account/list`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token }),
    });

    const data = await response.json();

    if (!data.ok) {
      throw new Error('Failed to fetch accounts');
    }

    return {
      accounts: data.list,
      prepaidCards: data.prepaidCards,
    };
  }

  /**
   * Public account data (no authorization)
   */
  async getPublicAccounts(address: string, solanaAddress?: string) {
    const body = solanaAddress
      ? {
          walletKind: 'yourwallet',
          wallets: [
            { network: this.isTestnet ? 'ton-testnet' : 'ton-mainnet', address },
            { network: 'solana', address: solanaAddress },
          ],
        }
      : {
          walletKind: 'yourwallet',
          network: this.isTestnet ? 'ton-testnet' : 'ton-mainnet',
          address,
        };

    const response = await fetch(`${this.endpoint}/v2/public/accounts`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });

    const data = await response.json();

    if (!data.ok) {
      throw new Error('Failed to fetch public accounts');
    }

    return data.accounts;
  }

  /**
   * Access check
   */
  async checkInvite(address: string, countryCode: string) {
    const response = await fetch(`${this.endpoint}/v2/invite/wallet/check/v2`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        wallet: address,
        network: this.isTestnet ? 'ton-testnet' : 'ton-mainnet',
        region: { countryCode },
      }),
    });

    return response.json();
  }
}

// Usage example
async function example() {
  const holders = new HoldersIntegration(false); // Production
  
  // Create a proof and connect the wallet
  const result = await holders.enrollWallet(
    Address.parse('EQ...'),
    { publicKey: Buffer.from('...'), secretKey: Buffer.from('...') },
    'base64-state-init...',
    'optional-invite-code'
  );
  
  if (result.success && result.token) {
    // Save the token
    localStorage.setItem('holders-token', result.token);
    
    // Fetch the state
    const state = await holders.getUserState(result.token);
    console.log('User state:', state);
    
    // Fetch the accounts
    if (state.state === 'ok') {
      const accounts = await holders.getAccounts(result.token);
      console.log('Accounts:', accounts);
    }
  }
}
```

## React Native: WebView component

```
import React, { useRef, useMemo, useCallback } from 'react';
import { View } from 'react-native';
import WebView, { WebViewMessageEvent } from 'react-native-webview';

interface HoldersWebViewProps {
  token: string;
  userState: any;
  accounts?: any[];
  onClose?: () => void;
  isTestnet?: boolean;
}

export function HoldersWebView({
  token,
  userState,
  accounts,
  onClose,
  isTestnet = false,
}: HoldersWebViewProps) {
  const webViewRef = useRef<WebView>(null);

  const baseUrl = isTestnet
    ? 'https://tonhub-stage.holders.io'
    : 'https://tonhub.holders.io';

  const source = useMemo(() => {
    const params = new URLSearchParams({
      lang: 'en',
      currency: 'USD',
      theme: 'holders',
      'theme-style': 'dark',
      'initial-route': '/accounts',
    });

    return { uri: `${baseUrl}?${params.toString()}` };
  }, [baseUrl]);

  // Initial state injection
  const injectedJS = useMemo(() => {
    const initialState = {
      user: {
        status: {
          state: userState.state,
          suspended: userState.suspended || false,
        },
        token,
      },
      accountsList: accounts || [],
    };

    return `
      (function() {
        window.initialState = ${JSON.stringify(initialState)};
      })();
      true;
    `;
  }, [token, userState, accounts]);

  // Handling messages from the WebView
  const handleMessage = useCallback((event: WebViewMessageEvent) => {
    try {
      const message = JSON.parse(event.nativeEvent.data);
      
      switch (message.type) {
        case 'closeApp':
          onClose?.();
          break;
          
        case 'sendTransaction':
          // Transaction handling
          handleTransaction(message.payload);
          break;
          
        case 'loaded':
          // WebView loaded
          console.log('Holders WebView loaded');
          break;
      }
    } catch (error) {
      console.error('Failed to parse WebView message:', error);
    }
  }, [onClose]);

  return (
    <View style={{ flex: 1 }}>
      <WebView
        ref={webViewRef}
        source={source}
        injectedJavaScriptBeforeContentLoaded={injectedJS}
        onMessage={handleMessage}
        allowsInlineMediaPlayback
        javaScriptEnabled
        domStorageEnabled
      />
    </View>
  );
}
```

# Interaction flowcharts

## Full Holders workflow

<img src="https://mintcdn.com/whalescorp/sYcwF1WqD1mDpark/images/Integrationflow(1).png?fit=max&auto=format&n=sYcwF1WqD1mDpark&q=85&s=4ea34aedeb560eeec405e3e47c52676a" alt="Integrationflow(1)" width="2840" height="5224" data-path="images/Integrationflow(1).png" />

## Handling transactions via TonConnect

<img src="https://mintcdn.com/whalescorp/sYcwF1WqD1mDpark/images/Screenshot2026-01-27at13.19.41.png?fit=max&auto=format&n=sYcwF1WqD1mDpark&q=85&s=91235c0b1cc4cb7f3f22c8478cd8b73b" alt="Screenshot2026 01 27at13 19 41" width="920" height="885" data-path="images/Screenshot2026-01-27at13.19.41.png" />

# Push notifications

The Holders platform implements a push notification system that informs users about key operations being performed. For each integration project, it is recommended to individually determine the optimal mechanism for delivering notifications to end users. For questions regarding the configuration of this functionality, please contact the Holders integration manager.

# Data storage

Recommended storage structure\
**Typescript**

```
interface HoldersStorage {
  // JWT token by wallet address
  getToken(address: string): string | null;
  setToken(address: string, token: string): void;
  deleteToken(address: string): void;
  
  // Apple Pay credentials
  getProvisioningCredentials(): ProvisioningCredential[];
  setProvisioningCredentials(creds: ProvisioningCredential[]): void;
  
  // Invite/referral data
  getInviteId(): string | null;
  setInviteId(id: string): void;
  
  // Campaign parameters
  getCampaignParams(): Record<string, string>;
  setCampaignParams(params: Record<string, string>): void;
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.