KimlPay sends HTTP POST requests to your configured webhook endpoints when payment events occur. Each webhook request includes a cryptographic signature that you can use to verify the authenticity of the request.
Webhooks are triggered for payment transactions with the following statuses:
completed- Payment has been successfully completedpayment-authorized- Payment has been authorizedpaid- Payment has been paid
cancelled- Payment was cancelled by the userrejected- Payment was rejectedpayment-error- An error occurred during payment processing
Note: Webhooks are NOT sent for transactions with started status.
Each webhook request contains the following JSON payload:
{
"transaction_id": "string",
"payment_link_id": "string",
"pl_id": "string",
"status": "success|failed",
"total_fees": "string",
"total_amount": {
"amount": "string",
"currency": "string"
},
"received_amount": {
"amount": "string",
"currency": "string"
},
"user_info": {
"email": "string"
},
"payment_info": "object"
}| Field | Type | Description |
|---|---|---|
transaction_id |
string | transaction request id |
payment_link_id |
string | Internal payment link identifier |
pl_id |
string | Public payment link ID |
status |
string | Payment status: success or failed |
total_fees |
string | Total fees charged |
total_amount.amount |
string | Original payment amount |
total_amount.currency |
string | Original payment currency |
received_amount.amount |
string | Amount received after fees |
received_amount.currency |
string | Received amount currency |
user_info.email |
string | Payer's email address |
payment_info |
object | Additional payment details about payment method |
Each webhook request includes the following headers:
Content-Type: application/json
X-Request-Signature: <base64-encoded-signature>
Each webhook request includes a cryptographic signature in the X-Request-Signature header. This signature is generated using RSA-SHA256 and your private key, allowing you to verify that the request originated from KimlPay.
Use the following verifySignature function to verify incoming webhooks:
const crypto = require('crypto');
function verifySignature(payload, signature, publicKey) {
try {
// Create a verify object
const verify = crypto.createVerify('RSA-SHA256');
// Convert payload to string if it's an object
let payloadString;
if (typeof payload === 'object') {
// Use JSON.stringify with consistent ordering for objects
payloadString = JSON.stringify(payload);
} else {
payloadString = String(payload);
}
// Add the original payload data
verify.update(payloadString, 'utf8');
// Verify the signature using public key
const isValid = verify.verify(publicKey, signature, 'base64');
return isValid;
} catch (error) {
console.error('Error verifying signature:', error.message);
return false;
}
}Here's how to verify webhooks in your endpoint:
const express = require('express');
const app = express();
// Your public key (corresponding to the private key configured in KimlPay)
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
YOUR_PUBLIC_KEY_HERE
-----END PUBLIC KEY-----`;
app.use(express.json());
app.post('/webhook', (req, res) => {
const signature = req.headers['x-request-signature'];
const payload = req.body;
// Verify the signature
const isValid = verifySignature(payload, signature, PUBLIC_KEY);
if (!isValid) {
console.log('Invalid signature - webhook rejected');
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the verified webhook
console.log('Webhook verified successfully:', payload);
// Handle the payment event
switch (payload.status) {
case 'success':
// Handle successful payment
console.log(`Payment completed: ${payload.pl_id}`);
break;
case 'failed':
// Handle failed payment
console.log(`Payment failed: ${payload.pl_id}`);
break;
default:
console.log(`Unknown status: ${payload.status}`);
}
// Always respond with 200 to acknowledge receipt
res.status(200).json({ received: true });
});Never process webhook requests without verifying the signature first. This prevents malicious requests from being processed.
Always configure your webhook URLs to use HTTPS to ensure data transmission is encrypted.
Even after signature verification, validate that the payload data meets your expected format and business rules.
KimlPay may occasionally send duplicate webhooks. Implement idempotency using the transaction_id to prevent duplicate processing.
Respond to webhook requests within 60 seconds. KimlPay considers requests that take longer than 60 seconds as failed.
If your endpoint returns a non-200 status code, KimlPay may retry the webhook. Ensure your endpoint can handle retries safely.
Your webhook endpoint must:
- Return HTTP 200 - Any other status code is considered a failure
- Respond within 60 seconds - Longer responses are considered timeouts
- Handle retries - Failed webhooks may be retried
Example response:
res.status(200).json({ received: true });-
Signature Verification Fails
- Ensure you're using the correct public key
- Verify the payload is being processed as received (no modifications)
- Check that you're using the correct signature from the header
-
Webhook Not Received
- Verify your endpoint URL is accessible from the internet
- Check that your server is responding within 60 seconds
- Ensure your endpoint accepts POST requests
-
Duplicate Webhooks
- Implement idempotency checking using
transaction_id - Store processed webhook IDs to prevent reprocessing
- Implement idempotency checking using
- Log both successful and failed verification attempts
- Test signature verification with known good payloads
- Use webhook testing tools to simulate requests
- Monitor your endpoint's response times and error rates
If you encounter issues with webhook integration, please contact KimlPay support with:
- Your webhook endpoint URL
- Sample webhook payload (remove sensitive data)
- Error messages or logs
- Timestamp of the issue