Skip to main content

Webhook Event Types

Complete reference guide to all webhook event types available in Omise, with detailed payload examples and integration guidance.

Overviewโ€‹

Omise sends webhook events for various payment and account activities. Each event contains:

  • Event key: Unique identifier for the event type (e.g., charge.complete)
  • Event ID: Unique identifier for this specific event occurrence
  • Created timestamp: When the event occurred
  • Data object: Full resource object related to the event
  • API version: Version of the Omise API used

Event Categoriesโ€‹

Events are organized into these categories:

  • Charge Events - Payment creation, completion, and status changes
  • Customer Events - Customer profile creation and updates
  • Card Events - Credit/debit card creation and updates
  • Refund Events - Refund creation and completion
  • Transfer Events - Transfer creation, updates, and settlements
  • Recipient Events - Recipient profile creation and updates
  • Dispute Events - Dispute creation, updates, and resolutions
  • Link Events - Payment link creation and payments
  • Schedule Events - Scheduled task creation, completion, and expiration

Charge Eventsโ€‹

Charge events track the lifecycle of payment charges from creation to completion or failure.

charge.createโ€‹

Fired when a new charge is created but not yet processed.

Use cases:

  • Log charge creation for audit trail
  • Track payment initiation metrics
  • Update order status to "processing"

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"livemode": false,
"location": "/events/evnt_test_5xq6zfg18b4bxg37kjh",
"key": "charge.create",
"created_at": "2024-02-06T10:30:00Z",
"data": {
"object": "charge",
"id": "chrg_test_5xq6zfexznf7wjh0aj3",
"location": "/charges/chrg_test_5xq6zfexznf7wjh0aj3",
"amount": 100000,
"net": 96750,
"fee": 3250,
"fee_vat": 0,
"interest": 0,
"interest_vat": 0,
"funding_amount": 100000,
"refunded_amount": 0,
"currency": "THB",
"description": "Order #1234",
"status": "pending",
"capture": true,
"authorized": false,
"paid": false,
"reversed": false,
"refundable": false,
"disputable": false,
"expired": false,
"expires_at": "2024-02-06T10:45:00Z",
"return_uri": "https://example.com/orders/1234",
"authorize_uri": "https://pay.omise.co/payments/paym_test_5xq6zfexznf7wjh0aj3/authorize",
"metadata": {
"order_id": "1234",
"customer_name": "John Doe"
},
"card": {
"object": "card",
"id": "card_test_5xq6zfexznf7wjh0aj3",
"brand": "Visa",
"last_digits": "4242",
"name": "JOHN DOE",
"expiration_month": 12,
"expiration_year": 2025
},
"customer": null,
"ip": "203.0.113.1",
"created_at": "2024-02-06T10:30:00Z"
}
}

Implementation example:

// Node.js
async function handleChargeCreate(event) {
const charge = event.data;

console.log(`Charge created: ${charge.id}`);
console.log(`Amount: ${charge.amount} ${charge.currency}`);
console.log(`Status: ${charge.status}`);

// Update order status
await db.orders.update(
{ id: charge.metadata.order_id },
{
status: 'processing',
charge_id: charge.id,
payment_method: charge.card ? 'card' : 'other'
}
);

// Log for analytics
await analytics.track('charge_created', {
charge_id: charge.id,
amount: charge.amount,
currency: charge.currency
});
}
# Python
async def handle_charge_create(event):
charge = event['data']

print(f"Charge created: {charge['id']}")
print(f"Amount: {charge['amount']} {charge['currency']}")
print(f"Status: {charge['status']}")

# Update order status
await db.orders.update(
{'id': charge['metadata']['order_id']},
{
'status': 'processing',
'charge_id': charge['id'],
'payment_method': 'card' if charge.get('card') else 'other'
}
)

# Log for analytics
await analytics.track('charge_created', {
'charge_id': charge['id'],
'amount': charge['amount'],
'currency': charge['currency']
})

charge.completeโ€‹

Fired when a charge is successfully completed and payment is confirmed.

Use cases:

  • Fulfill orders and ship products
  • Send payment confirmation emails
  • Update accounting systems
  • Grant access to digital products

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"livemode": false,
"location": "/events/evnt_test_5xq6zfg18b4bxg37kjh",
"key": "charge.complete",
"created_at": "2024-02-06T10:30:15Z",
"data": {
"object": "charge",
"id": "chrg_test_5xq6zfexznf7wjh0aj3",
"location": "/charges/chrg_test_5xq6zfexznf7wjh0aj3",
"amount": 100000,
"net": 96750,
"fee": 3250,
"currency": "THB",
"description": "Order #1234",
"status": "successful",
"capture": true,
"authorized": true,
"paid": true,
"reversed": false,
"refundable": true,
"transaction": "trxn_test_5xq6zfg1j2k3l4m5n6o",
"metadata": {
"order_id": "1234",
"customer_name": "John Doe"
},
"card": {
"object": "card",
"id": "card_test_5xq6zfexznf7wjh0aj3",
"brand": "Visa",
"last_digits": "4242"
},
"created_at": "2024-02-06T10:30:00Z"
}
}

Implementation example:

# Ruby
def handle_charge_complete(event)
charge = event['data']

puts "Charge completed: #{charge['id']}"
puts "Amount: #{charge['amount']} #{charge['currency']}"
puts "Transaction: #{charge['transaction']}"

# Fulfill order
order = Order.find_by(id: charge['metadata']['order_id'])
order.update!(
status: 'paid',
paid_at: Time.current,
transaction_id: charge['transaction']
)

# Send confirmation email
OrderMailer.payment_confirmation(order).deliver_later

# Process fulfillment
FulfillmentService.process_order(order)

# Update inventory
order.line_items.each do |item|
item.product.decrement!(:stock_quantity, item.quantity)
end
end
// PHP
function handleChargeComplete($event) {
$charge = $event['data'];

error_log("Charge completed: " . $charge['id']);
error_log("Amount: " . $charge['amount'] . " " . $charge['currency']);

// Fulfill order
$orderId = $charge['metadata']['order_id'];
$order = Order::find($orderId);
$order->status = 'paid';
$order->paid_at = date('Y-m-d H:i:s');
$order->transaction_id = $charge['transaction'];
$order->save();

// Send confirmation email
Mail::send('emails.payment-confirmation', ['order' => $order], function($message) use ($order) {
$message->to($order->customer_email);
$message->subject('Payment Confirmation - Order #' . $order->id);
});

// Process fulfillment
FulfillmentService::processOrder($order);
}
// Go
func handleChargeComplete(event Event) error {
charge := event.Data

log.Printf("Charge completed: %s", charge.ID)
log.Printf("Amount: %d %s", charge.Amount, charge.Currency)

// Fulfill order
orderID := charge.Metadata["order_id"]
order, err := db.GetOrder(orderID)
if err != nil {
return fmt.Errorf("failed to get order: %w", err)
}

order.Status = "paid"
order.PaidAt = time.Now()
order.TransactionID = charge.Transaction

if err := db.UpdateOrder(order); err != nil {
return fmt.Errorf("failed to update order: %w", err)
}

// Send confirmation email
if err := mailer.SendPaymentConfirmation(order); err != nil {
log.Printf("Failed to send confirmation email: %v", err)
}

// Process fulfillment
go fulfillment.ProcessOrder(order)

return nil
}

charge.updateโ€‹

Fired when charge details are updated (e.g., metadata changes).

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "charge.update",
"created_at": "2024-02-06T10:35:00Z",
"data": {
"object": "charge",
"id": "chrg_test_5xq6zfexznf7wjh0aj3",
"status": "successful",
"metadata": {
"order_id": "1234",
"customer_name": "John Doe",
"shipping_status": "shipped",
"tracking_number": "1Z999AA10123456784"
}
}
}

charge.captureโ€‹

Fired when an authorized charge is captured.

Use cases:

  • Confirm hotel bookings
  • Complete restaurant pre-authorizations
  • Finalize rental charges

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "charge.capture",
"created_at": "2024-02-06T12:00:00Z",
"data": {
"object": "charge",
"id": "chrg_test_5xq6zfexznf7wjh0aj3",
"amount": 100000,
"capture": true,
"authorized": true,
"paid": true,
"status": "successful",
"captured_at": "2024-02-06T12:00:00Z"
}
}

charge.reverseโ€‹

Fired when an authorized charge is reversed without capture.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "charge.reverse",
"created_at": "2024-02-06T11:00:00Z",
"data": {
"object": "charge",
"id": "chrg_test_5xq6zfexznf7wjh0aj3",
"amount": 100000,
"reversed": true,
"status": "reversed",
"reversed_at": "2024-02-06T11:00:00Z"
}
}

charge.expireโ€‹

Fired when an unpaid charge expires.

Use cases:

  • Cancel pending orders
  • Release inventory holds
  • Notify customers of expired payment links

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "charge.expire",
"created_at": "2024-02-06T10:45:00Z",
"data": {
"object": "charge",
"id": "chrg_test_5xq6zfexznf7wjh0aj3",
"amount": 100000,
"status": "expired",
"expired": true,
"expires_at": "2024-02-06T10:45:00Z",
"paid": false
}
}

Implementation example:

// Node.js
async function handleChargeExpire(event) {
const charge = event.data;
const orderId = charge.metadata.order_id;

// Cancel order
await db.orders.update(
{ id: orderId },
{
status: 'cancelled',
cancelled_reason: 'payment_expired',
cancelled_at: new Date()
}
);

// Release inventory
const order = await db.orders.findOne({ id: orderId });
for (const item of order.items) {
await db.products.increment(
{ id: item.product_id },
{ available_quantity: item.quantity }
);
}

// Notify customer
await emailService.send({
to: order.customer_email,
template: 'payment-expired',
data: { order, charge }
});
}

Customer Eventsโ€‹

Customer events track changes to customer profiles.

customer.createโ€‹

Fired when a new customer is created.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "customer.create",
"created_at": "2024-02-06T10:00:00Z",
"data": {
"object": "customer",
"id": "cust_test_5xq6zfexznf7wjh0aj3",
"location": "/customers/cust_test_5xq6zfexznf7wjh0aj3",
"email": "john.doe@example.com",
"description": "John Doe - Premium Customer",
"metadata": {
"user_id": "12345",
"plan": "premium"
},
"created_at": "2024-02-06T10:00:00Z"
}
}

customer.updateโ€‹

Fired when customer details are updated.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "customer.update",
"created_at": "2024-02-06T11:00:00Z",
"data": {
"object": "customer",
"id": "cust_test_5xq6zfexznf7wjh0aj3",
"email": "john.doe@example.com",
"description": "John Doe - Enterprise Customer",
"metadata": {
"user_id": "12345",
"plan": "enterprise",
"upgraded_at": "2024-02-06T11:00:00Z"
}
}
}

customer.destroyโ€‹

Fired when a customer is deleted.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "customer.destroy",
"created_at": "2024-02-06T12:00:00Z",
"data": {
"object": "customer",
"id": "cust_test_5xq6zfexznf7wjh0aj3",
"deleted": true
}
}

Card Eventsโ€‹

Card events track changes to stored payment cards.

card.createโ€‹

Fired when a card is added to a customer.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "card.create",
"created_at": "2024-02-06T10:00:00Z",
"data": {
"object": "card",
"id": "card_test_5xq6zfexznf7wjh0aj3",
"location": "/customers/cust_test_5xq6zfexznf7wjh0aj3/cards/card_test_5xq6zfexznf7wjh0aj3",
"country": "TH",
"city": "Bangkok",
"postal_code": "10240",
"financing": "",
"brand": "Visa",
"last_digits": "4242",
"fingerprint": "XjlLNCwGLXJKLnBMckNQSUpYWE1PTE5M",
"name": "JOHN DOE",
"expiration_month": 12,
"expiration_year": 2025,
"security_code_check": true,
"created_at": "2024-02-06T10:00:00Z"
}
}

card.updateโ€‹

Fired when card details are updated.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "card.update",
"created_at": "2024-02-06T11:00:00Z",
"data": {
"object": "card",
"id": "card_test_5xq6zfexznf7wjh0aj3",
"name": "JOHN DOE JR",
"expiration_month": 12,
"expiration_year": 2026
}
}

card.destroyโ€‹

Fired when a card is removed from a customer.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "card.destroy",
"created_at": "2024-02-06T12:00:00Z",
"data": {
"object": "card",
"id": "card_test_5xq6zfexznf7wjh0aj3",
"deleted": true
}
}

Refund Eventsโ€‹

Refund events track refund creation and processing.

refund.createโ€‹

Fired when a refund is created.

Use cases:

  • Update order status to refunded
  • Process inventory returns
  • Send refund confirmation emails
  • Update accounting records

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "refund.create",
"created_at": "2024-02-06T14:00:00Z",
"data": {
"object": "refund",
"id": "rfnd_test_5xq6zfexznf7wjh0aj3",
"location": "/charges/chrg_test_5xq6zfexznf7wjh0aj3/refunds/rfnd_test_5xq6zfexznf7wjh0aj3",
"amount": 50000,
"currency": "THB",
"charge": "chrg_test_5xq6zfexznf7wjh0aj3",
"transaction": "trxn_test_5xq6zfg1j2k3l4m5n6o",
"status": "pending",
"metadata": {
"reason": "customer_request",
"requested_by": "support@example.com"
},
"created_at": "2024-02-06T14:00:00Z"
}
}

Implementation example:

# Python
async def handle_refund_create(event):
refund = event['data']
charge_id = refund['charge']

# Get original charge
charge = await omise.Charge.retrieve(charge_id)
order_id = charge['metadata']['order_id']

# Update order status
await db.orders.update(
{'id': order_id},
{
'status': 'refunded',
'refund_id': refund['id'],
'refund_amount': refund['amount'],
'refund_reason': refund['metadata'].get('reason'),
'refunded_at': datetime.now()
}
)

# Process inventory return
order = await db.orders.find_one({'id': order_id})
for item in order['items']:
await db.products.update(
{'id': item['product_id']},
{'$inc': {'available_quantity': item['quantity']}}
)

# Send confirmation email
await email_service.send(
to=order['customer_email'],
template='refund-confirmation',
data={'order': order, 'refund': refund}
)

refund.updateโ€‹

Fired when refund status changes.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "refund.update",
"created_at": "2024-02-06T14:05:00Z",
"data": {
"object": "refund",
"id": "rfnd_test_5xq6zfexznf7wjh0aj3",
"amount": 50000,
"currency": "THB",
"charge": "chrg_test_5xq6zfexznf7wjh0aj3",
"status": "succeeded",
"funded_at": "2024-02-06T14:05:00Z"
}
}

Transfer Eventsโ€‹

Transfer events track fund transfers to your bank account.

transfer.createโ€‹

Fired when a transfer is created.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "transfer.create",
"created_at": "2024-02-06T00:00:00Z",
"data": {
"object": "transfer",
"id": "trsf_test_5xq6zfexznf7wjh0aj3",
"location": "/transfers/trsf_test_5xq6zfexznf7wjh0aj3",
"amount": 965000,
"currency": "THB",
"fee": 35000,
"sent": false,
"paid": false,
"recipient": "recp_test_5xq6zfexznf7wjh0aj3",
"bank_account": {
"object": "bank_account",
"brand": "kbank",
"number": "1234567890",
"name": "JOHN DOE"
},
"created_at": "2024-02-06T00:00:00Z"
}
}

transfer.updateโ€‹

Fired when transfer status changes.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "transfer.update",
"created_at": "2024-02-07T09:00:00Z",
"data": {
"object": "transfer",
"id": "trsf_test_5xq6zfexznf7wjh0aj3",
"amount": 965000,
"sent": true,
"paid": true,
"sent_at": "2024-02-07T09:00:00Z"
}
}

transfer.destroyโ€‹

Fired when a scheduled transfer is cancelled.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "transfer.destroy",
"created_at": "2024-02-06T12:00:00Z",
"data": {
"object": "transfer",
"id": "trsf_test_5xq6zfexznf7wjh0aj3",
"deleted": true
}
}

Recipient Eventsโ€‹

Recipient events track changes to transfer recipients.

recipient.createโ€‹

Fired when a new recipient is created.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "recipient.create",
"created_at": "2024-02-06T10:00:00Z",
"data": {
"object": "recipient",
"id": "recp_test_5xq6zfexznf7wjh0aj3",
"location": "/recipients/recp_test_5xq6zfexznf7wjh0aj3",
"verified": false,
"active": true,
"name": "John Doe",
"email": "john.doe@example.com",
"type": "individual",
"tax_id": "1234567890123",
"bank_account": {
"object": "bank_account",
"brand": "kbank",
"number": "1234567890",
"name": "JOHN DOE"
},
"created_at": "2024-02-06T10:00:00Z"
}
}

recipient.updateโ€‹

Fired when recipient details are updated.

recipient.destroyโ€‹

Fired when a recipient is deleted.

Dispute Eventsโ€‹

Dispute events track chargeback and dispute lifecycle.

dispute.createโ€‹

Fired when a dispute is opened by a customer.

Use cases:

  • Alert finance team
  • Gather evidence for dispute response
  • Suspend related services if needed
  • Track dispute metrics

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "dispute.create",
"created_at": "2024-02-10T10:00:00Z",
"data": {
"object": "dispute",
"id": "dspt_test_5xq6zfexznf7wjh0aj3",
"location": "/disputes/dspt_test_5xq6zfexznf7wjh0aj3",
"amount": 100000,
"currency": "THB",
"status": "open",
"charge": "chrg_test_5xq6zfexznf7wjh0aj3",
"reason_code": "fraudulent",
"reason_message": "Customer claims transaction was fraudulent",
"respond_by": "2024-02-17T23:59:59Z",
"created_at": "2024-02-10T10:00:00Z",
"metadata": {}
}
}

Implementation example:

# Ruby
def handle_dispute_create(event)
dispute = event['data']
charge_id = dispute['charge']

# Get charge and order details
charge = Omise::Charge.retrieve(charge_id)
order = Order.find_by(charge_id: charge_id)

# Create internal dispute record
internal_dispute = Dispute.create!(
omise_dispute_id: dispute['id'],
order: order,
amount: dispute['amount'],
reason_code: dispute['reason_code'],
reason_message: dispute['reason_message'],
respond_by: dispute['respond_by'],
status: 'pending_response'
)

# Alert finance team
FinanceMailer.dispute_alert(internal_dispute).deliver_now

# Create Slack notification
SlackNotifier.notify(
channel: '#disputes',
message: "New dispute created for Order ##{order.id}",
fields: {
'Dispute ID': dispute['id'],
'Amount': "#{dispute['amount']} #{dispute['currency']}",
'Reason': dispute['reason_code'],
'Respond By': dispute['respond_by']
}
)
end

dispute.updateโ€‹

Fired when dispute status changes.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "dispute.update",
"created_at": "2024-02-15T14:00:00Z",
"data": {
"object": "dispute",
"id": "dspt_test_5xq6zfexznf7wjh0aj3",
"amount": 100000,
"currency": "THB",
"status": "won",
"charge": "chrg_test_5xq6zfexznf7wjh0aj3",
"closed_at": "2024-02-15T14:00:00Z"
}
}

dispute.closeโ€‹

Fired when a dispute is closed (won, lost, or withdrawn).

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "dispute.close",
"created_at": "2024-02-15T14:00:00Z",
"data": {
"object": "dispute",
"id": "dspt_test_5xq6zfexznf7wjh0aj3",
"amount": 100000,
"status": "won",
"closed_at": "2024-02-15T14:00:00Z"
}
}

Payment link events track payment link lifecycle.

link.createโ€‹

Fired when a payment link is created.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "link.create",
"created_at": "2024-02-06T10:00:00Z",
"data": {
"object": "link",
"id": "link_test_5xq6zfexznf7wjh0aj3",
"location": "/links/link_test_5xq6zfexznf7wjh0aj3",
"amount": 100000,
"currency": "THB",
"title": "Invoice #1234",
"description": "Payment for Order #1234",
"payment_uri": "https://pay.omise.co/link/link_test_5xq6zfexznf7wjh0aj3",
"used": false,
"multiple": false,
"created_at": "2024-02-06T10:00:00Z"
}
}

link.payment.createโ€‹

Fired when a payment is made through a payment link.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "link.payment.create",
"created_at": "2024-02-06T11:00:00Z",
"data": {
"object": "charge",
"id": "chrg_test_5xq6zfexznf7wjh0aj3",
"amount": 100000,
"status": "successful",
"link": "link_test_5xq6zfexznf7wjh0aj3"
}
}

Schedule Eventsโ€‹

Schedule events track scheduled tasks like recurring charges.

schedule.createโ€‹

Fired when a schedule is created.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "schedule.create",
"created_at": "2024-02-06T10:00:00Z",
"data": {
"object": "schedule",
"id": "schd_test_5xq6zfexznf7wjh0aj3",
"location": "/schedules/schd_test_5xq6zfexznf7wjh0aj3",
"status": "active",
"every": 1,
"period": "month",
"on": {
"days_of_month": [1]
},
"start_date": "2024-02-01",
"end_date": "2024-12-31",
"charge": {
"amount": 99900,
"currency": "THB",
"customer": "cust_test_5xq6zfexznf7wjh0aj3",
"description": "Monthly subscription"
},
"created_at": "2024-02-06T10:00:00Z"
}
}

schedule.updateโ€‹

Fired when schedule details are updated.

schedule.expiration.closeโ€‹

Fired when a schedule reaches its end date.

Payload example:

{
"object": "event",
"id": "evnt_test_5xq6zfg18b4bxg37kjh",
"key": "schedule.expiration.close",
"created_at": "2024-12-31T23:59:59Z",
"data": {
"object": "schedule",
"id": "schd_test_5xq6zfexznf7wjh0aj3",
"status": "expired",
"end_date": "2024-12-31"
}
}

Real-World Scenariosโ€‹

E-commerce Order Processingโ€‹

Complete webhook integration for order fulfillment:

// Node.js - E-commerce webhook handler
const webhookHandlers = {
'charge.create': handleChargeCreate,
'charge.complete': handleChargeComplete,
'charge.expire': handleChargeExpire,
'refund.create': handleRefundCreate
};

async function handleChargeCreate(charge) {
await Order.updateOne(
{ _id: charge.metadata.order_id },
{
status: 'payment_pending',
charge_id: charge.id
}
);
}

async function handleChargeComplete(charge) {
const order = await Order.findOne({ charge_id: charge.id });

// Update order status
order.status = 'paid';
order.paid_at = new Date();
await order.save();

// Reduce inventory
for (const item of order.items) {
await Product.updateOne(
{ _id: item.product_id },
{ $inc: { stock: -item.quantity } }
);
}

// Create shipment
const shipment = await Shipment.create({
order_id: order._id,
status: 'pending',
items: order.items
});

// Send confirmations
await emailService.sendOrderConfirmation(order);
await smsService.sendPaymentConfirmation(order.phone);
}

async function handleChargeExpire(charge) {
const order = await Order.findOne({ charge_id: charge.id });

order.status = 'cancelled';
order.cancelled_reason = 'payment_expired';
await order.save();

// Release inventory holds
for (const item of order.items) {
await Product.updateOne(
{ _id: item.product_id },
{ $inc: { reserved: -item.quantity } }
);
}

await emailService.sendPaymentExpiredNotification(order);
}

async function handleRefundCreate(refund) {
const charge = await omise.charges.retrieve(refund.charge);
const order = await Order.findOne({ charge_id: charge.id });

order.status = 'refunded';
order.refund_amount = refund.amount;
order.refunded_at = new Date();
await order.save();

// Return inventory
for (const item of order.items) {
await Product.updateOne(
{ _id: item.product_id },
{ $inc: { stock: item.quantity } }
);
}

await emailService.sendRefundConfirmation(order, refund);
}

Subscription Managementโ€‹

Handling recurring payments and subscription lifecycle:

# Python - Subscription webhook handler
async def handle_subscription_webhooks(event):
handlers = {
'charge.complete': handle_subscription_payment,
'charge.failed': handle_subscription_failure,
'schedule.expiration.close': handle_subscription_expiration
}

handler = handlers.get(event['key'])
if handler:
await handler(event['data'])

async def handle_subscription_payment(charge):
customer_id = charge['customer']
subscription = await db.subscriptions.find_one({
'customer_id': customer_id,
'status': 'active'
})

if not subscription:
return

# Extend subscription
next_billing_date = subscription['next_billing_date'] + timedelta(days=30)
await db.subscriptions.update_one(
{'_id': subscription['_id']},
{
'$set': {
'last_payment_date': datetime.now(),
'next_billing_date': next_billing_date,
'failed_attempts': 0
},
'$push': {
'payment_history': {
'charge_id': charge['id'],
'amount': charge['amount'],
'paid_at': datetime.now()
}
}
}
)

# Send renewal confirmation
await email_service.send_subscription_renewal(subscription)

# Grant access
await service_access.extend(customer_id, days=30)

async def handle_subscription_failure(charge):
customer_id = charge['customer']
subscription = await db.subscriptions.find_one({'customer_id': customer_id})

failed_attempts = subscription.get('failed_attempts', 0) + 1

await db.subscriptions.update_one(
{'_id': subscription['_id']},
{
'$set': {'failed_attempts': failed_attempts},
'$push': {
'payment_history': {
'charge_id': charge['id'],
'status': 'failed',
'failed_at': datetime.now()
}
}
}
)

if failed_attempts >= 3:
# Suspend subscription after 3 failures
await db.subscriptions.update_one(
{'_id': subscription['_id']},
{'$set': {'status': 'suspended'}}
)
await service_access.suspend(customer_id)
await email_service.send_subscription_suspended(subscription)
else:
# Send payment failure notification
await email_service.send_payment_failed(subscription, failed_attempts)

Multi-tenant Platformโ€‹

Managing webhooks for multiple merchants:

// Go - Multi-tenant webhook handler
type WebhookHandler struct {
db *mongo.Database
cache *redis.Client
}

func (h *WebhookHandler) HandleWebhook(event Event) error {
// Extract tenant ID from metadata
tenantID := event.Data.Metadata["tenant_id"]
if tenantID == "" {
return fmt.Errorf("missing tenant_id in metadata")
}

// Get tenant configuration
tenant, err := h.getTenant(tenantID)
if err != nil {
return fmt.Errorf("failed to get tenant: %w", err)
}

// Route to appropriate handler
switch event.Key {
case "charge.complete":
return h.handleChargeComplete(tenant, event.Data)
case "refund.create":
return h.handleRefundCreate(tenant, event.Data)
case "dispute.create":
return h.handleDisputeCreate(tenant, event.Data)
}

return nil
}

func (h *WebhookHandler) handleChargeComplete(tenant Tenant, charge Charge) error {
// Update tenant's order
order, err := h.db.Collection("orders").UpdateOne(
context.Background(),
bson.M{
"tenant_id": tenant.ID,
"charge_id": charge.ID,
},
bson.M{
"$set": bson.M{
"status": "paid",
"paid_at": time.Now(),
"updated_at": time.Now(),
},
},
)

if err != nil {
return fmt.Errorf("failed to update order: %w", err)
}

// Notify tenant via their configured webhook
if tenant.WebhookURL != "" {
go h.notifyTenant(tenant, "payment.completed", charge)
}

// Update tenant analytics
h.updateTenantAnalytics(tenant.ID, charge.Amount)

return nil
}

func (h *WebhookHandler) notifyTenant(tenant Tenant, eventType string, data interface{}) {
payload := map[string]interface{}{
"event": eventType,
"data": data,
"timestamp": time.Now(),
}

jsonData, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", tenant.WebhookURL, bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Tenant-ID", tenant.ID)

client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
log.Printf("Failed to notify tenant %s: %v", tenant.ID, err)
return
}
defer resp.Body.Close()

if resp.StatusCode >= 400 {
log.Printf("Tenant webhook failed: %s (status %d)", tenant.ID, resp.StatusCode)
}
}

Best Practicesโ€‹

Event Processingโ€‹

  • Verify signatures for all webhook events before processing
  • Respond quickly (within 10 seconds) to avoid retries
  • Process asynchronously using background jobs or message queues
  • Implement idempotency to handle duplicate events safely
  • Log all events for debugging and audit trails

Error Handlingโ€‹

// Comprehensive error handling
app.post('/webhooks/omise', async (req, res) => {
const eventId = req.body.id;
const eventKey = req.body.key;

try {
// Verify signature
if (!verifySignature(req)) {
logger.error('Invalid signature', { eventId, eventKey });
return res.status(401).json({ error: 'Invalid signature' });
}

// Check for duplicate events
const isDuplicate = await cache.exists(`event:${eventId}`);
if (isDuplicate) {
logger.info('Duplicate event ignored', { eventId });
return res.status(200).json({ received: true });
}

// Mark as received
await cache.setex(`event:${eventId}`, 86400, '1');

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

// Process asynchronously
await queue.add('webhook', req.body, {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 }
});

} catch (error) {
logger.error('Webhook processing error', {
eventId,
eventKey,
error: error.message,
stack: error.stack
});
res.status(500).json({ error: 'Internal server error' });
}
});

Data Validationโ€‹

# Validate webhook data
from marshmallow import Schema, fields, ValidationError

class ChargeSchema(Schema):
id = fields.Str(required=True)
amount = fields.Int(required=True)
currency = fields.Str(required=True)
status = fields.Str(required=True)
metadata = fields.Dict()

def validate_webhook_data(event):
try:
if event['key'].startswith('charge.'):
ChargeSchema().load(event['data'])
# Add more schemas for other event types
return True
except ValidationError as err:
logger.error(f'Invalid webhook data: {err.messages}')
return False

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

if not validate_webhook_data(event):
return jsonify({'error': 'Invalid data'}), 400

# Process valid event
process_webhook(event)
return jsonify({'received': True}), 200

Testing Event Handlersโ€‹

# RSpec tests for webhook handlers
RSpec.describe 'Webhook Handlers' do
describe 'charge.complete' do
let(:charge_complete_event) do
{
'key' => 'charge.complete',
'data' => {
'id' => 'chrg_test_123',
'amount' => 100000,
'currency' => 'THB',
'status' => 'successful',
'metadata' => { 'order_id' => '1234' }
}
}
end

it 'updates order status to paid' do
order = create(:order, id: '1234', status: 'pending')

handle_webhook(charge_complete_event)

expect(order.reload.status).to eq('paid')
end

it 'sends confirmation email' do
order = create(:order, id: '1234')

expect {
handle_webhook(charge_complete_event)
}.to have_enqueued_job(OrderConfirmationEmailJob)
end

it 'reduces product inventory' do
order = create(:order, id: '1234')
product = create(:product, stock: 10)
create(:order_item, order: order, product: product, quantity: 2)

handle_webhook(charge_complete_event)

expect(product.reload.stock).to eq(8)
end
end
end

Troubleshootingโ€‹

Event Not Receivedโ€‹

Check these common issues:

  1. Webhook endpoint not configured - Verify in dashboard
  2. Endpoint not accessible - Test with curl
  3. SSL certificate invalid - Check certificate validity
  4. Firewall blocking requests - Check security rules
  5. Event type not subscribed - Verify event subscriptions

Duplicate Eventsโ€‹

Implement idempotency to handle duplicates:

// Using Redis for idempotency
async function processWebhook(event) {
const eventId = event.id;
const key = `webhook:processed:${eventId}`;

// Check if already processed
const exists = await redis.exists(key);
if (exists) {
console.log(`Event ${eventId} already processed`);
return;
}

// Process event
await handleEvent(event);

// Mark as processed (expire after 7 days)
await redis.setex(key, 7 * 24 * 3600, '1');
}

Slow Processingโ€‹

Optimize webhook processing:

  • Use message queues for async processing
  • Batch database operations
  • Cache frequently accessed data
  • Profile slow operations
  • Scale horizontally if needed

FAQโ€‹

What happens if I don't respond to a webhook in time?โ€‹

Omise considers the delivery failed and will retry according to the retry schedule. Respond within 10 seconds to avoid retries.

Can I receive webhooks for test mode events?โ€‹

Yes, configure separate webhook endpoints for test and live modes. Test webhooks are sent to test mode endpoints.

How do I know which events to subscribe to?โ€‹

Subscribe to "All Events" initially, then monitor which events you're actually processing. Refine subscriptions to reduce noise.

Are webhooks guaranteed to arrive in order?โ€‹

No, webhooks may arrive out of order due to network conditions or retries. Always check the created_at timestamp to determine event sequence.

What should I do if I receive an unknown event type?โ€‹

Log the event and respond with 200 OK. This ensures forward compatibility when Omise adds new event types.

Can I manually trigger webhook retries?โ€‹

Yes, use the Omise dashboard to view recent deliveries and manually resend specific events.

How long are webhook events stored?โ€‹

Webhook delivery logs are available in the dashboard for 30 days. Store critical event data in your system for longer retention.

What's the maximum size of event metadata?โ€‹

Metadata is limited to 4KB per object. Keep metadata concise and use IDs to reference larger data in your system.

Do webhooks include the full object data?โ€‹

Yes, webhook events include the complete object (charge, refund, etc.) with all fields populated.

Can I test webhooks without ngrok?โ€‹

Yes, you can use services like RequestBin, webhook.site, or create test endpoints on cloud platforms for testing.

Next Stepsโ€‹

Now that you understand webhook event types:

  1. Implement event handlers for your business logic
  2. Add signature verification for security
  3. Set up idempotency to handle retries
  4. Test with real events in test mode
  5. Monitor webhook health in production