Webhooks Integration Guide
KuvarPay Webhooks Guide#
This guide explains how to receive, verify, and process webhooks sent from the KuvarPay Business Platform.Webhooks allow KuvarPay to notify your backend in real time when events happen (e.g., payments completed, withdrawals processed). Your server exposes an HTTPS endpoint; KuvarPay delivers JSON payloads via POST requests to that endpoint.1.
Deploy an HTTPS endpoint that accepts POST requests with a JSON body (application/json).
2.
In the KuvarPay dashboard, add your endpoint URL and obtain your Webhook Secret.
3.
Save the Webhook Secret securely (environment variable). You will use it to verify request signatures.
4.
Optionally enable/disable specific event types you want to receive.
Tip: For local development, use a tunneling service (e.g., ngrok) to expose your local server and register the tunnel URL in the dashboard.Content-Type: application/json
Timeout: Your endpoint should respond within 30 seconds. KuvarPay will retry failed deliveries with exponential backoff.
Retries: Duplicate deliveries may occur due to retries; ensure your processing is idempotent using the delivery ID header.
X-KuvarPay-Signature: HMAC-SHA256 signature in the format sha256=<hex_signature>
X-KuvarPay-Event: The event type (e.g., payment.completed
, webhook.test
)
X-KuvarPay-Delivery: Unique delivery ID for idempotency tracking
User-Agent: KuvarPay webhook user agent
Content-Type: application/json
Signature Verification#
The signature is calculated as:HMAC-SHA256(webhook_secret, raw_json_payload)
Always verify the signature before processing the event.
Use the raw, unmodified request body for HMAC calculation (not a re-serialized JSON string).
Compare signatures using a constant-time comparison to avoid timing attacks.
Reject requests with missing/invalid signatures with HTTP 401.
3) Events#
Common event types include:checkout_session.completed
- Sent when a checkout session is completed
checkout_session.failed
- Sent when a checkout session fails
checkout_session.expired
- Sent when a checkout session expires
checkout_session.payment_received
- Sent when payment is received for a session
checkout_session.payment_partial
- Sent when partial payment is received for a session
checkout_session.payment_excess
- Sent when excess payment is received for a session
checkout_session.compliance_hold
- Sent when a session is held for compliance review
checkout_session.status_updated
- Sent when session status changes
checkout_session.updated
- Sent for general session updates
Invoice Events#
These events are related to invoice processing:invoice.paid
- Sent when an invoice is paid
Payment Link Events#
These events are related to payment link processing:payment_link.paid
- Sent when a payment link is paid
Subscription Events#
These events are related to subscription lifecycle management:subscription.created
- Sent when a new subscription is created
subscription.updated
- Sent when subscription details are modified
subscription.cancelled
- Sent when a subscription is cancelled
subscription.activated
- Sent when a subscription becomes active
subscription.trial_ended
- Sent when trial period ends
subscription.payment_failed
- Sent when payment attempts fail
Subscription Invoice Events#
These events are related to subscription invoice processing:subscription_invoice.created
- Sent when a new subscription invoice is generated
subscription_invoice.paid
- Sent when a subscription invoice is successfully paid
subscription_invoice.payment_failed
- Sent when subscription invoice payment fails
Test Events#
These events are used for testing webhook endpoints:webhook.test
- Sent for testing webhook endpoint connectivity
Note: The exact payload shape varies by event. Use the event type to route to the correct handler in your code.4) Example Payloads#
{
"event": "webhook.test",
"timestamp": "2024-01-01T00:00:00Z"
}
{
"id": "pay_1234567890",
"event": "payment.completed",
"amount": "100.00",
"currency": "USD",
"status": "completed",
"customer_id": "cust_1234567890",
"timestamp": "2024-01-01T00:00:00Z"
}
{
"subscription": {
"id": 123,
"uid": "sub_1234567890",
"customerId": "cust_1234567890",
"priceId": "price_1234567890",
"status": "active",
"currentPeriodStart": "2024-01-01T00:00:00Z",
"currentPeriodEnd": "2024-02-01T00:00:00Z",
"trialStart": null,
"trialEnd": null,
"cancelAtPeriodEnd": false,
"canceledAt": null,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
}
}
Subscription invoice created:{
"subscription_invoice": {
"id": 456,
"uid": "inv_1234567890",
"subscriptionId": "sub_1234567890",
"amount": "29.99",
"currency": "USD",
"status": "pending",
"invoiceId": "invoice_1234567890",
"periodStart": "2024-01-01T00:00:00Z",
"periodEnd": "2024-02-01T00:00:00Z",
"dueDate": "2024-01-08T00:00:00Z",
"nextPaymentAttempt": "2024-01-01T12:00:00Z",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
}
}
5) Implementation Examples#
Below are minimal examples demonstrating signature verification and idempotency.Node.js (Express)#
import express from 'express';
import crypto from 'crypto';
const app = express();
const PORT = process.env.PORT || 3001;
const WEBHOOK_SECRET = process.env.RAYSWAP_WEBHOOK_SECRET || '';
// Capture raw body for HMAC verification
app.use('/webhook', express.raw({ type: 'application/json' }));
function verifySignature(rawBody: Buffer, signatureHeader?: string): boolean {
if (!signatureHeader || !WEBHOOK_SECRET) return false;
const [algo, expectedSig] = signatureHeader.split('=');
if (algo !== 'sha256' || !expectedSig) return false;
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
hmac.update(rawBody);
const digest = hmac.digest('hex');
// Constant-time comparison
const a = Buffer.from(digest, 'utf8');
const b = Buffer.from(expectedSig, 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Simple in-memory idempotency store (replace with persistent storage)
const processedDeliveries = new Set<string>();
app.post('/webhook', (req, res) => {
const rawBody = req.body as Buffer;
const signature = req.header('X-KuvarPay-Signature');
const event = req.header('X-KuvarPay-Event');
const deliveryId = req.header('X-KuvarPay-Delivery') || '';
if (!verifySignature(rawBody, signature)) {
return res.status(401).send('Invalid signature');
}
if (processedDeliveries.has(deliveryId)) {
return res.status(200).send('Duplicate delivery');
}
let payload: unknown;
try {
payload = JSON.parse(rawBody.toString('utf8'));
} catch (e) {
return res.status(400).send('Invalid JSON');
}
// TODO: handle event types (e.g., payment.completed)
// route by `event` header or by `payload.event`
processedDeliveries.add(deliveryId);
return res.status(200).send('ok');
});
app.get('/health', (_req, res) => res.send('ok'));
app.listen(PORT, () => console.log(`Webhook server listening on ${PORT}`));
6) Testing#
From the KuvarPay dashboard, use the “Send test webhook” or similar feature to trigger webhook.test
to your endpoint.
Use cURL for manual testing:
For local development, run the Node sample and expose it via a tunnel, then set that public URL in the dashboard.7) Idempotency and Retries#
Use X-KuvarPay-Delivery
as a unique key to detect duplicate deliveries.
Store processed delivery IDs and skip re-processing when a duplicate is received.
KuvarPay retries failed deliveries with exponential backoff.
8) Response Codes#
2xx: Acknowledged (processing succeeded or will be handled asynchronously)
4xx: Client errors (e.g., 400 for invalid JSON, 401 for invalid signature)
5xx: Server errors; KuvarPay will retry
9) Troubleshooting#
Invalid signature: Ensure you use the exact raw request body and correct Webhook Secret.
JSON parse errors: Read the raw body before parsing; don’t modify the body string prior to HMAC calculation.
Timeouts: Respond within 30 seconds; consider acknowledging and deferring heavy work to background jobs.
Duplicate events: Implement idempotency using delivery IDs.
Modified at 2025-09-15 18:42:19