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
| Type | Description | Trigger |
|---|---|---|
deposit | Customer payment received or expired | On-chain balance detected or invoice timer expires |
withdrawal | End-user withdrawal processed | Auto-approved or merchant approve/reject |
payout | Company treasury movement | Always 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
| Field | Type | Description |
|---|---|---|
type | string | deposit, withdrawal, or payout |
transactionId | string | Unique transaction identifier |
clientId | string | Your merchant ID |
amount | number | Original amount in USDT |
fee | number | Network fee deducted |
netAmount | number | Amount after fee deduction |
actualBalance | number | On-chain balance at detection (deposits only) |
currencyType | string | e.g., USDT-TRC20, USDT-ERC20 |
status | string | Current transaction status |
blockchainTxHash | string | On-chain transaction hash |
HTTP Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-Webhook-Source | 3Pay-Payment-Processor |
X-Transaction-Id | Transaction this webhook relates to |
X-Webhook-Signature | HMAC-SHA256 signature for verification |
Status Values
Deposits
| Status | Meaning |
|---|---|
confirmed | Payment detected and credited |
failed | Invoice expired before payment |
Withdrawals
| Status | Meaning |
|---|---|
pending | Awaiting merchant approval |
completed | Blockchain transfer succeeded |
failed | Blockchain transfer failed (balance restored) |
rejected | Rejected by merchant |
Payouts
| Status | Meaning |
|---|---|
completed | Executed on-chain |
failed | Execution 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:
| Attempt | Delay |
|---|---|
| 1st | 1 minute |
| 2nd | 5 minutes |
| 3rd | 30 minutes |
| 4th | 2 hours |
| 5th | 12 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
- Respond with HTTP 200 immediately — process data asynchronously. Slow responses trigger retries.
- Implement idempotency — use
transactionId+statusas dedup key. You may receive duplicates after retries. - Route by type — always check
data.type. Don't assume all webhooks are deposits. - Handle all statuses — log unknown statuses instead of throwing errors.
- Use HTTPS — payload contains transaction amounts and wallet addresses.
- Verify signatures — prevents forged webhook requests.
Troubleshooting
| Issue | Solution |
|---|---|
| Not receiving webhooks | Set URL in Dashboard > Settings or via API |
| Signature mismatch | Verify against raw body bytes, not re-serialized JSON |
| Duplicate webhooks | Implement idempotency with transactionId + status |
| All retries exhausted | Manual retry: POST /webhook/retry/{id} |
Missing X-Webhook-Signature | Rotate secret: POST /webhook/secret/rotate |
Updated 26 days ago
