[Go to site: main page, start]

TON DocsTON Docs

Get started with TON Connect

This page walks you through every supported integration path.

Start with What you need and then select the section for your stack:

What you need

Before you start integrating, prepare the manifest file and pick a TON Connect SDK.

Prepare the manifest

Prepare tonconnect-manifest.json — the JSON file the wallet fetches to learn your app's name, icon, and policy URLs. The wallet shows this metadata to the user before approving the connection.

The minimum:

{
  "url": "https://yourapp.com",
  "name": "Your App",
  "iconUrl": "https://yourapp.com/icon-180.png"
}

Optional:

{
  "termsOfUseUrl": "https://yourapp.com/terms",
  "privacyPolicyUrl": "https://yourapp.com/privacy"
}

Hosting requirements:

The manifest must be publicly accessible by the time wallets connect to your app:

  • The file must be reachable with a GET from any origin, without CORS restrictions, without auth and without a Cloudflare-style proxy challenge.
  • The icon at the URL listed in iconUrl must be PNG or ICO. SVG is not supported. Use a 180×180 px PNG.
  • The manifest must be served over HTTPS. Wallets do not guarantee they will fetch a manifest served over plain HTTP.
  • Any reachable HTTPS URL is valid. Hosting the manifest at the root of your domain (e.g. https://yourapp.com/tonconnect-manifest.json) keeps access simple.

If the wallet cannot fetch the manifest, the connect flow returns MANIFEST_NOT_FOUND_ERROR (code 2) or MANIFEST_CONTENT_ERROR (code 3). See Manifest 404 and CORS.

For the full field reference, see Manifest.

Pick an SDK

There are three dApp-facing packages, all published from ton-connect/sdk:

PackageWhen to use
@tonconnect/ui-reactReact or Next.js dApps. Recommended. Hooks, prebuilt button, modal.
@tonconnect/uiVanilla JS or non-React frameworks. Same UI components, no React bindings.
@tonconnect/sdkHeadless integrations — server-side flows, custom UI from scratch.

Pick ui-react if your app is React. Pick ui if it is not. Reach for sdk only when you need low-level control.

Build a dApp with React

Install the UI kit, host the manifest, mount the provider, add a connect button, and send a test transaction.

For Next.js-specific notes (App Router, 'use client', SSR), see Build a dApp with Next.js.

Install

npm i @tonconnect/ui-react

Host the manifest

Place tonconnect-manifest.json at the root of your app's domain. See What you need for the full requirements.

Wrap the app in TonConnectUIProvider

import { TonConnectUIProvider } from '@tonconnect/ui-react';

export function App() {
    return (
        <TonConnectUIProvider manifestUrl="https://yourapp.com/tonconnect-manifest.json">
            <YourApp />
        </TonConnectUIProvider>
    );
}

Add the connect button

import { TonConnectButton } from '@tonconnect/ui-react';

export function Header() {
    return (
        <header>
            <TonConnectButton />
        </header>
    );
}

The button toggles between "Connect Wallet" and the connected account state automatically.

You can pass a className or style prop:

<TonConnectButton className="my-class" style={{ float: 'right' }} />

Read connection state

import {
    useIsConnectionRestored,
    useTonAddress,
    useTonWallet,
} from '@tonconnect/ui-react';

function Status() {
    const address = useTonAddress();
    const wallet = useTonWallet();
    const restored = useIsConnectionRestored();

    if (!restored) return <span>Restoring…</span>;
    if (!wallet) return <span>Not connected</span>;
    return <span>Connected as {address}</span>;
}

Send a transaction

The destination here is the connected wallet's own address, returned by useTonAddress() in user-friendly form. Replace it with your recipient.

import { useTonAddress, useTonConnectUI, useTonWallet } from '@tonconnect/ui-react';

function PayButton() {
    const [tonConnectUi] = useTonConnectUI();
    const wallet = useTonWallet();
    const address = useTonAddress();

    const handlePay = async () => {
        if (!address) return;
        try {
            await tonConnectUi.sendTransaction({
                validUntil: Math.floor(Date.now() / 1000) + 300,
                network: '-239', // mainnet
                messages: [
                    { address, amount: '100000000' }, // 0.1 Gram
                ],
            });
        } catch (e) {
            console.error(e);
        }
    };

    return (
        <button onClick={handlePay} disabled={!wallet}>
            Send 0.1 Gram
        </button>
    );
}

address must be in user-friendly format (base64url with the bounce flag — EQ… for bounceable, UQ… for non-bounceable). Wallets reject raw 0:<hex> addresses. To convert one, use toUserFriendlyAddress from @tonconnect/ui-react. Each message also accepts optional payload, stateInit, and extraCurrency fields. Amounts use nanograms: 1 Gram = 10⁹ nanogram. Send 100000000 for 0.1 Gram.

Open the modal manually

const [tonConnectUi] = useTonConnectUI();

<button onClick={() => tonConnectUi.openModal()}>
    Connect Wallet
</button>

Customize the UI

import { useTonConnectUI, THEME } from '@tonconnect/ui-react';

const [tonConnectUi] = useTonConnectUI();

tonConnectUi.uiOptions = {
    language: 'ru',
    uiPreferences: { theme: THEME.DARK },
};

uiOptions is a setter, not a plain object. Assigning to it runs the merge, theme switch, and re-render logic; mutating a nested property (e.g. tonConnectUi.uiOptions.uiPreferences.theme = ...) bypasses the setter and has no effect. Always reassign the whole object.

Build a dApp with Next.js

Next.js needs two adjustments on top of the React tutorial: TonConnectUIProvider must run on the client, and the manifest is served from public/. The hooks behave the same as in plain React.

Install

npm i @tonconnect/ui-react

Host the manifest

Drop tonconnect-manifest.json into your project's public/ directory:

public/tonconnect-manifest.json

Next.js serves the file at https://yourapp.com/tonconnect-manifest.json. Make sure your hosting layer does not add CORS or auth gates — see Manifest 404 and CORS.

Set up App Router

TonConnectUIProvider reads from local storage and opens modals — both browser-only. Mount it in a client component:

// app/providers.tsx
'use client';

import { TonConnectUIProvider } from '@tonconnect/ui-react';

export function Providers({ children }: { children: React.ReactNode }) {
    return (
        <TonConnectUIProvider manifestUrl="https://yourapp.com/tonconnect-manifest.json">
            {children}
        </TonConnectUIProvider>
    );
}

Then wrap the root layout:

// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
        <html>
            <body>
                <Providers>{children}</Providers>
            </body>
        </html>
    );
}

Components that use useTonWallet, useTonConnectUI, etc. must also start with 'use client'.

Set up Pages Router

Dynamic-import the provider with SSR off so it does not run on the server:

import dynamic from 'next/dynamic';
import type { AppProps } from 'next/app';

const TonConnectUIProvider = dynamic(
    () => import('@tonconnect/ui-react').then(m => m.TonConnectUIProvider),
    { ssr: false }
);

function MyApp({ Component, pageProps }: AppProps) {
    return (
        <TonConnectUIProvider manifestUrl="https://yourapp.com/tonconnect-manifest.json">
            <Component {...pageProps} />
        </TonConnectUIProvider>
    );
}

export default MyApp;

SSR pitfalls

  • Local storage. TON Connect persists the session in localStorage. Code that reads wallet state during SSR cannot know whether a session will restore. Render wallet-dependent UI in a client component and use useIsConnectionRestored() to distinguish "restoring" from "disconnected."
  • Browser-only APIs. Anything from @tonconnect/ui-react that touches window, modals, or storage must run in a client component or be lazy-loaded with ssr: false.

The hooks, button, and transaction-sending API are identical to plain React — see Build a dApp with React for the rest.

Build a dApp with vanilla JS

Same flow as the React tutorial — connect button, status subscription, transaction — built with @tonconnect/ui for plain HTML / JavaScript.

Install

Via npm:

npm i @tonconnect/ui

Or via CDN:

<script src="https://unpkg.com/@tonconnect/ui@latest/dist/tonconnect-ui.min.js"></script>

The CDN bundle exposes the API on window.TON_CONNECT_UI.

Host the manifest

Place tonconnect-manifest.json at the root of your domain. Hosting rules in What you need.

Mark up a connect-button container

<div id="ton-connect"></div>
<button id="send" disabled>Send 0.1 Gram</button>

Initialize the UI

const ui = new TON_CONNECT_UI.TonConnectUI({
    manifestUrl: 'https://yourapp.com/tonconnect-manifest.json',
    buttonRootId: 'ton-connect',
});

The library renders the connect button into #ton-connect. Tapping it opens the wallet picker.

If you imported via npm:

import { TonConnectUI } from '@tonconnect/ui';
const ui = new TonConnectUI({ /* same options */ });

Subscribe to connection status

ui.onStatusChange(wallet => {
    document.getElementById('send').disabled = !wallet;
});

wallet is null when disconnected, or a connected Wallet (with device, account, and optional connectItems) otherwise.

Send a transaction

import { toUserFriendlyAddress } from '@tonconnect/ui';

document.getElementById('send').onclick = async () => {
    const wallet = ui.wallet;
    if (!wallet) return;
    const address = toUserFriendlyAddress(wallet.account.address);  // or window.TON_CONNECT_UI.toUserFriendlyAddress
    try {
        await ui.sendTransaction({
            validUntil: Math.floor(Date.now() / 1000) + 300,
            network: '-239',
            messages: [
                { address, amount: '100000000' }, // 0.1 Gram
            ],
        });
    } catch (error) {
        console.error('Transaction failed:', error);
    }
};

The destination address must be in user-friendly format; see the note in the React example. Amounts are in nanograms: 1 Gram = 10⁹ nanograms.

Restore on reload

onStatusChange fires whenever the wallet state changes — connect, disconnect, and a successful session restore.

Build without a UI (@tonconnect/sdk)

@tonconnect/sdk is the headless TON Connect implementation — the same protocol layer that ships inside @tonconnect/ui-react and @tonconnect/ui, with no wallet picker, no modal, and no DOM dependencies. Use it on a server, in a custom UI you render yourself, or as a reference when porting TON Connect to another language.

For a regular browser dApp, prefer @tonconnect/ui-react (React, Next.js) or @tonconnect/ui (vanilla JS). They wrap this SDK and add the wallet picker, connect button, and notifications.

This section covers the headless API end-to-end: install, connector setup, custom storage, wallet discovery, the connect handshake, status events, and sendTransaction. The same shape applies whether you call it from a server or a custom in-browser UI.

Install

npm i @tonconnect/sdk

Create a connector

import TonConnect from '@tonconnect/sdk';

const connector = new TonConnect({
    manifestUrl: 'https://yourapp.com/tonconnect-manifest.json',
    storage: myStorage,
});

await connector.restoreConnection();

manifestUrl is the public URL of your tonconnect-manifest.json. The wallet fetches it during connect and shows the metadata to the user. See What you need for hosting rules.

storage is an IStorage implementation. In a browser, the SDK falls back to window.localStorage if you omit it. Anywhere else — Node.js, a worker — supply your own. restoreConnection() reads from storage and wires the connector back to the bridge if a session is already there. Call it once per instance, not on every request.

Custom storage

interface IStorage {
    setItem(key: string, value: string): Promise<void>;
    getItem(key: string): Promise<string | null>;
    removeItem(key: string): Promise<void>;
}

A trivial in-memory implementation suits one-shot flows:

class MemoryStorage implements IStorage {
    private data = new Map<string, string>();
    async setItem(key: string, value: string) { this.data.set(key, value); }
    async getItem(key: string) { return this.data.get(key) ?? null; }
    async removeItem(key: string) { this.data.delete(key); }
}

For long-running servers, back IStorage with a per-user record in your database (Postgres, Redis, etc.) keyed by your user ID. See Long-lived servers.

Discover wallets

const wallets = await connector.getWallets();

Each entry is a WalletInfo with the fields a custom UI needs to render a picker and start a connect:

interface WalletInfoBase {
    name: string;        // human-readable display name
    appName: string;     // stable identifier
    imageUrl: string;
    aboutUrl: string;
    tondns?: string;
    platforms: ('ios' | 'android' | 'macos' | 'windows'
              | 'linux' | 'chrome' | 'firefox' | 'safari')[];
    features?: Feature[];
}

interface WalletInfoRemote extends WalletInfoBase {
    universalLink: string;
    bridgeUrl: string;
    deepLink?: string;
}

interface WalletInfoInjectable extends WalletInfoBase {
    jsBridgeKey: string;
    injected: boolean;
    embedded: boolean;
}

type WalletInfo =
    | WalletInfoRemote
    | WalletInfoInjectable
    | (WalletInfoRemote & WalletInfoInjectable);

A wallet that supports both transports satisfies the intersection. Narrow with 'universalLink' in wallet for HTTP wallets and 'jsBridgeKey' in wallet for injected ones.

Connect

For an HTTP wallet, connect() returns a universal link. Show it to the user as a clickable URL, a deep link, or a QR code:

const link = connector.connect({
    universalLink: 'https://connect.mytonwallet.org',
    bridgeUrl: 'https://tonconnectbridge.mytonwallet.org/bridge/',
});

For a JS-injected wallet (browser extension or in-wallet browser), pass the bridge key. The wallet handles the handoff in-page, so connect() returns void:

connector.connect({ jsBridgeKey: 'mytonwallet' });

To request ton_proof alongside the address, pass it under request:

connector.connect(
    { universalLink, bridgeUrl },
    { request: { tonProof: nonce } },
);

See Connect a wallet for the proof-verification flow.

Listen for status changes

const unsubscribe = connector.onStatusChange(wallet => {
    if (wallet) {
        // wallet.account.address — raw 0:<hex> (convert to user-friendly before passing to sendTransaction)
        // wallet.account.publicKey — hex string without 0x, optional (some wallets omit it)
        // wallet.connectItems?.tonProof — TonProofItemReply, may carry proof or error
    } else {
        // disconnected — by the user or by the wallet
    }
});

The same callback fires for connects, restores, and wallet-initiated disconnects. Call unsubscribe() when you no longer need it.

Send a transaction

const result = await connector.sendTransaction({
    validUntil: Math.floor(Date.now() / 1000) + 300,
    network: '-239', // mainnet (use '-3' for testnet)
    messages: [
        { address: 'UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJKZ', // burn address
          amount: '20000000' },
    ],
});

console.log(result.boc); // base64 BoC of the signed external message

Each wallet advertises its own per-call limit on the SendTransaction feature entry: wallet.device.features.find(f => typeof f === 'object' && f.name === 'SendTransaction')?.maxMessages.

Long-lived servers

  • One TonConnect instance per signed-in user, with an IStorage keyed by user ID, and an in-process cache so the same instance is reused across requests.
  • The HTTP bridge stays open over SSE for as long as the connector is live. Call pauseConnection() when a user goes idle and unPauseConnection() when they return.
  • React to wallet-initiated disconnects through onStatusChange. When the callback fires with null, evict the cached connector and clear any session token you issued.
  • Persist the session per user, not globally. Two users sharing one TonConnect will leak addresses and overwrite each other's session keypairs.

Next steps