Receive real-time event notifications from Kelviq to your server.
Kelviq can push event notifications to your server whenever something meaningful happens — a subscription is created, an invoice is paid, or a refund is issued. This guide explains how to register an endpoint, verify incoming requests, and handle events.
When an event occurs, Kelviq creates a WebhookEvent and fans it out to all enabled endpoints you have registered for that event type. Each delivery is a single POST request with a JSON body and signed headers.Kelviq retries failed deliveries up to 3 times with a 60-second delay between attempts. A delivery is considered successful when your endpoint returns a 2xx status code.
Go to Settings → Developers in the Kelviq dashboard to create a webhook endpoint. You will specify:
URL — The public HTTPS URL Kelviq will POST events to.
Events — The subset of event types you want to subscribe to.
After creation, Kelviq generates a signing secret for the endpoint in the format kq_whsec_<random>. Store this secret securely — you will need it to verify signatures.
Your signing secret is shown only once. If you lose it, you must regenerate it from the dashboard.
Always verify the signature before processing a webhook. Skipping verification exposes your endpoint to spoofed requests from third parties.
Kelviq signs every request using HMAC-SHA256. To verify a request:
Read the webhook-id and webhook-timestamp headers.
Construct the signed string by concatenating: {webhook-id}.{webhook-timestamp}.{raw-request-body} (joined with .)
Compute HMAC-SHA256 over the signed string using your endpoint’s signing secret as the key.
Compare the hex digest to the signature in the webhook-signature header (strip the v1, prefix before comparing).
Reject the request if the signatures do not match.
The raw request body must be used exactly as received — before any JSON parsing. The compact JSON serialization (no spaces) is what Kelviq sends and signs.
Optionally, also check that webhook-timestamp is within a few minutes of your server’s current time to defend against replay attacks.
Use express.raw() (not express.json()) in Node.js so that req.body contains the unmodified request bytes. Parsing the body before verification will break the signature check.
Copy
const crypto = require('crypto');function verifyWebhook(req, signingSecret) { const webhookId = req.headers['webhook-id']; const webhookTimestamp = req.headers['webhook-timestamp']; const webhookSignature = req.headers['webhook-signature']; if (!webhookId || !webhookTimestamp || !webhookSignature) { throw new Error('Missing required webhook headers'); } // Defend against replay attacks (5-minute tolerance) const now = Math.floor(Date.now() / 1000); if (Math.abs(now - parseInt(webhookTimestamp, 10)) > 300) { throw new Error('Webhook timestamp is too old'); } // Reconstruct the signed payload const rawBody = req.body; // must be the raw Buffer/string, not parsed JSON const signedPayload = `${webhookId}.${webhookTimestamp}.${rawBody}`; // Compute HMAC-SHA256 const expectedSignature = crypto .createHmac('sha256', signingSecret) .update(signedPayload, 'utf8') .digest('hex'); // Strip the "v1," prefix from the header value const [, receivedSignature] = webhookSignature.split(','); if (!crypto.timingSafeEqual( Buffer.from(expectedSignature, 'hex'), Buffer.from(receivedSignature, 'hex') )) { throw new Error('Webhook signature mismatch'); } return JSON.parse(rawBody);}// Express handler exampleapp.post('/webhooks/kelviq', express.raw({ type: 'application/json' }), (req, res) => { let event; try { event = verifyWebhook(req, process.env.KELVIQ_WEBHOOK_SECRET); } catch (err) { console.error('Webhook verification failed:', err.message); return res.status(400).send('Bad Request'); } switch (event.type) { case 'subscription.created': console.log('New subscription:', event.data.object.id); break; case 'invoice.paid': console.log('Invoice paid:', event.data.object.id); break; default: console.log('Unhandled event type:', event.type); } res.status(200).json({ received: true });});
Return 2xx fast. Acknowledge the webhook immediately and process it asynchronously. Long-running handlers increase the risk of timeouts and duplicate retries.
Make handlers idempotent. The same event may be delivered more than once. Use data.id (the event UUID) as a deduplication key.
Validate the timestamp. Reject requests where webhook-timestamp is more than 5 minutes from your server’s clock to prevent replay attacks.