Skip to main content
Use TonConnect’s native sendTransaction method when you need full control over the wallet connection UI and transaction flow. This approach is ideal for applications that require custom UI/UX or need to integrate with existing TonConnect implementations.

When to Use Direct TonConnect

Choose direct TonConnect integration when you need:
  • Custom UI Control: Full control over wallet connection buttons, modals, and transaction flow presentation
  • Existing TonConnect Setup: Your application already uses TonConnect for other blockchain interactions
  • Advanced Features: Access to TonConnect’s advanced features like status change listeners and custom wallet adapters
  • Non-React Applications: Integration with Vue, Angular, Svelte, or vanilla JavaScript
If you’re building a React application without specific UI requirements, consider using the useTonPay hook for a simpler integration experience.

Integration Overview

Direct TonConnect integration follows this flow:
1

Provider Setup

Configure the TonConnect UI provider with your application manifest.
2

Connection Management

Handle wallet connection state and provide UI for users to connect their wallets.
3

Message Creation

Build the transaction message using createTonPayTransfer on your server or client.
4

Transaction Sending

Send the transaction through TonConnect’s sendTransaction method.
5

Status Monitoring

Listen for transaction status updates and connection state changes.

React Implementation

Application Setup

Wrap your application with the TonConnect UI provider:
import { TonConnectUIProvider } from "@tonconnect/ui-react";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <TonConnectUIProvider manifestUrl="/tonconnect-manifest.json">
      {children}
    </TonConnectUIProvider>
  );
}
The manifest URL must be publicly accessible and served over HTTPS in production. The manifest file contains your application’s metadata and is required for wallet identification.

Payment Component

import {
  useTonAddress,
  useTonConnectModal,
  useTonConnectUI,
} from "@tonconnect/ui-react";
import { createTonPayTransfer } from "@ton-pay/api";
import { useState } from "react";

export function PaymentComponent({ orderId, amount }: { orderId: string; amount: number }) {
  const address = useTonAddress(true); // Get user-friendly address format
  const { open } = useTonConnectModal();
  const [tonConnectUI] = useTonConnectUI();
  const [loading, setLoading] = useState(false);

  const handlePayment = async () => {
    // Check if wallet is connected
    if (!address) {
      open();
      return;
    }

    setLoading(true);

    try {
      // Create the transaction message
      // Note: For production, consider moving this to a server endpoint
      const { message, reference, bodyBase64Hash } = await createTonPayTransfer(
        {
          amount,
          asset: "TON",
          recipientAddr: "EQ....yourWalletAddress",
          senderAddr: address,
          commentToSender: `Payment for Order ${orderId}`,
          commentToRecipient: `Order ${orderId}`,
        },
        {
          chain: "testnet",
          apiKey: "yourTonPayApiKey",
        }
      );

      // Store tracking identifiers
      await fetch("/api/store-payment", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ reference, bodyBase64Hash, orderId, amount }),
      });

      // Send transaction through TonConnect
      const result = await tonConnectUI.sendTransaction({
        messages: [message],
        validUntil: Math.floor(Date.now() / 1000) + 300, // 5 minutes
        from: address,
      });

      console.log("Transaction sent:", result.boc);

      // Handle success
      window.location.href = `/orders/${orderId}/success`;
    } catch (error) {
      console.error("Payment failed:", error);
      alert("Payment failed. Please try again.");
    } finally {
      setLoading(false);
    }
  };

  return (
    <button onClick={handlePayment} disabled={loading}>
      {loading ? "Processing..." : address ? `Pay ${amount} TON` : "Connect Wallet"}
    </button>
  );
}

Connection State Management

Listen for wallet connection changes to update your UI:
import { useEffect } from "react";
import { useTonConnectUI } from "@tonconnect/ui-react";

export function WalletStatus() {
  const [tonConnectUI] = useTonConnectUI();
  const [walletInfo, setWalletInfo] = useState(null);

  useEffect(() => {
    // Listen for connection status changes
    const unsubscribe = tonConnectUI.onStatusChange((wallet) => {
      if (wallet) {
        console.log("Wallet connected:", wallet.account.address);
        setWalletInfo({
          address: wallet.account.address,
          chain: wallet.account.chain,
          walletName: wallet.device.appName,
        });
      } else {
        console.log("Wallet disconnected");
        setWalletInfo(null);
      }
    });

    return () => {
      unsubscribe();
    };
  }, [tonConnectUI]);

  if (!walletInfo) {
    return <div>No wallet connected</div>;
  }

  return (
    <div>
      <p>Connected: {walletInfo.walletName}</p>
      <p>Address: {walletInfo.address}</p>
    </div>
  );
}

Server-Side Message Building

For production applications, build transaction messages on your server to centralize tracking and validation:

Backend Endpoint

import { createTonPayTransfer } from "@ton-pay/api";
import { validateWalletAddress } from "./utils/validation";

app.post("/api/create-transaction", async (req, res) => {
  const { orderId, senderAddr } = req.body;

  try {
    // Validate inputs
    if (!validateWalletAddress(senderAddr)) {
      return res.status(400).json({ error: "Invalid wallet address" });
    }

    // Fetch order details from database
    const order = await db.orders.findById(orderId);
    if (!order) {
      return res.status(404).json({ error: "Order not found" });
    }

    if (order.status !== "pending") {
      return res.status(400).json({ error: "Order already processed" });
    }

    // Create transaction message
    const { message, reference, bodyBase64Hash } = await createTonPayTransfer(
      {
        amount: order.amount,
        asset: order.currency || "TON",
        recipientAddr: "EQ....yourWalletAddress",
        senderAddr,
        commentToSender: `Payment for Order ${order.id}`,
        commentToRecipient: `Order ${order.id} - ${order.description}`,
      },
      {
        chain: "testnet",
        apiKey: "yourTonPayApiKey",
      }
    );

    // Store tracking identifiers
    await db.payments.create({
      orderId: order.id,
      reference,
      bodyBase64Hash,
      amount: order.amount,
      asset: order.currency || "TON",
      senderAddr,
      status: "pending",
      createdAt: new Date(),
    });

    // Return message to client
    res.json({ message });
  } catch (error) {
    console.error("Failed to create transaction:", error);
    res.status(500).json({ error: "Failed to create transaction" });
  }
});

Frontend Implementation

export function ServerManagedPayment({ orderId }: { orderId: string }) {
  const address = useTonAddress(true);
  const { open } = useTonConnectModal();
  const [tonConnectUI] = useTonConnectUI();
  const [loading, setLoading] = useState(false);

  const handlePayment = async () => {
    if (!address) {
      open();
      return;
    }

    setLoading(true);

    try {
      // Request transaction message from server
      const response = await fetch("/api/create-transaction", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ orderId, senderAddr: address }),
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.error || "Failed to create transaction");
      }

      const { message } = await response.json();

      // Send transaction
      const result = await tonConnectUI.sendTransaction({
        messages: [message],
        validUntil: Math.floor(Date.now() / 1000) + 300,
        from: address,
      });

      console.log("Transaction completed:", result.boc);

      // Navigate to success page
      window.location.href = `/orders/${orderId}/success`;
    } catch (error) {
      console.error("Payment error:", error);
      alert(error.message || "Payment failed");
    } finally {
      setLoading(false);
    }
  };

  return (
    <button onClick={handlePayment} disabled={loading}>
      {loading ? "Processing..." : "Complete Payment"}
    </button>
  );
}

Vanilla JavaScript Implementation

For non-React applications, use the TonConnect SDK directly:
import TonConnectUI from "@tonconnect/ui";
import { createTonPayTransfer } from "@ton-pay/api";

const tonConnectUI = new TonConnectUI({
  manifestUrl: "https://yourdomain.com/tonconnect-manifest.json",
});

// Connect wallet
async function connectWallet() {
  await tonConnectUI.connectWallet();
}

// Send payment
async function sendPayment(amount, orderId) {
  const wallet = tonConnectUI.wallet;

  if (!wallet) {
    await connectWallet();
    return;
  }

  try {
    // Create transaction message
    const { message, reference, bodyBase64Hash } = await createTonPayTransfer(
      {
        amount,
        asset: "TON",
        recipientAddr: "EQC...RECIPIENT",
        senderAddr: wallet.account.address,
        commentToSender: `Order ${orderId}`,
      },
      { chain: "mainnet" }
    );

    // Store tracking data
    await fetch("/api/store-payment", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ reference, bodyBase64Hash, orderId }),
    });

    // Send transaction
    const result = await tonConnectUI.sendTransaction({
      messages: [message],
      validUntil: Math.floor(Date.now() / 1000) + 300,
      from: wallet.account.address,
    });

    console.log("Payment successful:", result.boc);
  } catch (error) {
    console.error("Payment failed:", error);
  }
}

// Listen for connection changes
tonConnectUI.onStatusChange((wallet) => {
  if (wallet) {
    console.log("Wallet connected:", wallet.account.address);
    document.getElementById("wallet-address").textContent = wallet.account.address;
  } else {
    console.log("Wallet disconnected");
    document.getElementById("wallet-address").textContent = "Not connected";
  }
});

Transaction Parameters

Message Structure

The message object passed to sendTransaction must include:
address
string
required
Recipient wallet address in user-friendly format (e.g., “EQC…”).
amount
string
required
Amount to send in nanotons. For TON payments, multiply the human-readable amount by 1e9.
payload
string
required
Base64-encoded message payload containing transfer details and tracking information.

Transaction Options

validUntil
number
required
Unix timestamp indicating when the transaction expires. Typically set to 5 minutes from now.
validUntil: Math.floor(Date.now() / 1000) + 300
from
string
required
Sender’s wallet address. Must match the connected wallet address.
network
string
Network identifier. Usually omitted as it’s inferred from the connected wallet.

Error Handling

Handle common error scenarios appropriately:
async function handleTransaction() {
  try {
    const result = await tonConnectUI.sendTransaction({
      messages: [message],
      validUntil: Math.floor(Date.now() / 1000) + 300,
      from: address,
    });
    
    return result;
  } catch (error) {
    // User rejected the transaction
    if (error.message?.includes("rejected")) {
      console.log("User cancelled the transaction");
      return null;
    }
    
    // Wallet not connected
    if (error.message?.includes("Wallet is not connected")) {
      console.log("Please connect your wallet first");
      tonConnectUI.connectWallet();
      return null;
    }
    
    // Transaction expired
    if (error.message?.includes("expired")) {
      console.log("Transaction expired, please try again");
      return null;
    }
    
    // Network or other errors
    console.error("Transaction failed:", error);
    throw error;
  }
}

Best Practices

Check wallet connection status before attempting to send transactions. Provide clear UI feedback for connection state.
const wallet = tonConnectUI.wallet;

if (!wallet) {
  // Show connect button
  return;
}

// Proceed with transaction
Use a reasonable validUntil value (typically 5 minutes) to ensure transactions don’t become stale, but give users enough time to review and sign.
const validUntil = Math.floor(Date.now() / 1000) + 300; // 5 minutes
Ensure the sender address from TonConnect matches the format expected by your backend. Use the user-friendly format consistently.
const address = useTonAddress(true); // true = user-friendly format
Always persist reference and bodyBase64Hash before sending the transaction. If the transaction succeeds but your code fails afterward, you’ll still be able to reconcile the payment via webhooks.
// Good: Store first, then send
await storePaymentTracking(reference, bodyBase64Hash);
await tonConnectUI.sendTransaction(...);

// Bad: Send first, then store
await tonConnectUI.sendTransaction(...);
await storePaymentTracking(reference, bodyBase64Hash); // Might not execute
Monitor wallet connection changes to update your UI and handle disconnections gracefully.
useEffect(() => {
  const unsubscribe = tonConnectUI.onStatusChange((wallet) => {
    if (wallet) {
      setConnectedWallet(wallet.account.address);
    } else {
      setConnectedWallet(null);
    }
  });
  
  return unsubscribe;
}, [tonConnectUI]);
Users may reject transactions in their wallet. Treat rejection as a normal cancellation, not an error.
try {
  await tonConnectUI.sendTransaction(...);
} catch (error) {
  if (error.message?.includes("rejected")) {
    // Don't show error - user intentionally cancelled
    console.log("Transaction cancelled by user");
  } else {
    // Show error for unexpected failures
    showErrorMessage("Transaction failed");
  }
}

Troubleshooting

Ensure the wallet is connected before calling sendTransaction:
if (!tonConnectUI.wallet) {
  await tonConnectUI.connectWallet();
  // Wait for connection before proceeding
}
Consider adding a connection state check:
const isConnected = tonConnectUI.wallet !== null;
Verify that:
  • The from parameter matches the connected wallet address exactly
  • The recipient address in the message is a valid TON address
  • You’re using the correct address format (user-friendly vs raw)
const address = useTonAddress(true); // Ensure consistent format
The validUntil timestamp is too short or the user is taking too long to sign. Increase the validity period:
// Increase from 5 to 10 minutes if needed
validUntil: Math.floor(Date.now() / 1000) + 600
Common manifest issues:
  • URL is not publicly accessible
  • CORS headers are not configured correctly
  • Manifest JSON is malformed
  • URL is not HTTPS (required in production)
Test your manifest URL directly in a browser to verify accessibility.
Ensure you’re properly subscribing to status changes and that the subscription persists:
useEffect(() => {
  const unsubscribe = tonConnectUI.onStatusChange(handleWalletChange);
  return () => unsubscribe(); // Clean up subscription
}, [tonConnectUI]);
This can happen if you’re calling connectWallet() multiple times. Implement connection state tracking:
const [isConnecting, setIsConnecting] = useState(false);

const connect = async () => {
  if (isConnecting) return;
  setIsConnecting(true);
  try {
    await tonConnectUI.connectWallet();
  } finally {
    setIsConnecting(false);
  }
};

API Key Configuration

The TON Pay API key is optional but unlocks essential merchant features including transaction tracking, webhook notifications, and wallet management. When using TonConnect with server-side message building, include the API key in your backend:
import { createTonPayTransfer } from "@ton-pay/api";

app.post("/api/create-transaction", async (req, res) => {
  const { orderId, senderAddr } = req.body;
  const order = await db.orders.findById(orderId);

  const { message, reference, bodyBase64Hash } = await createTonPayTransfer(
    {
      amount: order.amount,
      asset: "TON",
      recipientAddr: "EQ....yourWalletAddress",
      senderAddr,
    },
    {
      chain: "testnet",
      apiKey: "yourTonPayApiKey",
    }
  );

  await db.payments.create({ orderId, reference, bodyBase64Hash });
  res.json({ message });
});
For complete details about API key benefits, security best practices, and configuration, see the API Key Configuration section.

Testnet Configuration

Test your TonConnect integration on the TON testnet before production deployment.

Basic Testnet Setup

Configure environment variables for testnet:
# .env.development
TON_CHAIN=testnet
MERCHANT_WALLET_ADDRESS=EQC...TESTNET_ADDRESS
import { useTonAddress, useTonConnectUI } from "@tonconnect/ui-react";
import { createTonPayTransfer } from "@ton-pay/api";

export function TestnetPayment({ amount }: { amount: number }) {
  const address = useTonAddress(true);
  const [tonConnectUI] = useTonConnectUI();

  const handlePayment = async () => {
    if (!address) {
      tonConnectUI.connectWallet();
      return;
    }

    const { message } = await createTonPayTransfer(
      {
        amount,
        asset: "TON",
        recipientAddr: "EQ....yourWalletAddress",
        senderAddr: address,
      },
      { chain: "testnet" } // Use testnet
    );

    const result = await tonConnectUI.sendTransaction({
      messages: [message],
      validUntil: Math.floor(Date.now() / 1000) + 300,
      from: address,
    });

    console.log("Testnet transaction:", result.boc);
  };

  return <button onClick={handlePayment}>Test Payment</button>;
}
For complete testnet configuration including faucet access, jetton testing, block explorers, webhook testing, best practices, and production transition checklist, see the Testnet Configuration section.

Next Steps