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

<AgentInstructions>

## Submitting Feedback

If you encounter incorrect, outdated, or confusing documentation on this page, submit feedback:

POST https://docs.ton.org/feedback

```json
{
  "path": "/ecosystem/ton-pay/payment-integration/payments-tonconnect",
  "feedback": "Description of the issue"
}
```

Only submit feedback when you have something specific and actionable to report.

</AgentInstructions>

# How to send payments using TON Connect

export const Aside = ({type = "note", title = "", icon = "", iconType = "regular", children}) => {
  const asideVariants = ["note", "tip", "caution", "danger"];
  const asideComponents = {
    note: {
      outerStyle: "border-sky-500/20 bg-sky-50/50 dark:border-sky-500/30 dark:bg-sky-500/10",
      innerStyle: "text-sky-900 dark:text-sky-200",
      calloutType: "note",
      icon: <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor" xmlns="http://www.w3.org/2000/svg" className="w-4 h-4 text-sky-500" aria-label="Note">
          <path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.3C10.14 1.3 12.7 3.86 12.7 7C12.7 10.14 10.14 12.7 7 12.7C5.48908 12.6974 4.0408 12.096 2.97241 11.0276C1.90403 9.9592 1.30264 8.51092 1.3 7C1.3 3.86 3.86 1.3 7 1.3ZM7 0C3.14 0 0 3.14 0 7C0 10.86 3.14 14 7 14C10.86 14 14 10.86 14 7C14 3.14 10.86 0 7 0ZM8 3H6V8H8V3ZM8 9H6V11H8V9Z"></path>
        </svg>
    },
    tip: {
      outerStyle: "border-emerald-500/20 bg-emerald-50/50 dark:border-emerald-500/30 dark:bg-emerald-500/10",
      innerStyle: "text-emerald-900 dark:text-emerald-200",
      calloutType: "tip",
      icon: <svg width="11" height="14" viewBox="0 0 11 14" fill="currentColor" xmlns="http://www.w3.org/2000/svg" className="text-emerald-600 dark:text-emerald-400/80 w-3.5 h-auto" aria-label="Tip">
          <path d="M3.12794 12.4232C3.12794 12.5954 3.1776 12.7634 3.27244 12.907L3.74114 13.6095C3.88471 13.8248 4.21067 14 4.46964 14H6.15606C6.41415 14 6.74017 13.825 6.88373 13.6095L7.3508 12.9073C7.43114 12.7859 7.49705 12.569 7.49705 12.4232L7.50055 11.3513H3.12521L3.12794 12.4232ZM5.31288 0C2.52414 0.00875889 0.5 2.26889 0.5 4.78826C0.5 6.00188 0.949566 7.10829 1.69119 7.95492C2.14321 8.47011 2.84901 9.54727 3.11919 10.4557C3.12005 10.4625 3.12175 10.4698 3.12261 10.4771H7.50342C7.50427 10.4698 7.50598 10.463 7.50684 10.4557C7.77688 9.54727 8.48281 8.47011 8.93484 7.95492C9.67728 7.13181 10.1258 6.02703 10.1258 4.78826C10.1258 2.15486 7.9709 0.000106649 5.31288 0ZM7.94902 7.11267C7.52078 7.60079 6.99082 8.37878 6.6077 9.18794H4.02051C3.63739 8.37878 3.10743 7.60079 2.67947 7.11294C2.11997 6.47551 1.8126 5.63599 1.8126 4.78826C1.8126 3.09829 3.12794 1.31944 5.28827 1.3126C7.2435 1.3126 8.81315 2.88226 8.81315 4.78826C8.81315 5.63599 8.50688 6.47551 7.94902 7.11267ZM4.87534 2.18767C3.66939 2.18767 2.68767 3.16939 2.68767 4.37534C2.68767 4.61719 2.88336 4.81288 3.12521 4.81288C3.36705 4.81288 3.56274 4.61599 3.56274 4.37534C3.56274 3.6515 4.1515 3.06274 4.87534 3.06274C5.11719 3.06274 5.31288 2.86727 5.31288 2.62548C5.31288 2.38369 5.11599 2.18767 4.87534 2.18767Z"></path>
        </svg>
    },
    caution: {
      outerStyle: "border-amber-500/20 bg-amber-50/50 dark:border-amber-500/30 dark:bg-amber-500/10",
      innerStyle: "text-amber-900 dark:text-amber-200",
      calloutType: "warning",
      icon: <svg className="flex-none w-5 h-5 text-amber-400 dark:text-amber-300/80" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-label="Warning">
          <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
        </svg>
    },
    danger: {
      outerStyle: "border-red-500/20 bg-red-50/50 dark:border-red-500/30 dark:bg-red-500/10",
      innerStyle: "text-red-900 dark:text-red-200",
      calloutType: "danger",
      icon: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" className="text-red-600 dark:text-red-400/80 w-4 h-4" aria-label="Danger">
          <path d="M17.1 292c-12.9-22.3-12.9-49.7 0-72L105.4 67.1c12.9-22.3 36.6-36 62.4-36l176.6 0c25.7 0 49.5 13.7 62.4 36L494.9 220c12.9 22.3 12.9 49.7 0 72L406.6 444.9c-12.9 22.3-36.6 36-62.4 36l-176.6 0c-25.7 0-49.5-13.7-62.4-36L17.1 292zm41.6-48c-4.3 7.4-4.3 16.6 0 24l88.3 152.9c4.3 7.4 12.2 12 20.8 12l176.6 0c8.6 0 16.5-4.6 20.8-12L453.4 268c4.3-7.4 4.3-16.6 0-24L365.1 91.1c-4.3-7.4-12.2-12-20.8-12l-176.6 0c-8.6 0-16.5 4.6-20.8 12L58.6 244zM256 128c13.3 0 24 10.7 24 24l0 112c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-112c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"></path>
        </svg>
    }
  };
  let variant = type;
  let gotInvalidVariant = false;
  if (!asideVariants.includes(type)) {
    gotInvalidVariant = true;
    variant = "danger";
  }
  const iconVariants = ["regular", "solid", "light", "thin", "sharp-solid", "duotone", "brands"];
  if (!iconVariants.includes(iconType)) {
    iconType = "regular";
  }
  return <>
      <div className={`callout my-4 px-5 py-4 overflow-hidden rounded-2xl flex gap-3 border ${asideComponents[variant].outerStyle}`} data-callout-type={asideComponents[variant].calloutType}>
        <div className="mt-0.5 w-4" data-component-part="callout-icon">
          {}
          {icon === "" ? asideComponents[variant].icon : <Icon icon={icon} iconType={iconType} size={14} />}
        </div>
        <div className={`text-sm prose min-w-0 w-full ${asideComponents[variant].innerStyle}`} data-component-part="callout-content">
          {gotInvalidVariant ? <p>
              <span className="font-bold">
                Invalid <code>type</code> passed!
              </span>
              <br />
              <span className="font-bold">Received: </span>
              {type}
              <br />
              <span className="font-bold">Expected one of: </span>
              {asideVariants.join(", ")}
            </p> : <>
              {title && <p className="font-bold">{title}</p>}
              {children}
            </>}
        </div>
      </div>
    </>;
};

Use TON Connect native `sendTransaction` method when:

* The application manages wallet connection UI and transaction flow directly.
* TON Connect is already used elsewhere in the application.
* Access to TON Connect-specific APIs is required, such as status change listeners or custom wallet adapters.

## Integration overview

Direct TON Connect integration follows these steps:

1. Configure the TON Connect UI provider with the application manifest.
2. Manage wallet connection state and provide UI for users to connect the wallets.
3. Create a transaction message on the client or backend using `createTonPayTransfer`.
4. Send the transaction using TON Connect's `sendTransaction` method.
5. Observe connection state changes and transaction status updates.

## React implementation

### Set up application

Wrap the application with the TON Connect UI provider:

```tsx theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
import { TonConnectUIProvider } from "@tonconnect/ui-react";

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

The manifest URL must be publicly accessible and served over HTTPS in production. Replace `<APP_URL>` with the public HTTPS origin that serves `tonconnect-manifest.json`. [The manifest file](/ecosystem/ton-connect/manifest) provides application metadata required for wallet identification.

### Payment component

```tsx expandable theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
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: "<RECIPIENT_WALLET_ADDRESS>",
          senderAddr: address,
          commentToSender: `Payment for Order ${orderId}`,
          commentToRecipient: `Order ${orderId}`,
        },
        {
          chain: "testnet",
          // Optional API key can be used client-side
          apiKey: "<TONPAY_API_KEY>", // optional
        }
      );

      // 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>
  );
}
```

### Manage connection state

Listen for wallet connection status changes using the TON Connect UI API:

```tsx theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
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 the server to centralize tracking and validation.

### Backend endpoint

```typescript expandable theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
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: "<RECIPIENT_WALLET_ADDRESS>",
        senderAddr,
        commentToSender: `Payment for Order ${order.id}`,
        commentToRecipient: `Order ${order.id} - ${order.description}`,
      },
      {
        chain: "testnet",
        apiKey: "TONPAY_API_KEY", // optional
      }
    );

    // 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

```tsx expandable theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
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 TON Connect SDK directly:

```javascript expandable theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
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: "<RECIPIENT_WALLET_ADDRESS>",
        senderAddr: wallet.account.address,
        commentToSender: `Order ${orderId}`,
      },
      { chain: "testnet" }
    );

    // 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 these fields:

<ResponseField name="address" type="string" required>
  Recipient wallet address in [user-friendly format](/foundations/addresses/formats).
</ResponseField>

<ResponseField name="amount" type="string" required>
  Amount to send in nanotons.
</ResponseField>

<ResponseField name="payload" type="string" required>
  Base64-encoded message payload containing transfer details and tracking information.
</ResponseField>

### Transaction options

<ResponseField name="validUntil" type="number" required>
  Unix timestamp indicating when the transaction expires. Typically is 5 minutes.

  ```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
  validUntil: Math.floor(Date.now() / 1000) + 300
  ```
</ResponseField>

<ResponseField name="from" type="string" required>
  Sender's wallet address. Must match the connected wallet address.
</ResponseField>

<ResponseField name="network" type="string">
  Network identifier. Usually omitted as it is inferred from the connected wallet.
</ResponseField>

## Error handling

```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
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("Connect the 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.

  ```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
  const wallet = tonConnectUI.wallet;

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

  // Proceed with transaction
  ```

* Use a reasonable `validUntil` value, typically 5 minutes, to prevent stale transactions while allowing enough time for user confirmation.

  ```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
  const validUntil = Math.floor(Date.now() / 1000) + 300; // 5 minutes
  ```

* Ensure the sender address from TON Connect matches the format expected by the backend. Use the [user-friendly format](/foundations/addresses/formats) consistently.

  ```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
  const address = useTonAddress(true); // true = user-friendly format
  ```

* Always persist `reference` and `bodyBase64Hash` before sending the transaction. This allows payment reconciliation through webhooks even if the client flow fails after submission.

  ```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
  // 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
  ```

* Implement connection state listeners to update the UI and handle wallet disconnections.

  ```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
  useEffect(() => {
      const unsubscribe = tonConnectUI.onStatusChange((wallet) => {
          if (wallet) {
              setConnectedWallet(wallet.account.address);
          } else {
              setConnectedWallet(null);
          }
      });

      return unsubscribe;
  }, [tonConnectUI]);
  ```

* Handle transaction rejection explicitly. Treat user rejection as a normal cancellation, not as an error.

  ```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
  try {
    await tonConnectUI.sendTransaction(transaction);
  } 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

<Accordion title="If a transaction fails with Wallet is not connected">
  Ensure the wallet is connected before calling `sendTransaction`:

  ```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
  if (!tonConnectUI.wallet) {
    await tonConnectUI.connectWallet();
    // Wait for connection before proceeding
  }
  ```

  Add a connection state check:

  ```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
  const isConnected = tonConnectUI.wallet !== null;
  ```
</Accordion>

<Accordion title="If sendTransaction throws Invalid address format">
  1. Ensure the `from` address matches the connected wallet address.
  2. Verify that the recipient address is [a valid TON address](/foundations/addresses/formats).
  3. Use the wallet address provided by the SDK to avoid format mismatches:

  ```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
  const address = useTonAddress(true); // Ensure consistent format
  ```
</Accordion>

<Accordion title="If a transaction expires before the user signs it">
  The `validUntil` timestamp may be too short. Increase the validity period to give the user more time to confirm the transaction:

  ```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
  // Increase from 5 to 10 minutes if needed
  validUntil: Math.floor(Date.now() / 1000) + 600
  ```
</Accordion>

<Accordion title="If the manifest URL fails to load">
  Check for the following common issues:

  * The URL is not publicly accessible.
  * [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) headers are not configured correctly.
  * The manifest JSON is malformed.
  * The URL is not HTTPS; required in production.

  Open the manifest URL directly in a browser to verify that it is accessible.
</Accordion>

<Accordion title="If onStatusChange is not triggered">
  Ensure that the status change subscription is created once and remains active for the lifetime of the component.

  ```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
  useEffect(() = >{
    const unsubscribe = tonConnectUI.onStatusChange(handleWalletChange);
    return () = >unsubscribe(); // Clean up subscription
  },
  [tonConnectUI]);
  ```
</Accordion>

<Accordion title="If multiple wallet connection prompts appear">
  `connectWallet()` may be called more than once. Track the connection state:

  ```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
  const[isConnecting, setIsConnecting] = useState(false);

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

## Optional API key configuration

When using TON Connect with server-side message building, [the optional API key can be included](/ecosystem/ton-pay/payment-integration/payments-tonconnect#api-key-configuration) in the backend:

```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
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: "<RECIPIENT_WALLET_ADDRESS>",
      senderAddr: "<SENDER_WALLET_ADDRESS>",
    },
    {
      chain: "testnet",
      apiKey: "TONPAY_API_KEY", // optional
    }
  );

  await db.payments.create({ orderId, reference, bodyBase64Hash });
  res.json({ message });
});
```

## Testnet configuration

<Aside type="danger" title="Funds at risk">
  Running tests on mainnet can result in irreversible loss of real TON. Always use `chain: "testnet"` and testnet wallet addresses during development. Verify the network before switching to mainnet.
</Aside>

### Set up testnet

Configure the environment to use testnet:

```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
# .env.development
TON_CHAIN=testnet
MERCHANT_WALLET_ADDRESS=TESTNET_ADDRESS
```

```tsx theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
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: "<RECIPIENT_WALLET_ADDRESS>",
        senderAddr: "<SENDER_WALLET_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>;
}
```

## Next steps

<CardGroup cols={2}>
  <Card title="Webhook integration" icon="webhook" href="/ecosystem/ton-pay/payment-integration/transfer">
    Receive real-time notifications when payments complete.
  </Card>

  <Card title="Transaction status" icon="magnifying-glass" href="/ecosystem/ton-pay/payment-integration/status-info">
    Query payment status using reference or body hash.
  </Card>
</CardGroup>
