> ## 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/webhooks",
  "feedback": "Description of the issue"
}
```

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

</AgentInstructions>

# TON Pay webhooks

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

<Aside type="caution" title="Beta version">
  Webhook functionality is in closed beta. Interfaces or behavior may change in the future.
</Aside>

TON Pay uses webhooks to notify applications in real-time about transfer events.

## How webhooks work

1. A transfer is completed on-chain and indexed by TON Pay.
2. TON Pay generates a webhook payload and signs it using [HMAC-SHA256](https://en.wikipedia.org/wiki/HMAC) and the optional API key when it is configured.
3. TON Pay sends a `POST` request to the webhook URL with the signed payload. The request includes the `X-TonPay-Signature` header for verification.
4. Verify the signature and process the event payload. Return a `2xx` status code to acknowledge receipt.

<Aside type="note" title="Important">
  The webhook URL must be publicly accessible and accept `POST` requests from the TON Pay backend.
</Aside>

## Configure the webhook URL

Configure the endpoint in TON Pay Merchant Dashboard.

1. Open the TON Pay Merchant Dashboard and sign in to the merchant account.
2. Navigate to the <kbd>Developer</kbd> section, then select <kbd>Webhooks</kbd>.
3. Enter the webhook URL. In production, the endpoint must use HTTPS. For example, `https://thedomain.com/webhooks/tonpay`.
4. Save the configuration and use the test feature to verify delivery.

<Aside type="note" title="Built-in test feature">
  Use a built-in test feature in the dashboard to send sample webhooks before going live.
</Aside>

## Webhook payload

### Event types

```ts 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 {
  type WebhookPayload,
  type WebhookEventType,
  type TransferCompletedWebhookPayload,
  type TransferRefundedWebhookPayload, // Coming soon
} from "@ton-pay/api";

// Event types
type WebhookEventType =
  | "transfer.completed"
  | "transfer.refunded"; // Coming soon

// Union type for all webhook payloads
type WebhookPayload =
  | TransferCompletedWebhookPayload
  | TransferRefundedWebhookPayload; // Coming soon
```

### `transfer.completed`

```ts 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"]}}
interface TransferCompletedWebhookPayload {
  event: "transfer.completed";
  timestamp: string;
  data: CompletedTonPayTransferInfo; // See API reference
}
```

<Aside type="tip" title="Type safety">
  Webhook types are exported from [`@ton-pay/api`](/ecosystem/ton-pay/api-reference). Use `WebhookPayload` for type safety in webhook handlers.
</Aside>

### Example payloads

Successful transfer:

```json 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"]}}
{
  "event": "transfer.completed",
  "timestamp": "2024-01-15T14:30:00.000Z",
  "data": {
    "amount": "10.5",
    "rawAmount": "10500000000",
    "senderAddr": "<SENDER_ADDR>",
    "recipientAddr": "<RECIPIENT_ADDR>",
    "asset": "TON",
    "assetTicker": "TON",
    "status": "success",
    "reference": "<REFERENCE>",
    "bodyBase64Hash": "<BODY_BASE64_HASH>",
    "txHash": "<TX_HASH>",
    "traceId": "<TRACE_ID>",
    "commentToSender": "Thanks for the purchase",
    "commentToRecipient": "Payment for order #1234",
    "date": "2024-01-15T14:30:00.000Z"
  }
}
```

Failed transfer:

```json 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"]}}
{
  "event": "transfer.completed",
  "timestamp": "2024-01-15T14:35:00.000Z",
  "data": {
    "amount": "10.5",
    "rawAmount": "10500000000",
    "senderAddr": "<SENDER_ADDR>",
    "recipientAddr": "<RECIPIENT_ADDR>",
    "asset": "TON",
    "assetTicker": "TON",
    "status": "failed",
    "reference": "<REFERENCE>",
    "bodyBase64Hash": "<BODY_BASE64_HASH>",
    "txHash": "<TX_HASH>",
    "traceId": "<TRACE_ID>",
    "commentToSender": "Transaction failed",
    "commentToRecipient": "Payment for order #5678",
    "date": "2024-01-15T14:35:00.000Z",
    "errorCode": 36,
    "errorMessage": "Not enough TON"
  }
}
```

Placeholders:

* `<SENDER_ADDR>` - sender wallet address.
* `<RECIPIENT_ADDR>` - recipient wallet address.
* `<REFERENCE>` - transfer reference returned by `createTonPayTransfer`.
* `<BODY_BASE64_HASH>` - Base64 hash of the transfer body.
* `<TX_HASH>` - transaction hash.
* `<TRACE_ID>` - trace identifier.

## Payload fields

<ResponseField name="event" type="string" required>
  Event type. `transfer.completed` indicates that on-chain processing is finished. The transfer may be successful or failed; check `data.status` for the result.
</ResponseField>

<ResponseField name="timestamp" type="string" required>
  [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) timestamp indicating when the event occurred.
</ResponseField>

<ResponseField name="data" type="object" required>
  Transfer details such as amount, addresses, and the transaction hash.

  <Expandable title="data properties">
    <ResponseField name="amount" type="string" required>
      Human-readable payment amount with decimals. For example, 10.5.
    </ResponseField>

    <ResponseField name="rawAmount" type="string" required>
      Amount in base units; nanotons for TON.
    </ResponseField>

    <ResponseField name="senderAddr" type="string" required>
      Sender wallet address on TON blockchain.
    </ResponseField>

    <ResponseField name="recipientAddr" type="string" required>
      Recipient wallet address on TON blockchain.
    </ResponseField>

    <ResponseField name="asset" type="string" required>
      Asset identifier. Use "TON" for TON coin or a jetton master address for tokens.
    </ResponseField>

    <ResponseField name="assetTicker" type="string">
      Human-readable asset ticker. For example, TON or USDT.
    </ResponseField>

    <ResponseField name="status" type="string" required>
      Transfer status: `success` or `failed`. Process only `success` status. Log `failed` status for investigation.
    </ResponseField>

    <ResponseField name="reference" type="string" required>
      Unique transfer reference. Use this to match the webhook with the internal order or transaction.
    </ResponseField>

    <ResponseField name="bodyBase64Hash" type="string" required>
      Base64 hash of the transfer body. Use for advanced verification.
    </ResponseField>

    <ResponseField name="txHash" type="string" required>
      Transaction hash on-chain. Use this to verify the transaction if needed.
    </ResponseField>

    <ResponseField name="traceId" type="string" required>
      Trace ID for tracking the transaction across systems.
    </ResponseField>

    <ResponseField name="commentToSender" type="string">
      Optional note visible to the sender during signing.
    </ResponseField>

    <ResponseField name="commentToRecipient" type="string">
      Optional note visible to the recipient. Use for order IDs or invoice numbers.
    </ResponseField>

    <ResponseField name="date" type="string" required>
      ISO 8601 timestamp of the transfer completion.
    </ResponseField>

    <ResponseField name="errorCode" type="number">
      Error code if the transfer failed.
    </ResponseField>

    <ResponseField name="errorMessage" type="string">
      Error message if the transfer failed.
    </ResponseField>
  </Expandable>
</ResponseField>

## Verify webhook signatures

Every webhook request includes an `X-TonPay-Signature` header containing an HMAC-SHA256 signature. Verify this signature to ensure the request comes from TON Pay.

<Aside type="danger" title="Security requirement">
  In production, webhook requests must be verified using the signature. Without verification, requests can be forged.
</Aside>

```ts 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 { verifySignature, type WebhookPayload } from "@ton-pay/api";

app.post("/webhook", (req, res) => {
  const signature = req.headers["x-tonpay-signature"];

  if (!verifySignature(req.body, signature, process.env.TONPAY_API_SECRET)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const webhookData: WebhookPayload = req.body;

  // Process the webhook in application-specific logic.
  res.status(200).json({ received: true });
});
```

<Aside type="note" title="Important">
  The webhook API secret is available in the Merchant Dashboard under <kbd>Developer</kbd> → <kbd>Webhooks</kbd>. Store it securely and use it only on the server.
</Aside>

## Validate webhook requests

Validate fields against the expected transaction data before marking an order as paid.

<Aside type="note">
  Return a `2xx` response for validation failures, such as an amount mismatch or an unknown order, to prevent retries. Log these issues for further investigation.
</Aside>

1. Verify the `X-TonPay-Signature` header and reject invalid requests.

   ```ts 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 (!verifySignature(payload, signature, apiKey)) { // apiKey is optional and used when configured
     return res.status(401).json({ error: "Invalid signature" });
   }
   ```

2. Verify the event type.

   ```ts 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 (webhookData.event !== "transfer.completed") {
     return res.status(400).json({ error: "Unexpected event type" });
   }
   ```

3. Check that the reference matches a transaction created earlier.

   ```ts 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 order = await db.getOrderByReference(webhookData.data.reference);
   if (!order) {
     console.error("Order not found:", webhookData.data.reference);
     return res.status(200).json({ received: true }); // Acknowledge to prevent retry
   }
   ```

4. Verify the amount matches the expected payment amount.

   ```ts 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 (parseFloat(webhookData.data.amount) !== order.expectedAmount) {
     console.error("Amount mismatch:", {
       expected: order.expectedAmount,
       received: webhookData.data.amount,
     });
     return res.status(200).json({ received: true }); // Acknowledge to prevent retry
   }
   ```

5. Verify the payment currency or token.

   ```ts 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 (webhookData.data.asset !== order.expectedAsset) {
     console.error("Asset mismatch:", {
       expected: order.expectedAsset,
       received: webhookData.data.asset,
     });
     return res.status(200).json({ received: true }); // Acknowledge to prevent retry
   }
   ```

6. Check status and process only successful transfers.

   ```ts 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 (webhookData.data.status !== "success") {
     await db.markOrderAsFailed(order.id, webhookData.data.txHash);
     return res.status(200).json({ received: true }); // Acknowledge to prevent retry
   }
   ```

7. Prevent duplicate processing using the reference.

   ```ts 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 (order.status === "completed") {
     return res.status(200).json({ received: true, duplicate: true });
   }
   ```

### Complete validation example

```ts 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 { verifySignature, type WebhookPayload } from "@ton-pay/api";

app.post("/webhooks/tonpay", async (req, res) => {
  try {
    // 1. Verify signature
    const signature = req.headers["x-tonpay-signature"] as string;

    if (!verifySignature(req.body, signature, process.env.TONPAY_API_SECRET!)) {
      console.error("Invalid webhook signature");
      return res.status(401).json({ error: "Invalid signature" });
    }

    const webhookData: WebhookPayload = req.body;

    // 2. Verify event type
    if (webhookData.event !== "transfer.completed") {
      return res.status(400).json({ error: "Unexpected event type" });
    }

    // 3. Find the order
    const order = await db.getOrderByReference(webhookData.data.reference);
    if (!order) {
      console.error("Order not found:", webhookData.data.reference);
      return res.status(200).json({ received: true }); // Acknowledge to prevent retry
    }

    // 4. Check for duplicate processing
    if (order.status === "completed") {
      console.log("Order already processed:", order.id);
      return res.status(200).json({ received: true, duplicate: true });
    }

    // 5. Verify transfer status
    if (webhookData.data.status !== "success") {
      await db.updateOrder(order.id, {
        status: "failed",
        txHash: webhookData.data.txHash,
        failureReason: "Transfer failed on blockchain",
      });
      return res.status(200).json({ received: true });
    }

    // 6. CRITICAL: Verify amount
    const receivedAmount = parseFloat(webhookData.data.amount);
    if (receivedAmount !== order.expectedAmount) {
      console.error("Amount mismatch:", {
        orderId: order.id,
        expected: order.expectedAmount,
        received: receivedAmount,
      });
      return res.status(200).json({ received: true }); // Acknowledge to prevent retry
    }

    // 7. CRITICAL: Verify asset/currency
    const expectedAsset = order.expectedAsset || "TON";
    if (webhookData.data.asset !== expectedAsset) {
      console.error("Asset mismatch:", {
        orderId: order.id,
        expected: expectedAsset,
        received: webhookData.data.asset,
      });
      return res.status(200).json({ received: true }); // Acknowledge to prevent retry
    }

    // All validations passed - acknowledge receipt
    res.status(200).json({ received: true });

    // Process order asynchronously
    await processOrderCompletion(order.id, {
      txHash: webhookData.data.txHash,
      senderAddr: webhookData.data.senderAddr,
      completedAt: webhookData.timestamp,
    });
  } catch (error) {
    console.error("Webhook processing error:", error);
    return res.status(500).json({ error: "Internal server error" });
  }
});
```

## Retry behavior

TON Pay retries failed webhook deliveries.

<CardGroup cols={2}>
  <Card title="Retry attempts" icon="rotate">
    Up to 3 automatic retries with exponential backoff.
  </Card>

  <Card title="Retry delays" icon="clock">
    1s, 5s, and 15s.
  </Card>
</CardGroup>

### Delivery outcomes

* Success responses: `2xx`. No retry; webhook marked as delivered.
* All other responses: `4xx`, `5xx`, or network errors. Automatic retry with exponential backoff.

<Aside type="note">
  Return a `2xx` status code only after validation and any required order updates complete, including cases where validation fails but the event is logged and marked as handled. Returning success before validation finishes can result in missed or duplicated processing.
</Aside>

## Best practices

* Validate the following fields before marking an order as paid:

  * Signature: authentication;
  * Reference: the transaction exists in the system;
  * Amount: the expected payment is matched;
  * Asset: correct currency or token;
  * Wallet ID: correct receiving wallet;
  * Status: the transaction succeeded.

  ```ts 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"]}}
  // Avoid: trust without verification
  await markOrderAsPaid(webhook.data.reference);

  // Prefer: validate and then process
  if (isValidWebhook(webhook, order)) {
    await markOrderAsPaid(order.id);
  }
  ```

* Return `2xx` only after successful validation, then process the webhook to avoid timeouts.

  ```ts 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"]}}
  app.post("/webhook", async (req, res) => {
    // Validate signature first
    if (!verifyWebhookSignature(...)) {
      return res.status(401).json({ error: "Invalid signature" });
    }

    // Quick validations
    if (!isValidWebhook(req.body)) {
      return res.status(400).json({ error: "Invalid webhook data" });
    }

    // Acknowledge after validation
    res.status(200).json({ received: true });

    // Process in background
    processWebhookAsync(req.body).catch(console.error);
  });
  ```

* Implement idempotency. Webhooks may be delivered more than once. Use the `reference` field to prevent duplicate processing.

  ```ts 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 processWebhook(payload: WebhookPayload) {
    const order = await db.getOrder(payload.reference);

    // Check if already processed
    if (order.status === "completed") {
      console.log("Order already completed:", payload.reference);
      return; // Skip processing
    }

    // Process and update status atomically
    await db.transaction(async (tx) => {
      await tx.updateOrder(order.id, { status: "completed" });
      await tx.createPaymentRecord(payload);
    });
  }
  ```

* Log all webhook attempts. Log each request for debugging and security auditing.

  ```ts 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"]}}
  await logger.info("Webhook received", {
    timestamp: new Date(),
    signature: req.headers["x-tonpay-signature"],
    reference: payload.data.reference,
    event: payload.event,
    amount: payload.data.amount,
    asset: payload.data.asset,
    status: payload.data.status,
    validationResult: validationResult,
  });
  ```

* To receive webhooks in production, ensure corresponding endpoints use HTTPS. Regular HTTP endpoints would be ignored by TON Pay.

* Monitor delivery failures. Set up alerts and review webhook delivery logs in the Merchant Dashboard

* Store transaction hashes. Save `txHash` to support on-chain verification for disputes.

  ```ts 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"]}}
  await db.updateOrder(order.id, {
    status: "completed",
    txHash: webhookData.data.txHash,
    senderAddr: webhookData.data.senderAddr,
    completedAt: webhookData.timestamp,
  });
  ```

## Troubleshooting

<Accordion title="If webhooks are not received">
  1. Verify the webhook URL configured in the Merchant Dashboard.
  2. Ensure the endpoint is publicly reachable and not blocked by a firewall.
  3. Use the dashboard test feature to send a sample webhook.
  4. Review webhook attempt logs in the Merchant Dashboard.
  5. Ensure the endpoint returns a `2xx` status code for valid requests.
  6. Ensure the endpoint responds within 10 seconds to avoid timeouts.
</Accordion>

<Accordion title="If signature verification fails">
  1. Use the optional API key from the Merchant Dashboard at <kbd>Developer</kbd> → <kbd>API</kbd> when webhook signing is configured.
  2. Compute the HMAC over the raw JSON string.
  3. Do not modify the request body before verification.
  4. Verify the header name is `x-tonpay-signature` in lowercase.
  5. Use the `HMAC-SHA256` algorithm.
  6. Ensure the signature value starts with `sha256=`.
</Accordion>

<Accordion title="If amount or asset mismatch, possible causes include">
  1. Wrong order lookup or reference mapping.
  2. Price changed after order creation.
  3. Currency mismatch between order and transfer.
  4. Potential attack or spoofed request.
</Accordion>

<Accordion title="If duplicate webhook processing">
  1. Implement idempotency using the `reference` field.
  2. Use database transactions to prevent race conditions.
  3. Check order status before processing.

  ```ts 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 (order.status === "completed") {
    return; // Already processed
  }
  ```
</Accordion>

<Accordion title="If timeout errors occur">
  1. Respond within 10 seconds.
  2. Process webhooks asynchronously after quick validation.
  3. Return `2xx` after validation passes.
</Accordion>

## Next steps

<CardGroup cols={2}>
  <Card title="Build a transfer" icon="paper-plane" href="/ecosystem/ton-pay/payment-integration/transfer">
    Create a TON Pay transfer message.
  </Card>

  <Card title="Check status and retrieve info" icon="magnifying-glass" href="/ecosystem/ton-pay/payment-integration/status-info">
    Query transfer status and details.
  </Card>
</CardGroup>
