Subscription Webhooks
Subscription webhooks notify your server when a subscription or subscription invoice changes.
Use these events to keep your own system in sync with subscription access, billing status, failed payments, retries, and cancellations.
Webhook Setup
- Configure your webhook URL in your business settings.
- Kyshi sends subscription webhooks to the configured webhook URL using the same webhook delivery system as transaction webhooks.
- Webhook requests are signed using your API key secret.
- Your server should verify the signature before trusting the event.
Webhook Payload Format
Subscription webhook payloads follow this structure:
{
"reference": "INV_xxxxxxxxxxxxx",
"amount": 5000,
"event": "invoice.payment_succeeded",
"status": "success",
"data": {
"subscription": {
"id": "7697cabd-ee1b-435a-9ae3-82b926cc5334",
"code": "SUB_xxxxxxxxxxxxx",
"status": "ACTIVE",
"isActive": true,
"startDate": "2026-05-01T00:00:00.000Z",
"previousPaymentDate": "2026-05-01T00:00:00.000Z",
"nextPaymentDate": "2026-06-01T00:00:00.000Z",
"currentPeriodStart": "2026-05-01T00:00:00.000Z",
"currentPeriodEnd": "2026-06-01T00:00:00.000Z",
"pastDueAt": null,
"nextRetryAt": null,
"retryCount": 0,
"maxRetryCount": 3,
"gracePeriodDays": 3,
"cancelledAt": null,
"cancelReason": null,
"invoiceLimit": 12,
"invoicesPaid": 1
},
"invoice": {
"id": "14bbd889-32f4-40dd-92ad-75d35028f874",
"code": "INV_xxxxxxxxxxxxx",
"amount": 5000,
"currency": "NGN",
"periodStart": "2026-05-01T00:00:00.000Z",
"periodEnd": "2026-06-01T00:00:00.000Z",
"dueAt": "2026-05-01T00:00:00.000Z",
"nextRetryAt": null,
"retryCount": 0,
"maxRetryCount": 3,
"status": "PAYMENT_SUCCEEDED",
"description": null,
"transactionId": "8b3dc3e2-10fd-487b-86e2-dacb975110c6"
}
},
"meta": {
"mode": "live",
"kyshiEventId": "9adf03c3-7162-4cd9-95c3-4b2f66f7e4b7",
"kyshiWebhookQueuedAt": "2026-05-01T00:01:10.000Z"
}
}
Subscription Events
| Event | Description |
|---|---|
| subscription.created | A subscription was created. |
| subscription.active | Subscription became active or recovered after successful payment. |
| subscription.past_due | Subscription renewal failed and entered past-due state. |
| subscription.cancelled | Subscription was cancelled or failed after retry/grace exhaustion. |
| subscription.completed | Subscription completed all configured billing cycles. |
| subscription.not_renewing | Subscription will not renew after the current paid period. |
Invoice Events
| Event | Description |
|---|---|
| invoice.created | A billing invoice was created for a subscription cycle. |
| invoice.updated | Invoice status or retry information changed. |
| invoice.payment_succeeded | Subscription invoice payment succeeded. |
| invoice.payment_failed | Subscription invoice payment failed. |
Event Examples
{
"reference": "SUB_xxxxxxxxxxxxx",
"amount": 0,
"event": "subscription.past_due",
"status": "success",
"data": {
"subscription": {
"id": "7697cabd-ee1b-435a-9ae3-82b926cc5334",
"code": "SUB_xxxxxxxxxxxxx",
"status": "PAST_DUE",
"isActive": false,
"pastDueAt": "2026-06-01T00:00:00.000Z",
"nextRetryAt": "2026-06-02T00:00:00.000Z",
"retryCount": 1,
"maxRetryCount": 3,
"gracePeriodDays": 3
},
"reason": "Insufficient funds"
},
"meta": {
"mode": "live",
"kyshiEventId": "evt_xxxxx",
"kyshiWebhookQueuedAt": "2026-06-01T00:01:00.000Z"
}
}
{
"reference": "INV_xxxxxxxxxxxxx",
"amount": 5000,
"event": "invoice.payment_failed",
"status": "failed",
"data": {
"subscription": {
"id": "7697cabd-ee1b-435a-9ae3-82b926cc5334",
"code": "SUB_xxxxxxxxxxxxx",
"status": "PAST_DUE",
"isActive": false
},
"invoice": {
"id": "14bbd889-32f4-40dd-92ad-75d35028f874",
"code": "INV_xxxxxxxxxxxxx",
"amount": 5000,
"currency": "NGN",
"status": "PAYMENT_FAILED",
"description": "Insufficient funds",
"retryCount": 1,
"maxRetryCount": 3,
"nextRetryAt": "2026-06-02T00:00:00.000Z"
}
},
"meta": {
"mode": "live",
"kyshiEventId": "evt_xxxxx",
"kyshiWebhookQueuedAt": "2026-06-01T00:01:00.000Z"
}
}
{
"reference": "INV_xxxxxxxxxxxxx",
"amount": 5000,
"event": "invoice.payment_succeeded",
"status": "success",
"data": {
"subscription": {
"id": "7697cabd-ee1b-435a-9ae3-82b926cc5334",
"code": "SUB_xxxxxxxxxxxxx",
"status": "ACTIVE",
"isActive": true,
"previousPaymentDate": "2026-06-01T00:00:00.000Z",
"nextPaymentDate": "2026-07-01T00:00:00.000Z"
},
"invoice": {
"id": "14bbd889-32f4-40dd-92ad-75d35028f874",
"code": "INV_xxxxxxxxxxxxx",
"amount": 5000,
"currency": "NGN",
"status": "PAYMENT_SUCCEEDED",
"retryCount": 0
}
},
"meta": {
"mode": "live",
"kyshiEventId": "evt_xxxxx",
"kyshiWebhookQueuedAt": "2026-06-01T00:01:00.000Z"
}
}
Recommended Handling
When you receive a subscription webhook:
- Verify the webhook signature.
- Use
meta.kyshiEventIdfor idempotency. - Read
event. - Update the subscription record in your system.
- Grant or revoke access based on
subscription.statusandsubscription.isActive.
Access Rule
Grant access only when:
subscription.status = ACTIVEandsubscription.isActive = true
Block Access When
subscription.status = PAST_DUEsubscription.status = CANCELLEDsubscription.status = COMPLETEDsubscription.isActive = false
NON_RENEWING Behavior
- Allow access until
subscription.currentPeriodEnd
Idempotency
Webhook events can be retried. Your webhook handler should be idempotent.
Use:
meta.kyshiEventId
to detect duplicate events.
Important Notes
invoice.payment_failedmeans payment failed for a billing cycle.subscription.past_duemeans the subscription should no longer have access.invoice.payment_succeededmeans payment was successful for the invoice.subscription.activemeans access may be restored.subscription.cancelledmeans the subscription should no longer renew.subscription.completedmeans all configured billing cycles are complete.
Updated about 12 hours ago
