Skip to main content

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โ€‹

  1. Log in to Omise Dashboard
  2. Navigate to Settings โ†’ Webhooks
  3. Click "Create Webhook Endpoint"
  4. Enter your endpoint URL (must be HTTPS)
  5. Select events to receive (or choose "All events")
  6. Save and copy the webhook secret
HTTPS Required

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โ€‹

EventDescription
charge.createCharge created
charge.completePayment successfully completed
charge.failedPayment failed
charge.expireAuthorization expired (uncaptured)
charge.reverseCharge reversed
charge.capturePre-authorized charge captured
charge.updateCharge updated

Customer Eventsโ€‹

EventDescription
customer.createCustomer created
customer.updateCustomer information updated
customer.update.cardCustomer's card updated
customer.destroyCustomer deleted

Card Eventsโ€‹

EventDescription
card.updateCard information updated
card.destroyCard deleted from customer

Refund Eventsโ€‹

EventDescription
refund.createRefund created and processed

Transfer Eventsโ€‹

EventDescription
transfer.createTransfer created
transfer.payTransfer marked as paid
transfer.sendTransfer sent to bank
transfer.failTransfer failed
transfer.destroyTransfer deleted
transfer.updateTransfer updated

Recipient Eventsโ€‹

EventDescription
recipient.createRecipient created
recipient.activateRecipient activated
recipient.deactivateRecipient deactivated
recipient.verifyRecipient verified
recipient.updateRecipient updated
recipient.destroyRecipient deleted

Dispute Eventsโ€‹

EventDescription
dispute.createDispute filed by customer
dispute.updateDispute information updated
dispute.acceptDispute accepted by merchant
dispute.closeDispute resolved
EventDescription
link.createPayment link created
linked_account.createDirect debit account linked
linked_account.completeDirect debit linking completed

Schedule Eventsโ€‹

EventDescription
schedule.createRecurring schedule created
schedule.suspendSchedule suspended (failed charges)
schedule.expiringSchedule about to expire
schedule.expireSchedule expired
schedule.destroySchedule 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โ€‹

  1. Go to Settings โ†’ Webhooks
  2. Select your endpoint
  3. Click "Test Webhook"
  4. Choose event type
  5. 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:

  1. Go to Settings โ†’ Webhooks
  2. Select your endpoint
  3. View webhook history
  4. 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:

  1. Run ngrok http 3000
  2. Copy the HTTPS URL
  3. Add to Omise dashboard webhooks
  4. Test locally with full webhook flow

Next Stepsโ€‹

  1. Set up webhook endpoint
  2. Implement signature verification
  3. Handle key events
  4. Test with ngrok
  5. Monitor webhook delivery
  6. Go live