Skip to main content
The useTonPay hook provides a streamlined interface for sending TON Pay transfers in React applications. It handles wallet connection, transaction signing, and error management automatically, abstracting the complexity of TonConnect integration.

How useTonPay Works

The useTonPay hook simplifies the payment process by managing the entire transaction lifecycle:
1

Wallet Connection

If the user’s wallet is not connected, the hook automatically opens the TonConnect modal and waits for a successful connection.
2

Message Creation

Your application provides a factory function that builds the transaction message. This can be done client-side or by calling your backend server.
3

Transaction Signing

The hook sends the transaction to the user’s wallet for approval and signature.
4

Result Handling

Upon completion, the hook returns the transaction result along with tracking identifiers for payment reconciliation.

Integration Approaches

You can integrate useTonPay using two different approaches depending on your application architecture:

Client-Side Message Building

Build the payment message directly in the browser. This approach is faster to implement and reduces server load, but requires the client to handle all transaction details. Best suited for:
  • Applications without backend infrastructure
  • Rapid prototyping and development
  • Simple payment flows without complex business logic

Server-Side Message Building

Delegate message creation to your backend server. The server builds the transaction, stores tracking identifiers, and returns the message to the client for signing. Best suited for:
  • Production applications requiring audit trails
  • Complex payment workflows with validation
  • Centralized logging and transaction management
  • Applications where tracking identifiers must be persisted before user action

Client-Side Implementation

Prerequisites

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

export function App() {
  return (
    <TonConnectUIProvider manifestUrl="/tonconnect-manifest.json">
      {/* Your application components */}
    </TonConnectUIProvider>
  );
}
The TonConnect manifest file must be publicly accessible and properly configured with your application’s metadata. See TonConnect documentation for manifest requirements.

Basic Implementation

import { useTonPay } from "@ton-pay/ui-react";
import { createTonPayTransfer } from "@ton-pay/api";

export function PaymentButton() {
  const { pay } = useTonPay();

  const handlePayment = async () => {
    try {
      const { txResult, message, reference, bodyBase64Hash } = await pay(
        async (senderAddr: string) => {
          const result = await createTonPayTransfer(
            {
              amount: 3.5,
              asset: "TON",
              recipientAddr: "EQ....yourWalletAddress",
              senderAddr,
              commentToSender: "Payment for Order #8451",
            },
            {
              chain: "mainnet",
              apiKey: "yourTonPayApiKey",
            }
          );

          return {
            message: result.message,
            reference: result.reference,
            bodyBase64Hash: result.bodyBase64Hash,
          };
        }
      );

      console.log("Transaction completed:", txResult.boc);
      console.log("Reference for tracking:", reference);
      console.log("Body hash:", bodyBase64Hash);

      // Store tracking identifiers in your database
      await savePaymentRecord({
        reference,
        bodyBase64Hash,
        amount: 3.5,
        asset: "TON",
      });
    } catch (error) {
      console.error("Payment failed:", error);
      // Handle error appropriately
    }
  };

  return <button onClick={handlePayment}>Pay 3.5 TON</button>;
}

Understanding the Response

The pay function returns an object containing:
txResult
SendTransactionResponse
required
Transaction result from TonConnect containing the signed transaction BOC (Bag of Cells) and other transaction details.
message
TonPayMessage
required
The transaction message that was sent, including the recipient address, amount, and payload.
reference
string
Unique tracking identifier for this transaction. Use this to correlate webhook notifications with your orders.
Store this reference in your database immediately. You will need it to match incoming webhook notifications to specific orders.
bodyBase64Hash
string
Base64-encoded hash of the transaction body. This can be used for advanced transaction verification.

Server-Side Implementation

Backend Endpoint

Create an API endpoint that builds the transaction message and stores tracking data:
import { createTonPayTransfer } from "@ton-pay/api";

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

  try {
    const { message, reference, bodyBase64Hash } = await createTonPayTransfer(
      {
        amount,
        asset: "TON",
        recipientAddr: "EQ........",
        senderAddr,
        commentToSender: `Payment for Order ${orderId}`,
        commentToRecipient: `Order ${orderId}`,
      },
      {
        chain: "testnet",
        apiKey: "yourTonPayApiKey",
      }
    );

    // Store tracking identifiers in your database
    await db.createPayment({
      orderId,
      reference,
      bodyBase64Hash,
      amount,
      asset: "TON",
      status: "pending",
      senderAddr,
    });

    // Return only the message to the client
    res.json({ message });
  } catch (error) {
    console.error("Failed to create payment:", error);
    res.status(500).json({ error: "Failed to create payment" });
  }
});
Always persist tracking identifiers (reference and bodyBase64Hash) in your database before returning the message to the client. If the client loses connection or closes the browser, you still need these identifiers to process incoming webhooks.

Frontend Implementation

import { useTonPay } from "@ton-pay/ui-react";

export function ServerPaymentButton({
  orderId,
  amount,
}: {
  orderId: string;
  amount: number;
}) {
  const { pay } = useTonPay();

  const handlePayment = async () => {
    try {
      const { txResult } = await pay(async (senderAddr: string) => {
        const response = await fetch("/api/create-payment", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ amount, senderAddr, orderId }),
        });

        if (!response.ok) {
          throw new Error("Failed to create payment");
        }

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

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

      // Optionally redirect or show success message
      window.location.href = `/orders/${orderId}/success`;
    } catch (error) {
      console.error("Payment failed:", error);
      alert("Payment failed. Please try again.");
    }
  };

  return <button onClick={handlePayment}>Pay {amount} TON</button>;
}

Error Handling

The useTonPay hook throws errors in the following scenarios:

Wallet Connection Errors

const { pay } = useTonPay();

try {
  await pay(getMessage);
} catch (error) {
  if (error.message === "Wallet connection modal closed") {
    // User closed the connection modal without connecting
    console.log("User cancelled wallet connection");
  } else if (error.message === "Wallet connection timeout") {
    // Connection attempt exceeded 5-minute timeout
    console.log("Connection timeout - please try again");
  }
}

Transaction Errors

try {
  await pay(getMessage);
} catch (error) {
  // User rejected the transaction in their wallet
  if (error.message?.includes("rejected")) {
    console.log("User rejected the transaction");
  }

  // Network or API errors
  else if (error.message?.includes("Failed to create TON Pay transfer")) {
    console.log("API error - check your configuration");
  }

  // Other unexpected errors
  else {
    console.error("Unexpected error:", error);
  }
}

Best Practices

The reference and bodyBase64Hash returned by createTonPayTransfer are essential for matching webhook notifications to your orders. Store these immediately in your database, preferably before the user signs the transaction.
// Good: Store before transaction
const { message, reference } = await createTonPayTransfer(...);
await db.createPayment({ reference, status: "pending" });
return { message };

// Bad: Only storing after successful transaction
const { txResult, reference } = await pay(...);
await db.createPayment({ reference }); // Too late if network fails
Never trust amount values sent from the client. Always validate or generate amounts server-side to prevent manipulation.
// Server-side endpoint
app.post("/api/create-payment", async (req, res) => {
  const { orderId, senderAddr } = req.body;
  
  // Fetch the actual amount from your database
  const order = await db.getOrder(orderId);
  
  // Use the verified amount, not req.body.amount
  const { message } = await createTonPayTransfer({
    amount: order.amount,
    asset: order.currency,
    recipientAddr: "EQC...yourWalletAddress",
    senderAddr,
  });
  
  res.json({ message });
});
Wrap your payment components with React error boundaries to gracefully handle failures.
class PaymentErrorBoundary extends React.Component {
  componentDidCatch(error: Error) {
    console.error("Payment component error:", error);
    // Log to your error tracking service
  }
  
  render() {
    return this.props.children;
  }
}
Payment processing can take several seconds. Provide clear feedback to users during wallet connection and transaction signing.
const [loading, setLoading] = useState(false);

const handlePayment = async () => {
  setLoading(true);
  try {
    await pay(getMessage);
  } finally {
    setLoading(false);
  }
};

return (
  <button onClick={handlePayment} disabled={loading}>
    {loading ? "Processing..." : "Pay Now"}
  </button>
);
Always use environment variables for sensitive data and chain configuration.
const { message } = await createTonPayTransfer(params, {
  chain: process.env.NEXT_PUBLIC_TON_CHAIN as "mainnet" | "testnet",
  apiKey: process.env.TONPAY_API_KEY,
});

Troubleshooting

Ensure your application is wrapped with TonConnectUIProvider:
import { TonConnectUIProvider } from "@tonconnect/ui-react";

function App() {
  return (
    <TonConnectUIProvider manifestUrl="/tonconnect-manifest.json">
      <YourComponents />
    </TonConnectUIProvider>
  );
}
  • Verify the TonConnect manifest URL is accessible and valid - Check browser console for TonConnect initialization errors - Ensure the manifest file is served with correct CORS headers - Test the manifest URL directly in your browser
  • Verify the recipient address is a valid TON address (starts with EQ, UQ, or 0:) - Ensure you’re using the correct address format for your chain (mainnet vs testnet) - Check that the address includes the full base64 format with workchain
Common causes:
  • Invalid API key or missing API key for authenticated endpoints
  • Network connectivity issues
  • Invalid parameter values (negative amounts, invalid addresses)
  • Wrong chain configuration (mainnet vs testnet)
Check the error cause for specific details:
try {
  await createTonPayTransfer(...);
} catch (error) {
  console.error("API Error:", error.cause); // HTTP status text
}
TonConnect may not always throw an error on rejection. Implement timeout handling:
const paymentPromise = pay(getMessage);
const timeoutPromise = new Promise((_, reject) => 
  setTimeout(() => reject(new Error("Transaction timeout")), 60000)
);

await Promise.race([paymentPromise, timeoutPromise]);

API Key Configuration

The TON Pay API key is optional but enables essential merchant features including transaction visibility in the dashboard, webhook notifications, and centralized wallet management. When using useTonPay with server-side message building, optionally include the API key in your backend endpoint:
// Backend endpoint
app.post("/api/create-payment", async (req, res) => {
  const { amount, senderAddr, orderId } = req.body;

  const { message, reference, bodyBase64Hash } = await createTonPayTransfer(
    {
      amount,
      asset: "TON",
      recipientAddr: process.env.MERCHANT_WALLET_ADDRESS!,
      senderAddr,
    },
    {
      chain: "mainnet",
      apiKey: process.env.TONPAY_API_KEY, // Enables dashboard features
    }
  );

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

Testnet Configuration

Before deploying to production, test your payment integration on the TON testnet.

Basic Testnet Setup

Configure your environment to use testnet:
# .env.development
NEXT_PUBLIC_TON_CHAIN=testnet
NEXT_PUBLIC_RECIPIENT_ADDRESS=EQC...TESTNET_ADDRESS
const { txResult, reference } = await pay(async (senderAddr: string) => {
  const result = await createTonPayTransfer(
    {
      amount: 1.0,
      asset: "TON",
      recipientAddr: process.env.NEXT_PUBLIC_RECIPIENT_ADDRESS!,
      senderAddr,
    },
    {
      chain: process.env.TON_CHAIN as "mainnet" | "testnet", // Use testnet for testing
    }
  );

  return {
    message: result.message,
    reference: result.reference,
    bodyBase64Hash: result.bodyBase64Hash,
  };
});
For complete testnet configuration guide, including how to get testnet TON, test jetton transfers, verify transactions, and transition to production, see the Testnet Configuration section.

Next Steps