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"
}
Webhooks | Zevio