Skip to main content
TON Pay uses webhooks to notify your application in real-time about transfer events. When a payment is completed, TON Pay sends an HTTP POST request to your configured webhook URL with event details.

πŸ”” How Webhooks Work

When a transfer is completed on the blockchain, TON Pay automatically sends a webhook notification to your configured endpoint. The transfer may be successful or failed - check the status field in the payload:
1

Event occurs

A transfer is completed on the blockchain and indexed by TON Pay.
2

Webhook triggered

TON Pay generates a webhook payload and signs it with your API key using HMAC-SHA256.
3

HTTP POST sent

TON Pay sends a POST request to your webhook URL with the signed payload.
The request includes an X-TonPay-Signature header for verification.
4

Your endpoint responds

Your server receives the webhook, verifies the signature, and processes the event.
Return a 2xx status code to acknowledge receipt.
Important: Webhooks are sent from the TON Pay backend. Ensure your webhook URL is publicly accessible and can receive POST requests.

πŸ”§ Configuration

Configure Webhook URL via Merchant Dashboard

Configure your webhook endpoint through the TON Pay Merchant Dashboard:
1

Login to Merchant Dashboard

Access the TON Pay merchant portal at tonpay.tech/dashboard
2

Navigate to Developer

Go to Developer β†’ Webhooks section
3

Enter Webhook URL

Enter your webhook endpoint URL (must be HTTPS in production) https://yourdomain.com/webhooks/tonpay
4

Save and Test

Save the configuration and use the test feature to verify your endpoint works correctly
Use the built-in test feature in the dashboard to send sample webhooks before going live.

πŸ” Security & Signature Verification

Verify Every Webhook Request

Every webhook request includes an X-TonPay-Signature header containing an HMAC-SHA256 signature. You must verify this signature to ensure the request came from TON Pay.
Critical: Never skip signature verification in production. Unverified webhooks could be forged by malicious actors attempting to manipulate your payment flow.
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, YOUR_API_SECRET)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const webhookData: WebhookPayload = req.body;

  // Process the webhook...
  res.status(200).json({ received: true });
});
Your webhook API secret is available in the Merchant Dashboard β†’ Developer β†’ Webhooks section. Keep it secure and never expose it in client-side code.

πŸ“¦ Webhook Payload

Event Types

import {
  type WebhookPayload,
  type WebhookEventType,
  type TransferCompletedWebhookPayload,
  type TransferRefundedWebhookPayload, // Coming Soon
} from "@ton-pay/api";

// Event types: "transfer.completed" | "transfer.refunded" (Coming Soon)
type WebhookEventType = "transfer.completed" | "transfer.refunded";

// Union type for all webhook payloads
type WebhookPayload =
  | TransferCompletedWebhookPayload
  | TransferRefundedWebhookPayload; // Coming Soon - data structure to be defined

transfer.completed Event

interface TransferCompletedWebhookPayload {
  event: "transfer.completed";
  timestamp: string;
  data: CompletedTonPayTransferInfo; // See full type definition in API Reference
}
All webhook types are available in @ton-pay/api package. Import WebhookPayload for type safety in your webhook handlers.

Example Payload

Successful Transfer

{
  "event": "transfer.completed",
  "timestamp": "2024-01-15T14:30:00.000Z",
  "data": {
    "amount": "10.5",
    "rawAmount": "10500000000",
    "senderAddr": "EQC...SENDER",
    "recipientAddr": "EQC...RECIPIENT",
    "asset": "TON",
    "assetTicker": "TON",
    "status": "success",
    "reference": "0x1234567890abcdef...fedcba0987654321",
    "bodyBase64Hash": "aGVsbG8gd29ybGQ...aGVsbG8gd29ybGQ=",
    "txHash": "dGVzdCB0eGhhc2g...dGVzdCB0eGhhc2g=",
    "traceId": "dHJhY2VfaWRfMTI...Y2VfaWRfMTIzNDU=",
    "commentToSender": "Thank you for your purchase",
    "commentToRecipient": "Payment for order #1234",
    "date": "2024-01-15T14:30:00.000Z"
  }
}

Failed Transfer

{
  "event": "transfer.completed",
  "timestamp": "2024-01-15T14:35:00.000Z",
  "data": {
    "amount": "10.5",
    "rawAmount": "10500000000",
    "senderAddr": "EQC...SENDER",
    "recipientAddr": "EQC...RECIPIENT",
    "asset": "TON",
    "assetTicker": "TON",
    "status": "failed",
    "reference": "0xfedcba0987654321...1234567890abcdef",
    "bodyBase64Hash": "ZmFpbGVkIGhhc2g...ZmFpbGVkIGhhc2g=",
    "txHash": "ZmFpbGVkIHR4aGE...ZmFpbGVkIHR4aGE=",
    "traceId": "dHJhY2VfaWRfNDU...Y2VfaWRfNDU2Nzg=",
    "commentToSender": "Transaction failed",
    "commentToRecipient": "Payment for order #5678",
    "date": "2024-01-15T14:35:00.000Z",
    "errorCode": 36,
    "errorMessage": "Not enough TON"
  }
}

πŸ“‹ Payload Fields

event
string
required
Event type. Currently only transfer.completed is supported. Note: transfer.completed means the transfer processing is finished - it may be successful or failed. Always check the status field in the data to determine the actual result.
Coming Soon: Additional event transfer.refunded will be available in future updates.
timestamp
string
required
ISO 8601 timestamp when the event occurred.
data
object
required
Transfer details including amount, addresses, and transaction hash.

βœ… Proper Webhook Validation

Critical Validation Steps

When processing a webhook, you must validate all critical fields against your expected transaction data:
Always return 200 OK for validation failures (amount mismatch, unknown order, etc.) to prevent retries, but log the issues for investigation.
1

1. Verify Signature

Always verify the X-TonPay-Signature header first. Reject the request immediately if signature is invalid.
if (!verifyWebhookSignature(payload, signature, apiKey)) {
  return res.status(401).json({ error: 'Invalid signature' });
}
2

2. Check Event Type

Verify the event type is what you expect.
if (webhookData.event !== 'transfer.completed') {
  return res.status(400).json({ error: 'Unexpected event type' });
}
3

3. Verify Reference

Check that the reference matches a transaction you created.
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

4. Validate Amount

Critical: Verify the amount matches your expected payment amount.
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

5. Validate Asset

Verify the payment was made in the correct currency/token.
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

6. Check Status

Only process successful transactions.
if (webhookData.data.status !== 'success') {
  await db.markOrderAsFailed(order.id, webhookData.data.txHash);
  return res.status(200).json({ received: true }); // Acknowledge but don't process
}
7

7. Check for Duplicates

Prevent duplicate processing using the reference.
if (order.status === 'completed') {
  // Already processed, just acknowledge
  return res.status(200).json({ received: true, duplicate: true });
}

Complete Validation Example

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 immediately
    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 Logic

TON Pay automatically retries failed webhook deliveries to ensure reliable notification:

Retry Attempts

Up to 3 automatic retries with exponential backoff

Retry Delays

1 second β†’ 5 seconds β†’ 15 seconds

Retry Behavior

  • Success (2xx status): No retry, webhook marked as delivered
  • All other responses (4xx, 5xx, network errors): Automatic retry with exponential backoff
Return a 2xx status code only after successfully processing the webhook. Returning success prematurely may result in missed events.

πŸ’‘ Best Practices

Never trust webhook data without validation. Always verify:
  • Signature (authentication)
  • Reference (transaction exists in your system)
  • Amount (matches expected payment)
  • Asset (correct currency/token)
  • Wallet ID (correct receiving wallet)
  • Status (transaction succeeded)
// Bad: Trust without verification
await markOrderAsPaid(webhook.reference);

// Good: Validate everything
if (isValidWebhook(webhook, order)) {
  await markOrderAsPaid(order.id);
}
Acknowledge receipt immediately, then process in the background to avoid timeouts.
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 immediately
  res.status(200).json({ received: true });
  
  // Process in background
  processWebhookAsync(req.body).catch(console.error);
});
Webhooks may be delivered more than once. Use the reference field to prevent duplicate processing.
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);
  });
}
Keep comprehensive logs for debugging and security auditing.
await logger.info('Webhook received', {
  timestamp: new Date(),
  signature: req.headers['x-tonpay-signature'],
  reference: payload.reference,
  event: payload.event,
  amount: payload.transfer.amount,
  asset: payload.transfer.asset,
  status: payload.transfer.status,
  validationResult: validationResult
});
Always use HTTPS endpoints for webhooks in production. TON Pay may reject HTTP endpoints for security.
Set up alerts for webhook failures. Check the Merchant Dashboard for delivery logs and investigate any failures promptly.
Save the txHash from webhooks to enable on-chain verification if disputes arise.
await db.updateOrder(order.id, {
  status: 'completed',
  txHash: webhookData.data.txHash,
  senderAddr: webhookData.data.senderAddr,
  completedAt: webhookData.timestamp
});

πŸ” Troubleshooting

  • Check webhook URL in Merchant Dashboard is correct and accessible
  • Verify endpoint is publicly accessible and not blocked by firewall
  • Test using Dashboard test feature to send sample webhook
  • Check webhook attempt logs in the Merchant Dashboard
  • Ensure endpoint returns 2xx status for valid requests
  • Verify no request timeout (endpoint must respond within 10 seconds)
  • Get API key from Merchant Dashboard β†’ Developer β†’ API tab - Ensure payload is raw JSON string when computing HMAC - Don’t modify the request body before verification - Check header name is exactly x-tonpay-signature (lowercase) - Verify HMAC algorithm is SHA-256 - Check signature format starts with sha256=
This is a security-critical error. Possible causes: - Wrong order looked up - verify reference matching logic - Price changed - ensure prices are locked at order creation - Currency confusion - verify you’re comparing same currency - Potential attack - log and alert security team Never fulfill orders with mismatched amounts or assets!
  • Expected behavior - webhooks can be delivered multiple times
  • Implement idempotency using reference field
  • Use database transactions to prevent race conditions
  • Check order status before processing
if (order.status === 'completed') {
  return; // Already processed
}
  • Respond within 10 seconds to avoid timeout
  • Process webhooks asynchronously after quick validation
  • Return 2xx immediately after validation passes
  • Use background jobs for time-consuming operations

πŸ”— Next Steps