Webhooks

Receive real-time notifications when payment events occur. All webhooks are signed with HMAC-SHA256 for security.

HMAC Signed

Verify authenticity

Auto Retry

5 attempts on failure

Real-time

Instant delivery

Setting Up Webhooks

  1. 1

    Configure Webhook URL

    Set your webhook URL in the merchant dashboard or include it in the payment request.

  2. 2

    Store Your Secret

    Keep your API secret secure - you'll use it to verify webhook signatures.

  3. 3

    Implement Handler

    Create an endpoint that verifies the signature and processes the event.

Verifying Signatures

All webhooks include a signature header. Always verify this signature before processing:

Node.js Example
const crypto = require('crypto');

function verifyWebhook(req, webhookSecret) {
  const signature = req.headers['x-pulse2pay-signature'];
  const timestamp = req.headers['x-pulse2pay-timestamp'];
  // Use raw body string for signature verification
  const body = req.rawBody || JSON.stringify(req.body);

  // Check timestamp is within 5 minutes
  const now = Date.now();
  if (Math.abs(now - parseInt(timestamp)) > 300000) {
    throw new Error('Webhook timestamp too old');
  }

  // Verify signature (webhook uses: timestamp + "." + body)
  // Note: This is different from API signature format!
  const hmac = crypto.createHmac('sha256', webhookSecret);
  hmac.update(timestamp + '.' + body);
  const expectedSignature = hmac.digest('hex');

  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )) {
    throw new Error('Invalid webhook signature');
  }

  return true;
}

Important: Webhook signatures use a different format than API signatures. Webhook: timestamp + "." + body. Always verify the signature before processing webhook events.

Webhook Events

payment.created

A new payment has been created

Example Payload

{
  "id": "evt_a1b2c3d4_1705078200000",
  "type": "payment.created",
  "createdAt": "2025-01-12T15:00:00.000Z",
  "data": {
    "paymentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "pending",
    "amount": "100.50",
    "currency": "USDT",
    "network": "TRON",
    "tokenStandard": "TRC20",
    "generatedAddress": "TXyz...abc123",
    "generatedTokenAccount": null,
    "addressMode": "PER_PAYMENT",
    "merchantUserId": null,
    "expectedAmount": "100.50",
    "metadata": { "orderId": "1001" },
    "createdAt": "2025-01-12T15:00:00.000Z",
    "expiresAt": "2025-01-12T15:30:00.000Z"
  }
}
payment.pending

Payment is pending blockchain confirmations

Example Payload

{
  "id": "evt_a1b2c3d4_1705078300000",
  "type": "payment.pending",
  "createdAt": "2025-01-12T15:01:00.000Z",
  "data": {
    "paymentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "pending",
    "amount": "100.50",
    "currency": "USDT",
    "network": "TRON",
    "tokenStandard": "TRC20",
    "generatedAddress": "TXyz...abc123",
    "generatedTokenAccount": null,
    "txHash": "abc123def456...",
    "confirmations": 5,
    "addressMode": "PER_PAYMENT",
    "merchantUserId": null,
    "expectedAmount": "100.50",
    "receivedAmount": "100.50",
    "metadata": { "orderId": "1001" },
    "createdAt": "2025-01-12T15:00:00.000Z",
    "updatedAt": "2025-01-12T15:01:00.000Z",
    "expiresAt": "2025-01-12T15:30:00.000Z"
  }
}
payment.confirmed

Payment has been confirmed on the blockchain

Example Payload

{
  "id": "evt_a1b2c3d4_1705078500000",
  "type": "payment.confirmed",
  "createdAt": "2025-01-12T15:05:00.000Z",
  "data": {
    "paymentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "confirmed",
    "amount": "100.50",
    "currency": "USDT",
    "network": "TRON",
    "tokenStandard": "TRC20",
    "txHash": "abc123def456...",
    "externalId": "generated-id",
    "generatedAddress": "TXyz...abc123",
    "generatedTokenAccount": null,
    "confirmations": 19,
    "addressMode": "PER_PAYMENT",
    "merchantUserId": null,
    "expectedAmount": "100.50",
    "receivedAmount": "100.50",
    "differenceAmount": "0",
    "overpaidAmount": "0",
    "underpaidAmount": "0",
    "alertType": "NONE",
    "feeAmount": "1.005",
    "netAmount": "99.495",
    "metadata": { "orderId": "1001" },
    "createdAt": "2025-01-12T15:00:00.000Z",
    "updatedAt": "2025-01-12T15:05:00.000Z",
    "expiresAt": "2025-01-12T15:30:00.000Z"
  }
}
payment.underpaid

Received amount was less than expected

Example Payload

{
  "id": "evt_a1b2c3d4_1705078500000",
  "type": "payment.underpaid",
  "createdAt": "2025-01-12T15:05:00.000Z",
  "data": {
    "paymentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "underpaid",
    "amount": "100.50",
    "currency": "USDT",
    "network": "TRON",
    "expectedAmount": "100.50",
    "receivedAmount": "90.00",
    "differenceAmount": "10.50",
    "underpaidAmount": "10.50",
    "alertType": "UNDERPAID",
    "txHash": "abc123def456...",
    "generatedAddress": "TXyz...abc123"
  }
}
payment.overpaid

Received amount was more than expected

Example Payload

{
  "id": "evt_a1b2c3d4_1705078500000",
  "type": "payment.overpaid",
  "createdAt": "2025-01-12T15:05:00.000Z",
  "data": {
    "paymentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "overpaid",
    "amount": "100.50",
    "currency": "USDT",
    "network": "TRON",
    "expectedAmount": "100.50",
    "receivedAmount": "120.00",
    "differenceAmount": "19.50",
    "overpaidAmount": "19.50",
    "alertType": "OVERPAID",
    "txHash": "abc123def456...",
    "generatedAddress": "TXyz...abc123"
  }
}
payment.expired

Payment expired without receiving funds

Example Payload

{
  "id": "evt_a1b2c3d4_1705080600000",
  "type": "payment.expired",
  "createdAt": "2025-01-12T15:30:00.000Z",
  "data": {
    "paymentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "expired",
    "amount": "100.50",
    "currency": "USDT",
    "network": "TRON",
    "generatedAddress": "TXyz...abc123",
    "expiresAt": "2025-01-12T15:30:00.000Z"
  }
}
payment.failed

Payment processing failed

Example Payload

{
  "id": "evt_a1b2c3d4_1705079400000",
  "type": "payment.failed",
  "createdAt": "2025-01-12T15:10:00.000Z",
  "data": {
    "paymentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "failed",
    "amount": "100.50",
    "currency": "USDT",
    "network": "TRON",
    "generatedAddress": "TXyz...abc123"
  }
}
payment.canceled

Payment was canceled by merchant or system

Example Payload

{
  "id": "evt_a1b2c3d4_1705079100000",
  "type": "payment.canceled",
  "createdAt": "2025-01-12T15:15:00.000Z",
  "data": {
    "paymentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "canceled",
    "amount": "100.50",
    "currency": "USDT",
    "network": "TRON",
    "tokenStandard": "TRC20",
    "generatedAddress": "TXyz...abc123",
    "generatedTokenAccount": null,
    "addressMode": "PER_PAYMENT",
    "merchantUserId": null,
    "expectedAmount": "100.50",
    "metadata": { "orderId": "1001" },
    "createdAt": "2025-01-12T15:00:00.000Z",
    "updatedAt": "2025-01-12T15:15:00.000Z",
    "expiresAt": "2025-01-12T15:30:00.000Z"
  }
}

Retry Policy

If your endpoint returns a non-2xx status code or times out (30 second limit), we'll retry the webhook with exponential backoff:

1s
1st retry
5s
2nd retry
30s
3rd retry
1m
4th retry
5m
Final retry

After 5 failed attempts, the webhook will be marked as failed. You can view failed webhooks in your merchant dashboard.

Best Practices

  • Always verify webhook signatures before processing
  • Return 200 status code quickly, process asynchronously if needed
  • Handle duplicate webhooks gracefully (idempotency)
  • Log all webhook events for debugging and auditing
  • Use HTTPS endpoints with valid SSL certificates