Webhooki

Webhooki są wysyłane jako żądania POST na skonfigurowany adres URL. Każde żądanie zawiera w nagłówku sygnaturę potwierdzającą, że pochodzi od nas i że treść nie została zmieniona. Powinieneś zweryfikować tę sygnaturę u siebie, używając zapisanego sekretu webhooka - dokładny sposób opisany jest w przykładzie kodu poniżej. Żądania ze zbyt starą sygnaturą (np. starszą niż kilka minut) warto odrzucać, aby uniemożliwić ponowne odtworzenie tego samego żądania przez osoby trzecie.

Przykład obsługi webhooka

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 });
});

Odpowiedź

Wszystkie webhooki mają tę samą strukturę z typem wydarzenia, ładunkiem danych i znacznikiem czasu. Pole danych zawiera informacje specyficzne dla wydarzenia.

payment.success

Płatność zakończona pomyślnie

Płatność potwierdzona. Środki zostały pobrane.

{
  "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