Skip to main content

Setting Up Webhooks

Learn how to configure webhook endpoints in the Omise dashboard and set up your server to receive real-time payment event notifications.

Overviewโ€‹

Webhooks allow Omise to push real-time notifications to your server when events occur in your account. This guide covers:

  • Creating and configuring webhook endpoints in the dashboard
  • Setting up secure HTTPS endpoints
  • Testing webhooks locally with ngrok
  • Managing multiple endpoints
  • Verifying your webhook configuration

Dashboard Configurationโ€‹

Creating a Webhook Endpointโ€‹

Follow these steps to create a webhook endpoint in the Omise dashboard:

  1. Navigate to Webhooks Settings

    • Log in to your Omise Dashboard
    • Go to Settings > Webhooks
    • Click the Create Webhook button
  2. Configure Endpoint URL

    • Enter your HTTPS endpoint URL (e.g., https://api.example.com/webhooks/omise)
    • Ensure the URL is accessible from the internet
    • The endpoint must accept POST requests
  3. Select Events to Subscribe

    • Choose All Events to receive all webhook notifications
    • Or select specific events (charge.create, charge.complete, refund.create, etc.)
    • You can modify event subscriptions later
  4. Save and Retrieve Webhook Key

    • Click Create to generate the webhook endpoint
    • Copy and securely store the Webhook Signing Key
    • This key is used to verify webhook signatures (shown only once)

Dashboard Interface Elementsโ€‹

The webhook configuration page displays:

  • Endpoint URL: The destination for webhook events
  • Status: Active/Inactive toggle
  • Events: List of subscribed event types
  • Recent Deliveries: Log of recent webhook attempts with response codes
  • Signing Key: Used for HMAC-SHA256 signature verification

Managing Multiple Endpointsโ€‹

You can configure multiple webhook endpoints for different purposes:

Production Endpoint:
URL: https://api.example.com/webhooks/omise
Events: All events
Status: Active

Backup Endpoint:
URL: https://backup.example.com/webhooks/omise
Events: Critical events only (charge.complete, refund.create)
Status: Active

Analytics Endpoint:
URL: https://analytics.example.com/webhooks/omise
Events: All events
Status: Active

Best practices for multiple endpoints:

  • Use separate endpoints for production, staging, and development
  • Create dedicated endpoints for different services (accounting, analytics, notifications)
  • Monitor delivery success rates for each endpoint
  • Deactivate unused endpoints to reduce noise

Endpoint Requirementsโ€‹

HTTPS and SSL/TLSโ€‹

All webhook endpoints must meet these security requirements:

Required:

  • HTTPS protocol (HTTP is not supported)
  • Valid SSL/TLS certificate from a trusted CA
  • TLS 1.2 or higher
  • Strong cipher suites

Recommended:

  • Certificate from Let's Encrypt, DigiCert, or other trusted CA
  • Automatic certificate renewal
  • HSTS (HTTP Strict Transport Security) headers
# Example Nginx SSL configuration
server {
listen 443 ssl http2;
server_name api.example.com;

ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

location /webhooks/omise {
proxy_pass http://localhost:3000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
}
}

Response Requirementsโ€‹

Your endpoint must respond correctly to webhook deliveries:

Success Response:

  • HTTP status code: 200-299 (200 OK recommended)
  • Respond within 10 seconds
  • Return immediately without waiting for processing

Failure Response:

  • HTTP status code: 400-599 triggers automatic retry
  • Status codes outside 200-299 are considered failures
// Node.js/Express example
app.post('/webhooks/omise', async (req, res) => {
try {
// Verify signature first
const isValid = verifySignature(req);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}

// Respond immediately (don't wait for processing)
res.status(200).json({ received: true });

// Process webhook asynchronously
processWebhookAsync(req.body);
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
# Python/Flask example
@app.route('/webhooks/omise', methods=['POST'])
def handle_webhook():
try:
# Verify signature first
if not verify_signature(request):
return jsonify({'error': 'Invalid signature'}), 401

# Respond immediately
response = jsonify({'received': True})

# Process webhook asynchronously
process_webhook_async(request.json)

return response, 200
except Exception as e:
logging.error(f'Webhook error: {e}')
return jsonify({'error': 'Internal server error'}), 500
# Ruby/Sinatra example
post '/webhooks/omise' do
begin
# Verify signature first
unless verify_signature(request)
halt 401, { error: 'Invalid signature' }.to_json
end

# Respond immediately
status 200
body({ received: true }.to_json)

# Process webhook asynchronously
Thread.new do
process_webhook_async(JSON.parse(request.body.read))
end
rescue => e
logger.error("Webhook error: #{e.message}")
halt 500, { error: 'Internal server error' }.to_json
end
end
// PHP example
<?php
header('Content-Type: application/json');

try {
// Get request body
$payload = file_get_contents('php://input');
$event = json_decode($payload, true);

// Verify signature first
if (!verifySignature($payload, $_SERVER['HTTP_X_OMISE_SIGNATURE'])) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}

// Respond immediately
http_response_code(200);
echo json_encode(['received' => true]);

// Flush output to send response
fastcgi_finish_request();

// Process webhook after response is sent
processWebhook($event);
} catch (Exception $e) {
error_log('Webhook error: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Internal server error']);
}
?>
// Go example
func handleWebhook(w http.ResponseWriter, r *http.Request) {
// Read body
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request body", http.StatusBadRequest)
return
}
defer r.Body.Close()

// Verify signature first
signature := r.Header.Get("X-Omise-Signature")
if !verifySignature(body, signature) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}

// Respond immediately
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"received": true})

// Process webhook asynchronously
go processWebhookAsync(body)
}

Local Testing with ngrokโ€‹

ngrok creates a secure tunnel to your local development server, allowing you to test webhooks without deploying to production.

Installing ngrokโ€‹

macOS (Homebrew):

brew install ngrok/ngrok/ngrok

Linux:

curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | \
sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null && \
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | \
sudo tee /etc/apt/sources.list.d/ngrok.list && \
sudo apt update && sudo apt install ngrok

Windows:

choco install ngrok

Setting Up ngrokโ€‹

  1. Sign up for ngrok account (free tier available)

    • Visit ngrok.com
    • Create an account and get your authtoken
  2. Configure ngrok with your authtoken:

ngrok config add-authtoken YOUR_AUTHTOKEN
  1. Start your local server:
# Node.js
npm start
# or
node server.js

# Python
python app.py

# Ruby
ruby app.rb

# PHP
php -S localhost:3000

# Go
go run main.go
  1. Create ngrok tunnel:
# Create tunnel to local port 3000
ngrok http 3000

# With custom subdomain (paid plan)
ngrok http 3000 --subdomain=myapp-webhooks
  1. Copy the HTTPS forwarding URL:
ngrok by @inconshreveable

Session Status online
Account your-account (Plan: Free)
Version 3.0.0
Region United States (us)
Forwarding https://abc123.ngrok.io -> http://localhost:3000

Web Interface http://127.0.0.1:4040
  1. Configure webhook in Omise dashboard:
    • Use the HTTPS URL: https://abc123.ngrok.io/webhooks/omise
    • Select events to test
    • Save the configuration

ngrok Web Interfaceโ€‹

Access the ngrok web interface at http://127.0.0.1:4040 to:

  • Inspect all HTTP requests and responses
  • Replay webhook requests for debugging
  • View request headers and body
  • Check response times and status codes
# Example: View webhook request details
curl http://127.0.0.1:4040/api/requests

Testing Workflowโ€‹

  1. Start your local development server
  2. Start ngrok tunnel
  3. Configure webhook endpoint in Omise dashboard
  4. Trigger test events (create charge, refund, etc.)
  5. View webhook deliveries in ngrok interface
  6. Debug and iterate on your webhook handler
// Example local development setup
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/omise', (req, res) => {
console.log('Webhook received:', JSON.stringify(req.body, null, 2));
console.log('Headers:', req.headers);

// Verify signature
const signature = req.headers['x-omise-signature'];
console.log('Signature:', signature);

res.status(200).json({ received: true });
});

app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
console.log('Start ngrok: ngrok http 3000');
});

ngrok Best Practicesโ€‹

  • Use HTTPS URLs only for webhook endpoints
  • Restart ngrok when needed (free tier URLs change on restart)
  • Use custom subdomains (paid plans) for consistent testing
  • Monitor the web interface to debug webhook issues
  • Don't commit ngrok URLs to version control
  • Use environment variables for webhook URLs
// Environment-based webhook URL configuration
const webhookUrl = process.env.NODE_ENV === 'production'
? 'https://api.example.com/webhooks/omise'
: process.env.NGROK_URL + '/webhooks/omise';

console.log('Webhook URL:', webhookUrl);

Testing Webhooksโ€‹

Manual Testing in Dashboardโ€‹

The Omise dashboard provides tools for testing webhook deliveries:

  1. Navigate to Webhooks section
  2. Select your endpoint
  3. Click "Send Test Webhook"
  4. Choose event type (charge.complete, refund.create, etc.)
  5. Review delivery status and response

Triggering Test Eventsโ€‹

Create real events in test mode to trigger webhooks:

# Create a test charge (triggers charge.create webhook)
curl https://api.omise.co/charges \
-u skey_test_YOUR_SECRET_KEY: \
-d "amount=100000" \
-d "currency=THB" \
-d "card=tokn_test_5xxxxxxxxxxxxx"

# Complete a charge (triggers charge.complete webhook)
# For test mode, charges auto-complete if using test cards

# Create a refund (triggers refund.create webhook)
curl https://api.omise.co/charges/chrg_test_5xxxxxxxxxxxxx/refunds \
-u skey_test_YOUR_SECRET_KEY: \
-d "amount=50000"

Automated Testingโ€‹

Implement automated tests for your webhook handler:

// Node.js/Jest example
const request = require('supertest');
const crypto = require('crypto');
const app = require('./app');

describe('Webhook Handler', () => {
const webhookKey = 'test_webhook_key';

function generateSignature(payload) {
return crypto
.createHmac('sha256', webhookKey)
.update(payload)
.digest('hex');
}

test('accepts valid webhook with correct signature', async () => {
const payload = JSON.stringify({ key: 'charge.complete' });
const signature = generateSignature(payload);

const response = await request(app)
.post('/webhooks/omise')
.set('X-Omise-Signature', signature)
.send(payload)
.expect(200);

expect(response.body).toEqual({ received: true });
});

test('rejects webhook with invalid signature', async () => {
const payload = JSON.stringify({ key: 'charge.complete' });
const invalidSignature = 'invalid_signature';

await request(app)
.post('/webhooks/omise')
.set('X-Omise-Signature', invalidSignature)
.send(payload)
.expect(401);
});

test('handles charge.complete event', async () => {
const event = {
key: 'charge.complete',
data: {
object: 'charge',
id: 'chrg_test_123',
amount: 100000,
status: 'successful'
}
};

const payload = JSON.stringify(event);
const signature = generateSignature(payload);

const response = await request(app)
.post('/webhooks/omise')
.set('X-Omise-Signature', signature)
.send(payload)
.expect(200);

// Verify event was processed
// Add assertions based on your application logic
});
});
# Python/pytest example
import pytest
import hmac
import hashlib
import json
from app import app

@pytest.fixture
def client():
app.config['TESTING'] = True
with app.test_client() as client:
yield client

def generate_signature(payload, key='test_webhook_key'):
return hmac.new(
key.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()

def test_valid_webhook(client):
payload = json.dumps({'key': 'charge.complete'})
signature = generate_signature(payload)

response = client.post(
'/webhooks/omise',
data=payload,
headers={'X-Omise-Signature': signature},
content_type='application/json'
)

assert response.status_code == 200
assert response.json['received'] == True

def test_invalid_signature(client):
payload = json.dumps({'key': 'charge.complete'})

response = client.post(
'/webhooks/omise',
data=payload,
headers={'X-Omise-Signature': 'invalid'},
content_type='application/json'
)

assert response.status_code == 401

Real-World Scenariosโ€‹

E-commerce Platform Setupโ€‹

Setting up webhooks for an e-commerce platform:

// E-commerce webhook handler
const express = require('express');
const app = express();

// Multiple webhook handlers for different purposes
app.post('/webhooks/omise/payments', handlePaymentWebhooks);
app.post('/webhooks/omise/refunds', handleRefundWebhooks);
app.post('/webhooks/omise/disputes', handleDisputeWebhooks);

async function handlePaymentWebhooks(req, res) {
const event = req.body;

// Respond immediately
res.status(200).json({ received: true });

// Process based on event type
switch (event.key) {
case 'charge.complete':
await fulfillOrder(event.data);
await sendConfirmationEmail(event.data);
break;
case 'charge.failed':
await notifyCustomerOfFailure(event.data);
break;
}
}

Subscription Service Setupโ€‹

Handling recurring payments and subscription events:

# Subscription webhook handler
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/omise', methods=['POST'])
def handle_webhook():
event = request.json

# Respond immediately
response = jsonify({'received': True})

# Route to appropriate handler
if event['key'] == 'charge.complete':
extend_subscription(event['data'])
elif event['key'] == 'charge.failed':
retry_payment(event['data'])
elif event['key'] == 'refund.create':
cancel_subscription(event['data'])

return response, 200

def extend_subscription(charge_data):
"""Extend subscription when recurring charge succeeds"""
customer_id = charge_data['customer']
# Update subscription end date
# Send renewal confirmation email

def retry_payment(charge_data):
"""Handle failed recurring payment"""
customer_id = charge_data['customer']
# Notify customer of payment failure
# Schedule retry attempt
# If multiple failures, suspend subscription

Multi-tenant SaaS Platformโ€‹

Managing webhooks for multiple tenants:

# Multi-tenant webhook handler
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token

def omise
# Verify signature
unless verify_signature(request)
render json: { error: 'Invalid signature' }, status: :unauthorized
return
end

# Respond immediately
render json: { received: true }, status: :ok

# Identify tenant from metadata
event = JSON.parse(request.body.read)
tenant_id = event.dig('data', 'metadata', 'tenant_id')

# Process in background job with tenant context
WebhookProcessorJob.perform_later(event, tenant_id)
end
end

# Background job
class WebhookProcessorJob < ApplicationJob
queue_as :webhooks

def perform(event, tenant_id)
Tenant.find(tenant_id).scope do
case event['key']
when 'charge.complete'
ChargeCompleteHandler.new(event['data']).process
when 'refund.create'
RefundCreateHandler.new(event['data']).process
end
end
end
end

Best Practicesโ€‹

Securityโ€‹

  • Always verify webhook signatures before processing events
  • Use HTTPS with valid SSL certificates for all endpoints
  • Store webhook signing keys securely (environment variables, secrets manager)
  • Implement rate limiting to prevent abuse
  • Log all webhook deliveries for audit trail

Reliabilityโ€‹

  • Respond quickly (within 10 seconds) to avoid timeouts
  • Process asynchronously using background jobs or queues
  • Implement idempotency to handle duplicate deliveries
  • Monitor webhook health with alerting for failures
  • Test regularly in development and staging environments

Performanceโ€‹

  • Use message queues (Redis, RabbitMQ, AWS SQS) for processing
  • Scale horizontally with load balancers for high volume
  • Batch database operations when processing multiple events
  • Cache frequently accessed data to reduce database load
  • Set up monitoring and metrics for response times
// Example: Using Redis queue for async processing
const express = require('express');
const Queue = require('bull');
const app = express();

// Create webhook processing queue
const webhookQueue = new Queue('webhooks', {
redis: { host: 'localhost', port: 6379 }
});

// Webhook endpoint
app.post('/webhooks/omise', async (req, res) => {
// Verify signature
if (!verifySignature(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}

// Add to queue and respond immediately
await webhookQueue.add(req.body, {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 }
});

res.status(200).json({ received: true });
});

// Process webhooks from queue
webhookQueue.process(async (job) => {
const event = job.data;
console.log(`Processing webhook: ${event.key}`);

// Your webhook processing logic
await processWebhook(event);
});

Maintenanceโ€‹

  • Version your webhook handlers to support API changes
  • Document your webhook processing logic for team reference
  • Set up alerting for webhook failures or anomalies
  • Regular security audits of webhook endpoints
  • Keep dependencies updated for security patches

Troubleshootingโ€‹

Common Issues and Solutionsโ€‹

Issue: Webhook endpoint not receiving events

Solutions:

  • Verify endpoint is accessible via HTTPS from internet
  • Check firewall rules and security groups
  • Confirm endpoint is active in Omise dashboard
  • Review server logs for incoming requests
  • Test endpoint with curl or Postman
# Test endpoint accessibility
curl -X POST https://your-domain.com/webhooks/omise \
-H "Content-Type: application/json" \
-d '{"test": "data"}'

Issue: SSL certificate errors

Solutions:

  • Verify SSL certificate is valid and not expired
  • Check certificate chain is complete
  • Ensure certificate matches domain name
  • Use SSL testing tools to diagnose issues
# Check SSL certificate
openssl s_client -connect your-domain.com:443 -servername your-domain.com

# Test SSL with curl
curl -v https://your-domain.com/webhooks/omise

Issue: Webhook signature verification failing

Solutions:

  • Verify you're using the correct webhook signing key
  • Ensure raw request body is used for signature verification
  • Check for character encoding issues
  • Verify HMAC-SHA256 algorithm is implemented correctly
  • See the Security guide for detailed examples

Issue: Webhook timeouts

Solutions:

  • Respond to webhook within 10 seconds
  • Move processing to background jobs/queues
  • Optimize database queries
  • Add timeout monitoring and alerting
// Add timeout protection
app.post('/webhooks/omise', (req, res) => {
// Set response timeout
req.setTimeout(8000, () => {
console.error('Webhook processing timeout');
res.status(200).json({ received: true });
});

// Quick response
res.status(200).json({ received: true });

// Async processing
processWebhookAsync(req.body);
});

Issue: Duplicate webhook events

Solutions:

  • Implement idempotency using event IDs
  • Use database constraints to prevent duplicate processing
  • Track processed event IDs in cache or database
  • See the Retry Logic guide for idempotency patterns

Issue: ngrok tunnel disconnecting

Solutions:

  • Upgrade to ngrok paid plan for persistent URLs
  • Use ngrok agent auto-restart features
  • Set up process manager (PM2, systemd) for stability
  • Consider alternatives like localtunnel or serveo
# Run ngrok with auto-restart (using PM2)
pm2 start ngrok -- http 3000
pm2 save

Debugging Checklistโ€‹

When troubleshooting webhook issues:

  • Verify endpoint URL is correct and accessible
  • Check SSL certificate is valid
  • Confirm webhook is active in dashboard
  • Review Recent Deliveries section for errors
  • Check server logs for incoming requests
  • Verify signature verification implementation
  • Test with curl or webhook testing tools
  • Monitor response times and timeout issues
  • Check for rate limiting or firewall blocking
  • Validate JSON parsing and error handling

Monitoring and Loggingโ€‹

Implement comprehensive logging for webhook debugging:

const winston = require('winston');

const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'webhooks-error.log', level: 'error' }),
new winston.transports.File({ filename: 'webhooks-combined.log' })
]
});

app.post('/webhooks/omise', (req, res) => {
const startTime = Date.now();

logger.info('Webhook received', {
event_id: req.body.id,
event_key: req.body.key,
signature: req.headers['x-omise-signature']
});

try {
// Verify signature
if (!verifySignature(req)) {
logger.error('Invalid signature', {
event_id: req.body.id,
signature: req.headers['x-omise-signature']
});
return res.status(401).json({ error: 'Invalid signature' });
}

res.status(200).json({ received: true });

// Process webhook
processWebhookAsync(req.body)
.then(() => {
logger.info('Webhook processed successfully', {
event_id: req.body.id,
duration: Date.now() - startTime
});
})
.catch((error) => {
logger.error('Webhook processing failed', {
event_id: req.body.id,
error: error.message,
stack: error.stack
});
});
} catch (error) {
logger.error('Webhook error', {
event_id: req.body.id,
error: error.message,
stack: error.stack
});
res.status(500).json({ error: 'Internal server error' });
}
});

FAQโ€‹

How many webhook endpoints can I configure?โ€‹

You can configure multiple webhook endpoints per account. Each endpoint can subscribe to different events, allowing you to route webhooks to different services or environments.

Can I use HTTP instead of HTTPS for testing?โ€‹

No, all webhook endpoints must use HTTPS with valid SSL certificates. For local testing, use ngrok or similar tunneling services to create HTTPS endpoints.

What happens if my endpoint is down?โ€‹

Omise automatically retries failed webhook deliveries using exponential backoff. Webhooks are retried for up to 7 days. See the Retry Logic guide for details.

How do I handle webhook events that arrive out of order?โ€‹

Always check the created_at timestamp in the webhook payload. Store event IDs and timestamps in your database to detect and handle out-of-order events. Process events based on timestamp rather than arrival order.

Can I replay missed webhooks?โ€‹

Yes, you can manually trigger webhook redelivery from the Omise dashboard. Navigate to the webhook endpoint, view Recent Deliveries, and click "Resend" for specific events.

How do I update my webhook endpoint URL?โ€‹

Edit the webhook endpoint in the Omise dashboard. Update the URL and save changes. The webhook signing key remains the same unless you regenerate it.

Should I create different endpoints for test and live modes?โ€‹

Yes, it's recommended to use separate webhook endpoints for test and live modes. This prevents test events from affecting your production systems and simplifies debugging.

What's the maximum payload size for webhooks?โ€‹

Webhook payloads are typically under 100KB. The exact size depends on the event type and amount of data. Ensure your endpoint can handle payloads up to 1MB to accommodate future changes.

How quickly must I respond to webhooks?โ€‹

Respond within 10 seconds to avoid timeouts. Return a 2xx status code immediately and process the webhook asynchronously.

Can I filter which events are sent to my endpoint?โ€‹

Yes, when configuring your webhook endpoint, you can select specific event types or choose "All Events". You can modify event subscriptions at any time in the dashboard.

Next Stepsโ€‹

After setting up your webhook endpoint:

  1. Implement signature verification - See the Security guide for detailed examples
  2. Review event types - Check the Event Types reference for available events
  3. Handle retries and failures - Learn about Retry Logic and idempotency
  4. Test your integration - Create test charges and refunds to verify webhook delivery
  5. Monitor webhook health - Set up logging and alerting for production monitoring