Webhooks
Webhooks allow 4pay.online to push real-time transaction status updates to your server — no polling required.
How it works
- When creating a transaction, pass a
notifyUrlin the request body - When the transaction status changes, the platform sends a
POSTrequest to yournotifyUrl - Your server responds with
{"success": "true"}to acknowledge receipt - If your server does not respond with a success, the platform will retry
Setting up a webhook endpoint
Option 1 — Per transaction
Pass notifyUrl when creating each transaction:
{
"params": {
"type": "payment",
"amount": 1000,
"currency": "USD",
"txid": "order-12345",
"returnUrl": "https://yoursite.com/success",
"failUrl": "https://yoursite.com/fail",
"notifyUrl": "https://yoursite.com/webhooks/payment"
}
}
Option 2 — Terminal-level default
Your platform operator can configure a default notify_url at the terminal level in the Admin Console. This applies to all transactions on that terminal unless overridden per-request.
Webhook payload
{
"type": "payment",
"txid": "order-12345",
"id": "550e8400-e29b-41d4-a716-446655440000",
"amount": 1000,
"amount_dest": 950,
"amount_with_fee": 1050,
"status": "charged",
"error_description": ""
}
Fields
| Field | Type | Description |
|---|---|---|
type | string | Event type: payment, payout |
txid | string | Your original order ID |
id | string | Platform transaction UUID |
amount | integer | Original amount in smallest currency unit |
amount_dest | integer | Amount credited to you (after fees) |
amount_with_fee | integer | Amount including platform fee |
status | string | New transaction status |
error_description | string | Error description (empty on success) |
Status values in webhooks
| Status | Meaning |
|---|---|
started | Payment initiated |
charged | Payment successful |
failed | Payment failed |
refunded | Refund completed |
cancelled | Transaction cancelled |
Responding to a webhook
Your endpoint must respond with HTTP 200 and the following JSON body:
{ "success": "true" }
Any other response (non-200 status, different body, timeout) is treated as a failure and will trigger a retry.
Process the webhook asynchronously. Acknowledge receipt immediately and handle business logic in a background job. Long-running handlers may time out and cause duplicate deliveries.
Retry policy
If your endpoint fails to respond successfully, the platform retries delivery with exponential backoff. Ensure your webhook handler is idempotent — the same event may be delivered more than once.
To deduplicate events, use the id field (transaction UUID) as a unique key.
Testing webhooks locally
Use a tunnel tool (ngrok, localtunnel, Cloudflare Tunnel) to expose your local server to the internet during development:
ngrok http 3000
# → Forwarding: https://abc123.ngrok.io → localhost:3000
Use https://abc123.ngrok.io/webhooks/payment as your notifyUrl in test transactions.
See Sandbox & Testing for test transaction instructions.
Example handler (Node.js)
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/payment', (req, res) => {
const { type, txid, id, status, amount } = req.body;
// Deduplicate by transaction ID
// processPaymentEvent(id, status, amount);
console.log(`Transaction ${txid} (${id}): ${status}`);
// Acknowledge receipt
res.json({ success: 'true' });
});
app.listen(3000);
Example handler (Python)
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/payment', methods=['POST'])
def webhook():
data = request.get_json()
tx_id = data.get('id')
status = data.get('status')
txid = data.get('txid')
# Deduplicate by tx_id and handle event
print(f"Transaction {txid} ({tx_id}): {status}")
return jsonify({"success": "true"})