Webhooks
Topics covered on this page
Webhooks
Overview
Webhooks are HTTP POST requests sent from our servers to an HTTPS endpoint of your choice when specific events occur on your account. Each request includes an event object containing a data payload for the triggering action.
Webhooks enable asynchronous, real-time notification when important events occur on your account. Instead of repeatedly polling the API to check for status changes, webhooks push notifications directly to your server the moment an event happens.
Key benefits:
- Efficiency: Eliminates the need for continuous polling, reducing unnecessary API calls and server load
- Real-time updates: Receive instant notifications when events occur, such as successful charge completions, refunds, or disputes
- Automation: Triggers automated workflows in your application immediately on receiving an event, such as updating order statuses, sending customer emails, or reconciling accounts
- Reliability: Ensures you never miss critical events, even during periods when your application might not be actively checking for updates
Configuration
Configure webhook endpoints separately for test and live modes:
- Test mode: https://dashboard.omise.co/test/webhooks
- Live mode: https://dashboard.omise.co/live/webhooks
Viewing Event Data
Access all webhook event data through:
- The dashboard under
Events
. Click the Account icon on the right pane and select Events. - The Events API
Serialization
The event object in the POST request (or returned via the Events API) is always serialized according to the default API Version as of the event, regardless of the Omise-Version header value sent in the triggering request.
Example
For an account set at version 2017-11-02:
- A charge is created using
curlwith-H "Omise-Version: 2019-05-29" - The request generates a
charge.createevent with an embeddedchargeobject - The object is serialized according to version
2017-11-02of the Charge API, not version2019-05-29 - Even after updating your account to version
2019-05-29, the event remains serialized according to version2017-11-02
Requirements and Best Practices
Technical Requirements
- URLs must use HTTPS
- URLs must use a valid SSL certificate (self-signed certificates are not supported)
- Verify your SSL certificate using SSL Labs
- For free SSL certificates, visit Let's Encrypt
Security Best Practices
Recommended: Signature Verification
We strongly recommend implementing signature verification as the primary security measure to ensure the authenticity of webhooks. Signature verification cryptographically confirms that requests originate from Omise and have not been tampered with, providing the strongest protection against malicious actors attempting to send fraudulent webhooks to your endpoint. Refer to the Protecting Your Endpoints section for implementation details.
Alternative: Event Verification
If signature verification is not feasible for your implementation, use event verification as an alternative. Upon receiving a webhook event (e.g., charge.complete), use the resource ID to perform an independent GET request to confirm the status. Note that this method does not prevent your endpoint from receiving fraudulent requests; it only enables you to verify the authenticity of the event data after it has been received.
Example event verification workflow:
1. Receive `charge.complete` webhook
2. Extract Charge ID from the webhook payload
3. Perform GET request to `/charges/chrg_test_no1t4tnemucod0e51mo`
4. Verify the charge status independently
Supported Events
Card Events
| Event Name | Trigger |
|---|---|
card.destroy |
Card has been destroyed |
card.update |
Card has been updated |
Charge Events
| Event Name | Trigger |
|---|---|
charge.capture |
Charge has been captured (manual capture only) |
charge.complete |
Charge has been completed successfully (Note: completing non-3DS card charges does not trigger this webhook) |
charge.create |
Charge has been created |
charge.expire |
Charge has expired (Barcode Alipay only) |
charge.reverse |
Charge has been reversed (manual reversal only) |
charge.update |
Charge has been updated |
Customer Events
| Event Name | Trigger |
|---|---|
customer.create |
Customer has been created |
customer.destroy |
Customer has been destroyed |
customer.update |
Customer has been updated |
customer.update.card |
Card has been updated implicitly through a customer |
Dispute Events
| Event Name | Trigger |
|---|---|
dispute.accept |
Dispute has been accepted |
dispute.close |
Dispute has been closed |
dispute.create |
Dispute has been opened |
dispute.update |
Dispute has been updated |
Link Events
| Event Name | Trigger |
|---|---|
link.create |
Link has been created |
Linked Account Events
| Event Name | Trigger |
|---|---|
linked_account.create |
Linked account has been created |
linked_account.complete |
Linked account has been registered by the customer |
Recipient Events
| Event Name | Trigger |
|---|---|
recipient.activate |
Recipient has been activated |
recipient.create |
Recipient has been created |
recipient.deactivate |
Recipient has been deactivated |
recipient.destroy |
Recipient has been destroyed |
recipient.update |
Recipient has been updated |
recipient.verify |
Recipient has been verified |
Refund Events
| Event Name | Trigger |
|---|---|
refund.create |
Refund has been created |
Schedule Events
| Event Name | Trigger |
|---|---|
schedule.create |
Schedule has been created |
schedule.destroy |
Schedule has been destroyed |
schedule.expire |
Schedule has expired |
schedule.expiring |
Schedule will expire soon |
schedule.suspend |
Schedule has been suspended |
Transfer Events
| Event Name | Trigger |
|---|---|
transfer.create |
Transfer has been created |
transfer.destroy |
Transfer has been destroyed |
transfer.fail |
Transfer has been marked as failed |
transfer.pay |
Transfer has been marked as paid |
transfer.send |
Transfer has been marked as sent |
transfer.update |
Transfer has been updated |
Dynamic Webhooks
All Omise accounts include a single configurable static webhook endpoint. By default, notifications for all events are sent to this endpoint.
The dynamic webhooks feature allows you to specify custom webhook endpoints on a per-charge basis by passing the webhook_endpoints parameter when creating a charge.
Behavior
When webhook_endpoints is specified:
- All events for the specific charge are sent to the specified
webhook_endpoints - Events are not sent to the static webhook
When webhook_endpoints is not specified:
- Event notifications are sent to the account's static webhook
Protecting Your Endpoints
Omise uses the HMAC-SHA256 algorithm to sign webhook payloads cryptographically. When a webhook secret is configured for your account, a digital signature is included in the webhook headers. Verify the signature to ensure the authenticity and integrity of webhook requests.
Webhook Signature Headers
| Header | Description |
|---|---|
Omise-Signature |
Hex-encoded HMAC signature generated by Omise. During secret rotation, the header contains two comma-separated signatures, each corresponding to one of the secrets. |
Omise-Signature-Timestamp |
Unix timestamp when the signature was generated |
Example headers:
Omise-Signature: 072cc0d8b49d49ce7119857279b1e36a9efa25fadb468d0126628064d4062c83
Omise-Signature-Timestamp: 1758696391
// During rotation
Omise-Signature: 072cc0d8b49d49ce7119857279b1e36a9efa25fadb468d0126628064d4062c83,4bb1e14023f53d076e598245e5e16fd96a94d46072149c483815718eecf2a38d
How to Verify Webhook Signatures
Step 1: Retrieve the Signatures
Extract the Omise-Signature and Omise-Signature-Timestamp headers from the webhook request.
Step 2: Prepare the Signed Payload
Construct the payload for HMAC signature generation by concatenating:
- The value of the
Omise-Signature-Timestampheader - A dot (
.) - The UTF-8 encoded raw webhook request body
Format:
<TIMESTAMP>.<RAW_BODY>
Step 3: Decode the Webhook Secret
The webhook secret is a Base64-encoded HMAC secret key. Decode it before proceeding to the next step.
Step 4: Compute the Expected Signature
Compute the HMAC-SHA256 digest of the signed payload using your decoded secret, then encode the result as a hexadecimal string.
Step 5: Compare the Signatures
- Iterate over all signatures in the
Omise-Signatureheader - Compare each signature against the expected signature
- Reject the webhook request if none of the signatures match
- If a signature matches, you may proceed with processing the webhook immediately, or optionally validate the timestamp first for additional security against replay attacks
Important security considerations:
- Timing attack protection: Use constant-time string comparison algorithms when comparing signatures to prevent timing attacks
- Replay attack protection (optional): For enhanced security, validate timestamps by computing the difference between your current system time and the received timestamp. Reject webhooks if the difference exceeds your acceptable window (e.g., 5 minutes) to prevent replay attacks. Timestamp validation is optional; a successful signature match alone confirms the webhook is authentic.
Example Implementation
const express = require('express');
const crypto = require('crypto');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json({
verify: (req, res, buf) => {
req.rawBody = buf;
}
}));
function verifySignature({ req }) {
// Step 1: Retrieve the signature(s)
const signatureHeader = req.headers['omise-signature'];
const timestampHeader = req.headers['omise-signature-timestamp'];
// Step 2: Prepare the signed payload
const rawBody = req.rawBody.toString('utf8');
const signedPayload = `${timestampHeader}.${rawBody}`;
// Step 3: Decode the webhook secret
const secret = Buffer.from('<YOUR_WEBHOOK_SECRET>', 'base64');
// Step 4: Compute the expected signature
const expectedBuffer = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest();
// Step 5: Compare against all signatures in the header
const signatures = signatureHeader.split(',');
for (const sig of signatures) {
const sigBuffer = Buffer.from(sig, 'hex');
if (crypto.timingSafeEqual(sigBuffer, expectedBuffer)) {
// Optional: Add timestamp validation here
return true;
}
}
return false;
}
app.post('/webhook', (req, res) => {
if (!verifySignature(req)) {
res.status(401).send('Invalid signature')
}
// If verified, return 200 after processing webhook
res.status(200).send('OK');
});
Managing Webhook Secrets
You can view, generate, and manage your webhook secrets in the Webhooks Settings section of your Omise dashboard. Secrets are environment-specific; your Test Mode secret is independent of your Live Mode secret.
[CAUTION] Webhook Secrets are confidential. Treat them as sensitive credentials. Never expose them in client-side code, public repositories, or share them in insecure environments.
Secret Rotation
If you need to update your secret (e.g., for security compliance or because a secret was leaked), Omise provides a zero-downtime rotation path:
- Rotating a Secret: When you
Roll
a secret, a new one is generated immediately. To prevent service interruption, the previous secret remains valid for 24 hours. - Dual-Signatures: During this 24-hour transition, Omise signs every webhook with both the new and the old secret. The Omise-Signature header will contain two comma-separated signatures.
- Manual Revocation: You cannot delete your primary active secret. However, you can manually revoke an expiring secret at any time during the 24-hour window to end the transition period immediately.
- Capacity: You can have a maximum of two secrets active at once (one current, one expiring). If an expiring secret is still active, you must wait for it to lapse or manually revoke it before you can roll your secret again.
Testing
We recommend using Test Mode to validate your signature verification logic before switching to Live Mode.
- Safe Simulation: Rotating or revoking secrets in Test Mode has no impact on your Live production environment.
- Verify Rotation Logic: We strongly recommend testing the
Secret Rotation
flow in Test Mode. This ensures your code correctly handles the Dual-Signature behavior before you perform a rotation in your production environment.
Troubleshooting
Common Issues
Webhook not received:
- Verify your endpoint URL is HTTPS with a valid SSL certificate
- Check that your firewall allows incoming requests from Omise servers
- Confirm your endpoint is accessible and responding to requests
Signature verification fails:
- Confirm you are using the correct webhook secret for your environment (test vs. live)
- Verify you are using the raw request body without any modifications
- Ensure proper Base64 decoding of the webhook secret
Events sent to the wrong endpoint:
- Check if the
webhook_endpointsparameter was specified during charge creation - Verify static webhook configuration in the dashboard