Webhook Events
The complete catalogue of events Kyshi sends, when each one fires, and what it carries.
This is the full list of events Kyshi delivers to your webhook URL. For endpoint setup, headers, signature verification, and retry behaviour, see Webhook.
Every payload carries an event field naming the event, and a meta.kyshiEventId that is unique per delivery. Key your deduplication on meta.kyshiEventId.
Collections
Fired when money comes in, whether through checkout, a direct charge, a payment link, or a virtual account.
| Event | Fires when |
|---|---|
charge.success | A charge or virtual account collection succeeded. |
charge.completed | A charge reached a completed state at the provider. |
charge.failed | A charge or virtual account collection failed. |
virtual_account.credit | A virtual account received a credit. |
charge.success is the event most integrations act on. Note that a single customer payment can produce both virtual_account.credit and charge.success — deduplicate on meta.kyshiEventId and treat the transaction reference as the business key.
Always check collectionStatus on virtual account collections before fulfilling. A credit can arrive as PARTIAL, OVERPAID, or REVIEW. See Statuses And Lifecycles.
Payouts
| Event | Fires when |
|---|---|
transfer.success | A payout reached the beneficiary. |
transfer.failed | A payout failed. |
transfer.reversed | A previously successful payout was reversed. |
transfer.reversed can arrive well after transfer.success. If your system marks a payout final on success, make sure it can also unwind that decision.
Subscriptions
| Event | Fires when |
|---|---|
subscription.active | A subscription became active. |
subscription.past_due | A renewal failed and retries began. |
subscription.payment_issue | A subscription has a payment problem needing attention. |
subscription.not_renewing | A subscription will not renew at period end. |
subscription.completed | A subscription reached its invoice or payment limit. |
subscription.cancelled | A subscription was cancelled. |
subscription.invoice_payment_link | A payment link was issued for a subscription invoice. |
subscription.invoice_payment_link matters for non-card subscriptions. Subscriptions billed by bank_transfer, bank, or mobile_money cannot be auto-charged, so Kyshi issues a link for the customer to pay each cycle.
Invoices
| Event | Fires when |
|---|---|
invoice.created | A subscription invoice was created. |
invoice.updated | A subscription invoice changed. |
invoice.payment_succeeded | An invoice payment succeeded. |
invoice.payment_failed | An invoice payment failed. |
invoice.payment_failed carries the failure category for the attempt. See Failure Codes.
Payload Shape
All events share the same envelope. Fields that do not apply to an event are omitted or null.
{
"reference": "KYSHI-123456789",
"amount": 10000,
"customer": { "id": "customer-id", "email": "[email protected]" },
"meta": {
"localCurrency": "NGN",
"localAmount": 10000,
"settlementCurrency": "USD",
"settlementAmount": 6.67,
"netAmount": 9750,
"feeBearer": "CUSTOMER",
"mode": "live",
"transactionId": "transaction-id",
"kyshiEventId": "event-id",
"kyshiWebhookSentAt": "2026-05-08T12:00:00.000Z"
},
"event": "charge.success",
"status": "success"
}The full field reference, including authorization and meta.feeBreakdown, is on the Webhook page.
Handling Events Safely
Three rules that cover most of what goes wrong:
Deduplicate. The same business event can be delivered more than once. Store meta.kyshiEventId and ignore repeats.
Do not trust the payload alone for high-value decisions. Treat the webhook as a notification that something changed, then verify against the API before releasing goods or money.
Ignore events you do not recognise. New events are added over time. Return 2xx for anything you do not handle, or Kyshi will retry a delivery your system was never going to process.
app.post('/webhooks/kyshi', async (req, res) => {
if (!isValidSignature(req)) return res.sendStatus(401);
// Acknowledge fast, process out of band.
res.sendStatus(200);
const { event, meta } = req.body;
if (await alreadyProcessed(meta.kyshiEventId)) return;
switch (event) {
case 'charge.success':
return handleChargeSuccess(req.body);
case 'transfer.reversed':
return handleTransferReversed(req.body);
default:
return; // Unknown events are acknowledged, not processed.
}
});Your endpoint must respond within 10 seconds, so acknowledge first and do the work afterwards.
Updated about 20 hours ago
