Skip to content

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

  1. Go to Dashboard → Developers → Webhooks
  2. Click Add Endpoint
  3. Enter your webhook URL (must be HTTPS in production)
  4. Select events to subscribe to
  5. Copy the signing secret

Webhook Events

Payment Events

EventDescription
payment.createdA new payment was created
payment.authorizedPayment was authorized
payment.capturedPayment was successfully captured
payment.failedPayment attempt failed
payment.cancelledPayment was cancelled
payment.expiredPayment expired without completion

Refund Events

EventDescription
refund.createdA new refund was initiated
refund.succeededRefund was processed successfully
refund.failedRefund processing failed

Dispute Events

EventDescription
dispute.createdA dispute was created
dispute.updatedDispute status was updated
dispute.closedDispute was resolved

Webhook Payload

All webhook events follow this structure:

json
{
  "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

AttributeTypeDescription
idstringUnique event identifier
typestringEvent type (e.g., "payment.captured")
created_atstringTimestamp when event was created
dataobjectThe 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

typescript
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:

typescript
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:

typescript
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:

typescript
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:

AttemptDelay
15 minutes
230 minutes
32 hours
48 hours
524 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

bash
# 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/salam

Test Events

Trigger test events from the dashboard:

  1. Go to Developers → Webhooks
  2. Select your webhook endpoint
  3. Click Send Test Event
  4. Choose an event type
  5. Review the response

Example Implementation

Complete webhook handler example:

typescript
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

  1. Check the webhook URL is correct and accessible
  2. Ensure your server is using HTTPS (production)
  3. Verify the webhook is enabled in the dashboard
  4. Check firewall settings

Signature Verification Failing

  1. Confirm you're using the correct webhook secret
  2. Use raw request body (not parsed JSON)
  3. Check for encoding issues
  4. Verify timestamp tolerance (if implemented)

Timeouts

If your endpoint times out:

  1. Return 200 immediately
  2. Process webhooks asynchronously
  3. Optimize database queries
  4. Use queues for heavy processing

Need Help?

Contact support at support@salamgateway.com or check our Discord community.

Released under the MIT License.