Webhooks
Webhooks allow you to receive real-time notifications when events happen in your Salam Gateway account.
Overview
Webhooks are HTTP callbacks that we send to your server when events occur. You can use webhooks to:
- Update your database when a payment succeeds
- Email customers when a refund is processed
- Trigger fulfillment when a payment is captured
- Track failed payments for analysis
Setting Up Webhooks
- Go to Dashboard → Developers → Webhooks
- Click Add Endpoint
- Enter your webhook URL (must be HTTPS in production)
- Select events to subscribe to
- Copy the signing secret
Webhook Events
Payment Events
| Event | Description |
|---|---|
payment.created | A new payment was created |
payment.authorized | Payment was authorized |
payment.captured | Payment was successfully captured |
payment.failed | Payment attempt failed |
payment.cancelled | Payment was cancelled |
payment.expired | Payment expired without completion |
Refund Events
| Event | Description |
|---|---|
refund.created | A new refund was initiated |
refund.succeeded | Refund was processed successfully |
refund.failed | Refund processing failed |
Dispute Events
| Event | Description |
|---|---|
dispute.created | A dispute was created |
dispute.updated | Dispute status was updated |
dispute.closed | Dispute was resolved |
Webhook Payload
All webhook events follow this structure:
{
"id": "evt_abc123xyz",
"type": "payment.captured",
"created_at": "2024-01-15T12:00:00Z",
"data": {
"id": "pay_xyz789",
"object": "payment",
"amount": 10000,
"currency": "MYR",
"status": "captured",
"payment_method": "fpx",
"metadata": {}
}
}Event Object
| Attribute | Type | Description |
|---|---|---|
id | string | Unique event identifier |
type | string | Event type (e.g., "payment.captured") |
created_at | string | Timestamp when event was created |
data | object | The related object (payment, refund, etc.) |
Verifying Signatures
Security
Always verify webhook signatures to ensure the request came from Salam Gateway.
Each webhook request includes a Salam-Signature header containing a signature. Verify this signature before processing the webhook.
Using the Node.js SDK
import express from 'express';
import Salam from '@salamgateway/node';
const app = express();
const salam = new Salam({
apiKey: 'sk_live_xxx',
webhookSecret: 'whsec_xxx',
});
app.post('/webhooks/salam',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['salam-signature'];
try {
const event = salam.webhooks.constructEvent(
req.body,
signature
);
// Process the event
switch (event.type) {
case 'payment.captured':
handlePaymentCaptured(event.data);
break;
case 'payment.failed':
handlePaymentFailed(event.data);
break;
case 'refund.succeeded':
handleRefundSucceeded(event.data);
break;
}
res.json({ received: true });
} catch (error) {
console.error('Webhook verification failed:', error);
res.status(400).send('Invalid signature');
}
}
);Manual Verification
If you're not using our SDK:
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// In your webhook handler
const isValid = verifyWebhookSignature(
req.body.toString(),
req.headers['salam-signature'],
'whsec_xxx'
);
if (!isValid) {
return res.status(400).send('Invalid signature');
}Best Practices
1. Return 200 Quickly
Your endpoint should return a 200 status as quickly as possible. Process webhooks asynchronously if needed:
app.post('/webhooks', async (req, res) => {
const event = verifyWebhook(req);
// Return 200 immediately
res.json({ received: true });
// Process asynchronously
processWebhookAsync(event);
});2. Handle Duplicates
The same event may be sent multiple times. Use idempotency keys to handle duplicates:
async function processWebhook(event) {
// Check if already processed
const exists = await db.webhookEvents.findOne({
eventId: event.id,
});
if (exists) {
console.log('Event already processed');
return;
}
// Process and mark as done
await processEvent(event);
await db.webhookEvents.create({ eventId: event.id });
}3. Use HTTPS
Webhook URLs must use HTTPS in production. This ensures:
- Request encryption
- Server authentication
- Data integrity
4. Implement Retry Logic
We automatically retry failed webhooks with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | 5 minutes |
| 2 | 30 minutes |
| 3 | 2 hours |
| 4 | 8 hours |
| 5 | 24 hours |
After 5 failed attempts, the webhook is marked as failed and retries stop.
5. Monitor Webhook Health
Check your webhook dashboard regularly:
- Success rate
- Average response time
- Failed deliveries
Testing Webhooks
Local Testing with ngrok
# Install ngrok
npm install -g ngrok
# Start your local server
npm run dev
# Expose it publicly
ngrok http 3000
# Use the ngrok URL in your webhook settings
# https://abc123.ngrok.io/webhooks/salamTest Events
Trigger test events from the dashboard:
- Go to Developers → Webhooks
- Select your webhook endpoint
- Click Send Test Event
- Choose an event type
- Review the response
Example Implementation
Complete webhook handler example:
import express from 'express';
import Salam from '@salamgateway/node';
const app = express();
const salam = new Salam({
apiKey: process.env.SALAM_API_KEY,
webhookSecret: process.env.SALAM_WEBHOOK_SECRET,
});
app.post('/webhooks/salam',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.headers['salam-signature'];
try {
const event = salam.webhooks.constructEvent(
req.body,
signature
);
// Return 200 immediately
res.json({ received: true });
// Process asynchronously
await processWebhook(event);
} catch (error) {
console.error('Webhook error:', error);
res.status(400).send('Webhook Error');
}
}
);
async function processWebhook(event) {
// Check for duplicates
const exists = await db.events.findOne({ id: event.id });
if (exists) return;
// Handle event
switch (event.type) {
case 'payment.captured':
await handlePaymentSuccess(event.data);
break;
case 'payment.failed':
await handlePaymentFailure(event.data);
break;
case 'refund.succeeded':
await handleRefundSuccess(event.data);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
// Mark as processed
await db.events.create({ id: event.id, type: event.type });
}
async function handlePaymentSuccess(payment) {
// Update order status
await db.orders.update(
{ paymentId: payment.id },
{ status: 'paid', paidAt: new Date() }
);
// Send confirmation email
await emailService.sendPaymentConfirmation(payment);
// Trigger fulfillment
await fulfillmentService.createShipment(payment.metadata.orderId);
}
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});Troubleshooting
Webhook Not Receiving Events
- Check the webhook URL is correct and accessible
- Ensure your server is using HTTPS (production)
- Verify the webhook is enabled in the dashboard
- Check firewall settings
Signature Verification Failing
- Confirm you're using the correct webhook secret
- Use raw request body (not parsed JSON)
- Check for encoding issues
- Verify timestamp tolerance (if implemented)
Timeouts
If your endpoint times out:
- Return 200 immediately
- Process webhooks asynchronously
- Optimize database queries
- Use queues for heavy processing
Need Help?
Contact support at support@salamgateway.com or check our Discord community.