Webhook Handling

Receive real-time notifications when transaction events occur in your 3PAY account.

Webhook Handling

Webhooks push real-time HTTP POST notifications to your server when transactions occur — deposits confirmed, withdrawals completed, payouts processed.


Setting Up Your Webhook URL

Dashboard: Settings > Webhooks → enter your HTTPS endpoint.

API:

POST /api/v1/webhook/url
Authorization: Bearer <your-jwt-token>

{ "webhookUrl": "https://your-server.com/webhook" }

Test your endpoint:

POST /api/v1/webhook/test

Test webhooks include X-Webhook-Test: true.


Webhook Event Types

TypeDescriptionTrigger
depositCustomer payment received or expiredOn-chain balance detected or invoice timer expires
withdrawalEnd-user withdrawal processedAuto-approved or merchant approve/reject
payoutCompany treasury movementAlways auto-approved after dashboard OTP

Payload Structure

{
  "success": true,
  "message": "Transaction status update",
  "data": {
    "type": "deposit",
    "transactionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "clientId": "67890abcdef12345",
    "amount": 100.00,
    "fee": 0,
    "netAmount": 100.00,
    "actualBalance": 100.00,
    "currencyType": "USDT-TRC20",
    "status": "confirmed",
    "createdAt": "2026-02-20T10:00:00.000Z",
    "confirmedAt": "2026-02-20T10:05:32.000Z",
    "blockchainTxHash": "abc123def456789...",
    "walletAddress": "TXyz1234567890abcdef",
    "network": "USDT-TRC20",
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}

Field Reference

FieldTypeDescription
typestringdeposit, withdrawal, or payout
transactionIdstringUnique transaction identifier
clientIdstringYour merchant ID
amountnumberOriginal amount in USDT
feenumberNetwork fee deducted
netAmountnumberAmount after fee deduction
actualBalancenumberOn-chain balance at detection (deposits only)
currencyTypestringe.g., USDT-TRC20, USDT-ERC20
statusstringCurrent transaction status
blockchainTxHashstringOn-chain transaction hash

HTTP Headers

HeaderDescription
Content-Typeapplication/json
X-Webhook-Source3Pay-Payment-Processor
X-Transaction-IdTransaction this webhook relates to
X-Webhook-SignatureHMAC-SHA256 signature for verification

Status Values

Deposits

StatusMeaning
confirmedPayment detected and credited
failedInvoice expired before payment

Withdrawals

StatusMeaning
pendingAwaiting merchant approval
completedBlockchain transfer succeeded
failedBlockchain transfer failed (balance restored)
rejectedRejected by merchant

Payouts

StatusMeaning
completedExecuted on-chain
failedExecution failed (balance restored)

Webhook Lifecycle

Auto-approved flows (1 webhook): Request → Blockchain Execution → Webhook (completed / failed)

Applies to: all deposits, all payouts, withdrawals under auto-approve threshold, user withdrawals via API.

Manual approval flows (2 webhooks): Request → Webhook (pending) → Merchant action → Webhook (completed / failed / rejected)

Applies to: withdrawals over threshold or when autoWithdraw is disabled.


Verifying Webhook Signatures (HMAC-SHA256)

Every webhook includes an X-Webhook-Signature header. Verify using your webhook secret.

Manage your secret:

GET  /api/v1/webhook/secret          # View masked (last 8 chars)
POST /api/v1/webhook/secret/rotate   # Generate new secret (returned once)

Node.js

const crypto = require("crypto");

function verifyWebhookSignature(req, webhookSecret) {
  const signature = req.headers["x-webhook-signature"];
  if (!signature) return false;

  const expected =
    "sha256=" +
    crypto.createHmac("sha256", webhookSecret).update(JSON.stringify(req.body)).digest("hex");

  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

app.post("/webhook", express.json(), (req, res) => {
  if (!verifyWebhookSignature(req, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: "Invalid signature" });
  }
  res.status(200).json({ success: true });
  processWebhookAsync(req.body);
});

Python

import hmac, hashlib

def verify_webhook_signature(payload_bytes, signature_header, webhook_secret):
    expected = "sha256=" + hmac.new(
        webhook_secret.encode(), payload_bytes, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature_header or "", expected)

Important: Verify against the raw request body bytes. Re-serialized JSON won't match.


Automatic Retries

Failed deliveries retry with exponential backoff:

AttemptDelay
1st1 minute
2nd5 minutes
3rd30 minutes
4th2 hours
5th12 hours

After 5 failures, auto-retries stop. Manual retry:

POST /api/v1/webhook/retry/{webhookEventId}

Monitor delivery health:

GET /api/v1/webhook/events?page=1&limit=20&status=failed

Delivery statuses: pending, delivered, failed.


Best Practices

  1. Respond with HTTP 200 immediately — process data asynchronously. Slow responses trigger retries.
  2. Implement idempotency — use transactionId + status as dedup key. You may receive duplicates after retries.
  3. Route by type — always check data.type. Don't assume all webhooks are deposits.
  4. Handle all statuses — log unknown statuses instead of throwing errors.
  5. Use HTTPS — payload contains transaction amounts and wallet addresses.
  6. Verify signatures — prevents forged webhook requests.

Troubleshooting

IssueSolution
Not receiving webhooksSet URL in Dashboard > Settings or via API
Signature mismatchVerify against raw body bytes, not re-serialized JSON
Duplicate webhooksImplement idempotency with transactionId + status
All retries exhaustedManual retry: POST /webhook/retry/{id}
Missing X-Webhook-SignatureRotate secret: POST /webhook/secret/rotate