Webhook Notifications
Overview
Webhooks allow you to receive real-time notifications when events occur in your Unter account, such as successful payments or payment failures. Instead of constantly polling our API for updates, webhooks push notifications directly to your server when something important happens.
How Webhooks Work
Configure your webhook URL - Tell Unter where to send notifications
Event occurs - A payment succeeds, fails, etc.
Unter sends HTTP POST - We POST event data to your URL
Your server responds - Return 200 OK to acknowledge receipt
Retry if needed - We retry failed deliveries with exponential backoff
Webhook Events
payment.succeeded
Fired when a payment is successfully confirmed on the blockchain.
Event Type: payment.succeeded
Payload:
{
"id": "evt_1a2b3c4d5e6f7g8h",
"type": "payment.succeeded",
"created": 1690876543,
"data": {
"object": {
"id": "pay_9z8y7x6w5v4u3t2s",
"amount": {
"raw": "5000000",
"formatted": "5.00",
"decimals": 6
},
"currency": "USDC",
"status": "succeeded",
"reference": "order_12345",
"source_chain": "Ethereum",
"destination_chain": "Polygon",
"source_tx_hash": "0xabcd1234...",
"destination_tx_hash": "0xefgh5678...",
"payer_address": "0x742C4B8C2515Ad1D8e1C71e8e24c8D5F9A1a8B5A",
"created_at": "2024-08-14T13:45:00+00:00",
"completed_at": "2024-08-14T13:47:00+00:00"
}
}
}payment.failed
Fired when a payment fails to process or confirm.
Event Type: payment.failed
Payload:
{
"id": "evt_2b3c4d5e6f7g8h9i",
"type": "payment.failed",
"created": 1690876600,
"data": {
"object": {
"id": "pay_8y7x6w5v4u3t2s1r",
"amount": {
"raw": "2500000",
"formatted": "2.50",
"decimals": 6
},
"currency": "USDC",
"status": "failed",
"reference": "order_67890",
"source_chain": "Ethereum",
"destination_chain": "Polygon",
"source_tx_hash": "0x1234abcd...",
"destination_tx_hash": null,
"payer_address": "0x853D5C9F3A2E7B4F8C1E6A9D2B5F8E3C7A6B9E2A",
"created_at": "2024-08-14T14:15:00+00:00",
"completed_at": null
}
}
}Webhook Security
Unter signs all webhook payloads using HMAC-SHA256 with your webhook secret. This ensures the webhook is actually from Unter and hasn't been tampered with.
Getting Your Webhook Secret: You can find your webhook secret in the Unter merchant portal under the webhook settings section. The secret is automatically generated when you first configure your webhook URL.
Signature Verification
Each webhook includes a Unter-Signature header with the following format:
Unter-Signature: t=1690876543,v1=5d41402abc4b2a76b9719d911017c592Where:
t= Unix timestamp when the signature was generatedv1= HMAC-SHA256 hex digest of the signed payload
Verifying Signatures
Step 1: Extract timestamp and signature
t=1690876543,v1=5d41402abc4b2a76b9719d911017c592Step 2: Create signed payload Concatenate the timestamp, a period, and the raw request body:
1690876543.{"id":"evt_123","type":"payment.succeeded",...}Step 3: Generate expected signature
$expectedSignature = hash_hmac('sha256', $signedPayload, $webhookSecret);Step 4: Compare signatures Use constant-time comparison to prevent timing attacks:
if (hash_equals($expectedSignature, $receivedSignature)) {
// Signature is valid
}Step 5: Check timestamp tolerance Reject webhooks older than 5 minutes (300 seconds) to prevent replay attacks:
if (abs(time() - $timestamp) > 300) {
// Webhook too old, reject
}Retry Logic
Unter automatically retries failed webhook deliveries using exponential backoff:
Attempt 1: Immediately
Attempt 2: After 1 minute
Attempt 3: After 3 minutes
Attempt 4: After 5 minutes
Attempt 5: After 10 minutes
Attempt 6: After 30 minutes
Attempt 7: After 2 hours
A webhook is considered failed if:
Your server doesn't respond within 10 seconds
Your server returns a non-2xx HTTP status code
Network connection fails
After 7 failed attempts, we stop retrying and mark the webhook as permanently failed.
Implementation Examples
JavaScript (Node.js with Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
// Middleware to capture raw body for signature verification
app.use('/webhooks', express.raw({ type: 'application/json' }));
const WEBHOOK_SECRET = process.env.UNTER_WEBHOOK_SECRET;
function verifyWebhookSignature(payload, signature, secret) {
// Parse signature header
const elements = {};
signature.split(',').forEach(pair => {
const [key, value] = pair.split('=');
elements[key] = value;
});
if (!elements.t || !elements.v1) {
return false;
}
const timestamp = parseInt(elements.t);
// Check timestamp tolerance (5 minutes)
if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
return false;
}
// Create signed payload
const signedPayload = `${timestamp}.${payload}`;
// Generate expected signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Compare signatures
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(elements.v1, 'hex')
);
}
app.post('/webhooks/unter', (req, res) => {
const signature = req.headers['unter-signature'];
const payload = req.body.toString();
// Verify signature
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
console.error('Invalid webhook signature');
return res.status(400).send('Invalid signature');
}
// Parse webhook data
const event = JSON.parse(payload);
console.log('Received webhook:', event.type, event.id);
// Handle the event
switch (event.type) {
case 'payment.succeeded':
handlePaymentSucceeded(event.data.object);
break;
case 'payment.failed':
handlePaymentFailed(event.data.object);
break;
default:
console.log('Unhandled event type:', event.type);
}
// Always respond with 200 OK
res.status(200).send('OK');
});
function handlePaymentSucceeded(payment) {
console.log('Payment succeeded:', payment.id);
console.log('Amount:', payment.amount.formatted, payment.currency);
console.log('Reference:', payment.reference);
// Update your database
// Send confirmation email
// Fulfill the order
}
function handlePaymentFailed(payment) {
console.log('Payment failed:', payment.id);
console.log('Reference:', payment.reference);
// Log the failure
// Notify customer
// Update order status
}
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});Best Practices
1. Always Verify Signatures
Never process webhooks without verifying the signature. This prevents malicious actors from sending fake webhooks to your server.
2. Handle Idempotency
The same webhook may be delivered multiple times. Use the event ID to ensure you only process each event once:
const processedEvents = new Set();
app.post('/webhooks/unter', (req, res) => {
const event = JSON.parse(req.body);
// Check if we've already processed this event
if (processedEvents.has(event.id)) {
return res.status(200).send('Already processed');
}
// Process the event
handleEvent(event);
// Mark as processed
processedEvents.add(event.id);
res.status(200).send('OK');
});3. Respond Quickly
Respond with 200 OK as quickly as possible. Do heavy processing (database updates, email sending) asynchronously:
4. Handle Failures Gracefully
Your webhook endpoint should be fault-tolerant and handle errors gracefully:
app.post('/webhooks/unter', (req, res) => {
try {
// Process webhook
processWebhook(req.body);
res.status(200).send('OK');
} catch (error) {
console.error('Webhook processing failed:', error);
// Return 200 to prevent retries for unrecoverable errors
res.status(200).send('Error logged');
}
});Common Issues
1. Signature Verification Failures
Causes:
Wrong webhook secret
Modifying request body before verification
Clock synchronization issues
Solutions:
Verify you're using the correct secret from the webhook settings
Verify signature before parsing/modifying the request body
Ensure server time is synchronized with NTP
2. Webhook Timeouts
Causes:
Slow database queries in webhook handler
Synchronous external API calls
Heavy processing in webhook handler
Solutions:
Respond with 200 OK immediately
Process webhooks asynchronously
Optimize database queries
3. Duplicate Processing
Causes:
Not implementing idempotency
Multiple server instances processing same webhook
Solutions:
Use event ID to track processed webhooks
Implement proper database locking/transactions
Security Considerations
Always verify signatures - Never trust unverified webhooks
Validate timestamp - Reject old webhooks to prevent replay attacks
Last updated