Webhooks
Webhooks are sent as POST requests to your configured URL. Each request includes a signature in the header that confirms it came from us and that the payload was not altered. You should verify this signature on your side using your stored webhook secret - the exact steps are shown in the code example below. Reject requests whose signature is too old (e.g. older than a few minutes) to prevent someone from replaying the same request.
Webhook Handler Example
const express = require('express');
const app = express();
const endpointSecret = 'your_webhook_secret_here';
const constructEvent = async (body, signature, secret, tolerance = 300) => {
const h = signature.split(',').reduce((a, x) => { const i = x.indexOf('='); a[x.slice(0, i).trim()] = x.slice(i + 1).trim(); return a; }, {});
if (!h.t || !h.v1) throw new Error('Invalid');
const enc = new TextEncoder(), hex2buf = h => new Uint8Array(h.length / 2).map((_, i) => parseInt(h.substr(i * 2, 2), 16));
const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
const ok = await crypto.subtle.verify('HMAC', key, hex2buf(h.v1), enc.encode(h.t + '.' + body));
if (!ok || (tolerance && (Math.floor(Date.now() / 1000) - Number(h.t)) > tolerance)) throw new Error('Invalid');
return JSON.parse(body);
};
app.post('/webhook', express.raw({ type: 'application/json' }), async (request, response) => {
let event;
if (endpointSecret) {
try {
event = await constructEvent(request.body.toString('utf8'), request.headers['x-zevio-signature'], endpointSecret);
} catch (err) {
return response.sendStatus(400);
}
} else {
return response.sendStatus(400);
}
switch (event.event) {
case 'payment.success': break;
case 'payment.failed': break;
case 'plan.created': break;
case 'plan.updated': break;
case 'plan.canceled': break;
case 'subscription.created': break;
case 'subscription.updated': break;
case 'subscription.completed': break;
case 'subscription.expired': break;
case 'subscription.canceled': break;
case 'checkout_session.completed': break;
case 'refund.completed': break;
default: break;
}
response.json({ received: true });
});Response
All webhooks follow the same structure with an event type, data payload, and timestamp. The data field contains event-specific information.
payment.success
Payment completed successfully
Payment confirmed. Funds have been received.
{
"event": "payment.success",
"data": {
"id": "evt_Abc12XyZ34Qw",
"paymentId": "pay_abc123xyz",
"organizationId": "org_abc123xyz",
"qrId": "qr_abc123xyz",
"qrTitle": "Monthly Plan",
"customerId": "cus_abc123xyz",
"amount": 49.99,
"tax": 11.5,
"totalAmount": 61.49,
"currency": "PLN",
"method": "CARD",
"status": "SUCCESS",
"fundingSource": "DIRECT",
"subscriptionId": null,
"tpayTransactionId": "T123456",
"transactionId": "TXN-2025-001234",
"createdAt": "2024-01-01T12:00:00.000Z",
"processedAt": "2024-01-01T12:00:05.000Z",
"metadata": {
"order_id": "ord_123",
"source": "web"
},
"customer": {
"customerId": "cus_abc123xyz",
"email": "customer@example.com",
"firstName": "John",
"lastName": "Doe"
}
},
"timestamp": "2024-01-01T12:00:05.000Z"
}