Webhooks
Receive instant notifications about payment events, charge updates, and other important account activities through secure HTTP callbacks.
Overviewโ
Webhooks allow your application to receive real-time notifications when events happen in your Omise account. Instead of polling the API to check for changes, Omise automatically sends HTTP POST requests to your specified endpoints when events occur.
Key Benefits:
- Real-time Updates - Instant notification when events occur
- Reduced API Calls - No need to poll for status updates
- Reliable Delivery - Automatic retries on failure
- Secure - HMAC-SHA256 signature verification
- Comprehensive - 30+ event types supported
How Webhooks Workโ
Setup Webhooksโ
Dashboard Configurationโ
- Log in to Omise Dashboard
- Navigate to Settings โ Webhooks
- Click "Create Webhook Endpoint"
- Enter your endpoint URL (must be HTTPS)
- Select events to receive (or choose "All events")
- Save and copy the webhook secret
Webhook endpoints MUST use HTTPS with a valid SSL certificate. Self-signed certificates are not supported.
Test Your SSL Certificateโ
# Verify your SSL certificate
curl https://www.ssllabs.com/ssltest/analyze.html?d=yourdomain.com
Use SSL Labs to ensure your certificate is properly configured.
Webhook Endpoint Implementationโ
Basic Express.js Exampleโ
const express = require('express');
const crypto = require('crypto');
const app = express();
// IMPORTANT: Use raw body for signature verification
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}
}));
app.post('/webhooks/omise', (req, res) => {
// 1. Verify webhook signature
if (!verifyWebhookSignature(req)) {
console.error('Invalid webhook signature');
return res.sendStatus(401);
}
// 2. Get event data
const event = req.body;
console.log('Received event:', event.key);
// 3. Handle different event types
switch (event.key) {
case 'charge.complete':
handleChargeComplete(event.data);
break;
case 'charge.failed':
handleChargeFailed(event.data);
break;
case 'refund.create':
handleRefundCreate(event.data);
break;
// Add more event handlers...
default:
console.log('Unhandled event type:', event.key);
}
// 4. Always respond with 200
res.sendStatus(200);
});
function verifyWebhookSignature(req) {
// Express.js automatically lowercases header names
const signatureHeader = req.headers['x-omise-signature'];
const rawBody = req.rawBody;
if (!signatureHeader || !rawBody) {
return false;
}
// Create signed payload from raw body
const signedPayload = rawBody;
// Get webhook secret from environment
const secret = Buffer.from(process.env.OMISE_WEBHOOK_SECRET, 'base64');
// Compute expected signature
const expectedBuffer = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest();
// Compare signatures (timing-safe)
const signatures = signatureHeader.split(',');
for (const sig of signatures) {
const sigBuffer = Buffer.from(sig, 'hex');
if (crypto.timingSafeEqual(sigBuffer, expectedBuffer)) {
return true;
}
}
return false;
}
async function handleChargeComplete(charge) {
console.log(`Charge ${charge.id} completed successfully`);
// Update database
await updateOrderStatus(charge.metadata.order_id, 'paid');
// Send confirmation email
await sendPaymentConfirmation(charge.metadata.customer_email, charge);
// Update inventory
await decrementStock(charge.metadata.product_ids);
}
async function handleChargeFailed(charge) {
console.log(`Charge ${charge.id} failed: ${charge.failure_message}`);
// Notify customer
await sendPaymentFailedEmail(charge.metadata.customer_email, {
reason: charge.failure_message,
orderId: charge.metadata.order_id
});
// Update order status
await updateOrderStatus(charge.metadata.order_id, 'payment_failed');
}
app.listen(3000);
PHP Exampleโ
<?php
// webhook_handler.php
// Get raw POST body
$rawBody = file_get_contents('php://input');
$event = json_decode($rawBody, true);
// Get headers
$signatureHeader = $_SERVER['HTTP_OMISE_SIGNATURE'];
$timestampHeader = $_SERVER['HTTP_OMISE_SIGNATURE_TIMESTAMP'];
// Verify signature
if (!verifyWebhookSignature($signatureHeader, $timestampHeader, $rawBody)) {
http_response_code(401);
exit('Invalid signature');
}
// Handle event
switch ($event['key']) {
case 'charge.complete':
handleChargeComplete($event['data']);
break;
case 'charge.failed':
handleChargeFailed($event['data']);
break;
case 'refund.create':
handleRefundCreate($event['data']);
break;
}
// Always respond with 200
http_response_code(200);
function verifyWebhookSignature($signatureHeader, $timestampHeader, $rawBody) {
$webhookSecret = getenv('OMISE_WEBHOOK_SECRET');
$secret = base64_decode($webhookSecret);
$signedPayload = $timestampHeader . '.' . $rawBody;
$expectedSignature = hash_hmac('sha256', $signedPayload, $secret);
$signatures = explode(',', $signatureHeader);
foreach ($signatures as $signature) {
if (hash_equals($expectedSignature, $signature)) {
return true;
}
}
return false;
}
function handleChargeComplete($charge) {
// Update order status
updateOrderStatus($charge['metadata']['order_id'], 'paid');
// Send confirmation email
sendPaymentConfirmation($charge['metadata']['customer_email'], $charge);
}
?>
Webhook Eventsโ
Charge Eventsโ
| Event | Description |
|---|---|
charge.create | Charge created |
charge.complete | Payment successfully completed |
charge.failed | Payment failed |
charge.expire | Authorization expired (uncaptured) |
charge.reverse | Charge reversed |
charge.capture | Pre-authorized charge captured |
charge.update | Charge updated |
Customer Eventsโ
| Event | Description |
|---|---|
customer.create | Customer created |
customer.update | Customer information updated |
customer.update.card | Customer's card updated |
customer.destroy | Customer deleted |
Card Eventsโ
| Event | Description |
|---|---|
card.update | Card information updated |
card.destroy | Card deleted from customer |
Refund Eventsโ
| Event | Description |
|---|---|
refund.create | Refund created and processed |
Transfer Eventsโ
| Event | Description |
|---|---|
transfer.create | Transfer created |
transfer.pay | Transfer marked as paid |
transfer.send | Transfer sent to bank |
transfer.fail | Transfer failed |
transfer.destroy | Transfer deleted |
transfer.update | Transfer updated |
Recipient Eventsโ
| Event | Description |
|---|---|
recipient.create | Recipient created |
recipient.activate | Recipient activated |
recipient.deactivate | Recipient deactivated |
recipient.verify | Recipient verified |
recipient.update | Recipient updated |
recipient.destroy | Recipient deleted |
Dispute Eventsโ
| Event | Description |
|---|---|
dispute.create | Dispute filed by customer |
dispute.update | Dispute information updated |
dispute.accept | Dispute accepted by merchant |
dispute.close | Dispute resolved |
Link Eventsโ
| Event | Description |
|---|---|
link.create | Payment link created |
linked_account.create | Direct debit account linked |
linked_account.complete | Direct debit linking completed |
Schedule Eventsโ
| Event | Description |
|---|---|
schedule.create | Recurring schedule created |
schedule.suspend | Schedule suspended (failed charges) |
schedule.expiring | Schedule about to expire |
schedule.expire | Schedule expired |
schedule.destroy | Schedule deleted |
Security Best Practicesโ
1. Always Verify Signaturesโ
Never skip signature verification! This prevents attackers from sending fake webhooks.
function verifyWebhookSignature(req) {
const signatureHeader = req.headers['omise-signature'];
const timestampHeader = req.headers['omise-signature-timestamp'];
const rawBody = req.rawBody;
if (!signatureHeader || !timestampHeader || !rawBody) {
console.error('Missing signature headers');
return false;
}
const signedPayload = `${timestampHeader}.${rawBody}`;
const secret = Buffer.from(process.env.OMISE_WEBHOOK_SECRET, 'base64');
const expectedBuffer = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest();
const signatures = signatureHeader.split(',');
for (const sig of signatures) {
const sigBuffer = Buffer.from(sig, 'hex');
if (crypto.timingSafeEqual(sigBuffer, expectedBuffer)) {
return true;
}
}
return false;
}
2. Use HTTPSโ
- All webhook endpoints MUST use HTTPS
- Use valid SSL certificates (not self-signed)
- Test with SSL Labs
3. Respond Quicklyโ
app.post('/webhooks/omise', async (req, res) => {
// Verify signature
if (!verifyWebhookSignature(req)) {
return res.sendStatus(401);
}
// Respond immediately
res.sendStatus(200);
// Process event asynchronously
processWebhookAsync(req.body).catch(err => {
console.error('Error processing webhook:', err);
});
});
4. Implement Idempotencyโ
async function processWebhookAsync(event) {
// Check if already processed
const exists = await db.query(
'SELECT id FROM webhook_events WHERE event_id = ?',
[event.id]
);
if (exists.length > 0) {
console.log('Event already processed:', event.id);
return;
}
// Process event
await handleEvent(event);
// Mark as processed
await db.query(
'INSERT INTO webhook_events (event_id, event_key, processed_at) VALUES (?, ?, NOW())',
[event.id, event.key]
);
}
5. Handle Secret Rotationโ
When rotating webhook secrets, Omise includes both old and new signatures for 24 hours:
function verifyWebhookSignature(req) {
const signatureHeader = req.headers['omise-signature'];
const signatures = signatureHeader.split(','); // Multiple signatures during rotation
for (const sig of signatures) {
if (checkSignature(sig, req.rawBody, req.headers['omise-signature-timestamp'])) {
return true;
}
}
return false;
}
Retry Logicโ
Omise automatically retries failed webhook deliveries:
Retry Schedule:
- Immediate failure: Retry on subsequent days
- Return non-200 status code to trigger retry
- Check dashboard for failed deliveries
Best Practices:
- Always return 200 for received webhooks
- Process webhooks asynchronously
- Implement proper error handling
app.post('/webhooks/omise', async (req, res) => {
try {
// Verify signature
if (!verifyWebhookSignature(req)) {
// Don't retry - invalid signature
return res.sendStatus(401);
}
// Return 200 immediately
res.sendStatus(200);
// Process in background
await processWebhook(req.body);
} catch (error) {
// Log error but still return 200
// We've received the webhook successfully
console.error('Webhook processing error:', error);
res.sendStatus(200);
}
});
Testing Webhooksโ
Local Testing with ngrokโ
# Install ngrok
npm install -g ngrok
# Start your local server
node server.js # Running on port 3000
# Create tunnel
ngrok http 3000
# Use the HTTPS URL in Omise dashboard
# Example: https://abc123.ngrok.io/webhooks/omise
Manual Testing via Dashboardโ
- Go to Settings โ Webhooks
- Select your endpoint
- Click "Test Webhook"
- Choose event type
- View request/response
Test with cURLโ
# Simulate webhook (without signature)
curl -X POST http://localhost:3000/webhooks/omise \
-H "Content-Type: application/json" \
-d '{
"object": "event",
"id": "evnt_test_123",
"key": "charge.complete",
"data": {
"id": "chrg_test_123",
"status": "successful",
"amount": 10000
}
}'
Common Issues & Troubleshootingโ
Issue: Webhooks Not Receivedโ
Causes:
- Firewall blocking Omise IPs
- HTTPS certificate invalid
- Wrong endpoint URL
- Server not responding
Solution:
- Check firewall rules
- Verify SSL certificate
- Test endpoint with curl
- Check server logs
Issue: "Invalid Signature"โ
Causes:
- Wrong webhook secret
- Body parsing modified the raw body
- Incorrect signature verification logic
Solution:
// CORRECT: Preserve raw body
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}
}));
// INCORRECT: Don't do this
app.use(express.json()); // This modifies the body
app.use(bodyParser.json()); // Raw body lost
Issue: Duplicate Eventsโ
Cause: Webhook retries or multiple endpoints
Solution: Implement idempotency checking
const processedEvents = new Set();
function handleWebhook(event) {
if (processedEvents.has(event.id)) {
console.log('Already processed:', event.id);
return;
}
// Process event
processEvent(event);
// Mark as processed
processedEvents.add(event.id);
// Clean up old events (keep last 10000)
if (processedEvents.size > 10000) {
const first = processedEvents.values().next().value;
processedEvents.delete(first);
}
}
FAQโ
What IP addresses does Omise use for webhooks?
Omise does not publish a fixed list of IP addresses for webhooks. Instead, verify webhooks using signature verification (HMAC-SHA256), which is more secure than IP whitelisting.
Can I have multiple webhook endpoints?
Yes! You can configure multiple webhook endpoints in the dashboard. Each endpoint can subscribe to different events. This is useful for:
- Separate endpoints for different services
- Development/staging/production environments
- Backup endpoints for redundancy
How long does Omise retry failed webhooks?
Omise retries failed webhooks on subsequent days. Check your dashboard for failed webhook deliveries and manually retry if needed.
Can I replay old webhooks?
Yes, in the dashboard:
- Go to Settings โ Webhooks
- Select your endpoint
- View webhook history
- Click "Resend" on any event
What if my server is down when webhook is sent?
Omise will retry the webhook delivery. Make sure your webhook endpoint:
- Returns to service quickly
- Can be retried safely (idempotent)
- Logs failures for manual review
How do I test webhooks in development?
Use ngrok to expose your local server:
- Run
ngrok http 3000 - Copy the HTTPS URL
- Add to Omise dashboard webhooks
- Test locally with full webhook flow
Related Resourcesโ
- Events API - Query historical events
- Webhook Security - Signature verification
- Testing Guide - Test webhook handling
- Charge API - Understand charge objects
- Dashboard Webhooks - Configure endpoints