Webhooks¶
Receive real-time payment notifications via webhooks.
Overview¶
Nivatio sends webhook events to your configured URL whenever a payment status changes. This allows you to: - Fulfill orders automatically when payments succeed - Handle failed payments gracefully - Keep your database in sync with payment status
Configuring Webhooks¶
- Log in to Dashboard
- Navigate to Projects → Your Project → Webhooks
- Click Add Webhook Endpoint
- Enter your endpoint URL (must be HTTPS)
- Select events to subscribe to
- Save the configuration
Webhook URL Requirements
- Must use HTTPS in production
- Must return a
200status code within 5 seconds - Publicly accessible (not localhost)
Webhook Events¶
| Event Type | Description | When Sent |
|---|---|---|
payment.succeeded |
Payment confirmed on-chain | After transaction confirmation |
payment.failed |
Payment failed or was cancelled | When payment fails |
payment.pending |
Order created, awaiting payment | When order is created |
payment.expired |
Payment expired (timeout) | After timeout period |
Webhook Payload¶
Example payment.succeeded event:
{
"event": "payment.succeeded",
"timestamp": 1713888000,
"data": {
"id": "order_abc123",
"amount": 100000,
"currency": "NUSD",
"status": "PAID",
"txHash": "0xabc123...",
"blockNumber": 1234567,
"confirmations": 2,
"paidAt": "2026-05-06T18:29:45Z",
"metadata": {
"orderId": "internal_123"
}
}
}
Handling Webhooks¶
Example: Express.js¶
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {
const event = req.body;
// Verify event structure
if (!event.event || !event.data) {
return res.status(400).json({ error: 'Invalid payload' });
}
// Handle the event
switch (event.event) {
case 'payment.succeeded':
handleSuccessfulPayment(event.data);
break;
case 'payment.failed':
handleFailedPayment(event.data);
break;
default:
console.log(`Unhandled event type: ${event.event}`);
}
// Acknowledge receipt
res.json({ received: true });
});
function handleSuccessfulPayment(order) {
console.log(`Payment ${order.id} succeeded!`);
console.log(`Transaction: ${order.txHash}`);
// Fulfill the order in your system
}
app.listen(3000, () => console.log('Webhook listener running on port 3000'));
Example: Python (Flask)¶
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
event = request.json
if event['event'] == 'payment.succeeded':
handle_successful_payment(event['data'])
return jsonify({'received': True})
def handle_successful_payment(order):
print(f"Payment {order['id']} succeeded!")
# Fulfill the order
if name == 'main':
app.run(port=3000)
Example: Python (Flask)¶
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
event = request.json
if event['type'] == 'payment.succeeded':
handle_successful_payment(event['data'])
return jsonify({'received': True})
def handle_successful_payment(order):
print(f"Payment {order['id']} succeeded!")
# Fulfill the order
if __name__ == '__main__':
app.run(port=3000)
Idempotency & Retries¶
Implementing Idempotency¶
The webhook payload includes an eventId field for deduplication:
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
const event = req.body;
// Check if already processed
if (processedEvents.has(event.eventId)) {
return res.json({ received: true, reason: 'duplicate' });
}
// Process event...
// Mark as processed
processedEvents.add(event.eventId);
res.json({ received: true });
});
Retry Mechanism¶
Nivatio retries failed webhooks: - Initial delay: Immediate (t=0) - Backoff: 500ms, then 1000ms - Max retries: 3 attempts total - Timeout: 5 seconds per attempt
| Attempt | Delay from Start |
|---|---|
| 1 | Immediate (t=0) |
| 2 | +500ms (t=500ms) |
| 3 | +1000ms (t=1500ms total) |
Webhook Timeouts
If your endpoint doesn't respond within 5 seconds, the delivery is marked as failed and retried.
Testing Webhooks¶
Using Sandbox¶
- Configure webhook URL in sandbox project
- Create a test order
- Simulate payment using internal key:
curl -X POST https://sandbox.nivat.io/v1/sandbox/simulate-pay \ -H "x-nivatio-internal-key: YOUR_INTERNAL_KEY" \ -H "Content-Type: application/json" \ -d '{"orderId": "order_abc123"}' - Check your endpoint receives the webhook
Local Testing with ngrok¶
# Expose local server to internet
ngrok http 3000
# Use the ngrok URL as your webhook endpoint
# e.g., https://abc123.ngrok.io/webhook
Security Best Practices¶
- Use HTTPS - Never accept webhooks over HTTP in production
- Validate payload - Check required fields before processing
- Implement idempotency - Use
eventIdto prevent duplicates - Respond quickly - Return 200 within 5 seconds
- Log all events - Keep audit trail for debugging
- Verify transaction on-chain - For high-value orders, verify the txHash
Troubleshooting¶
| Issue | Solution |
|---|---|
| Webhook not received | Check URL is accessible, uses HTTPS, returns 200 |
| Duplicate events | Implement idempotency with eventId |
| Timeout errors | Optimize endpoint response time |
| Invalid payload | Verify webhook signature using x-nivatio-signature |