Subscriptions
Bill a customer on a repeating schedule, with automatic retries and dunning.
A subscription connects a customer to a plan and bills them on the plan's interval. Kyshi generates an invoice each cycle, attempts payment, retries on failure, and tells you when the state changes.
Create the plan first — a subscription cannot exist without one.
Card Or Manual
The payment method determines almost everything about how the subscription behaves.
| Card | Manual | |
|---|---|---|
paymentMethod | card | bank_transfer, bank, mobile_money |
| Each cycle | Charged automatically | Customer pays an invoice link |
| On failure | Kyshi retries | Reminders; the customer must act |
| You need | A saved card | Somewhere to send the link |
Manual subscriptions cannot be auto-charged. Kyshi issues a payment link per invoice and emits subscription.invoice_payment_link. If you do not handle that event, your customers will never be told to pay.
The Flow
sequenceDiagram
participant K as Kyshi
participant Y as Your backend
participant C as Customer
Y->>K: POST /v1/subscriptions
K-->>Y: subscription (PENDING)
K-->>Y: subscription.active
Y->>C: Grant access
loop Each billing cycle
K->>K: Create invoice
K-->>Y: invoice.created
alt Card
K->>K: Charge saved card
else Manual
K-->>Y: subscription.invoice_payment_link
Y->>C: Send the link
end
alt Paid
K-->>Y: invoice.payment_succeeded
else Failed
K-->>Y: invoice.payment_failed + subscription.past_due
K->>K: Retry until limit or grace end
end
end
Build It
1. Create the subscription
-H "x-api-key: your_secret_key"
-H "idempotency-key: unique-request-key"POST {{host}}/v1/subscriptions{
"planCode": "PLN_abc123",
"customer": "[email protected]",
"paymentMethod": "card",
"card": "AUTH_abc123",
"maxRetryCount": 3,
"gracePeriodDays": 3
}| Field | Required | Notes |
|---|---|---|
planCode | Yes | From Create Plan. |
customer | Yes | Customer identifier. |
paymentMethod | No | card, bank_transfer, bank, mobile_money. |
card | No | Saved card authorization. Required for card. |
startDate | No | Defer the first cycle. |
redirectUrl | No | Where the customer returns after paying an invoice. |
invoiceLimit | No | Stop after N invoices. The subscription then COMPLETED. |
maxRetryCount | No | Failed attempts before giving up. Defaults to 3. |
gracePeriodDays | No | Days in PAST_DUE before cancellation. Defaults to 3. |
2. Grant access on subscription.active
subscription.activeA newly created subscription is PENDING — the first charge has not settled. Wait for ACTIVE.
3. Handle each cycle
For card subscriptions, act on invoice.payment_succeeded and invoice.payment_failed.
For manual subscriptions, act on subscription.invoice_payment_link by delivering the link to the customer.
4. Decide your access policy for PAST_DUE
PAST_DUEKyshi does not revoke access. That is your decision, and you have to make it explicitly.
Statuses
Subscription
| Status | Meaning | Access |
|---|---|---|
PENDING | First charge not settled. | Not yet |
ACTIVE | Billing normally. | Yes |
PAST_DUE | A renewal failed; retries in progress. | Your call |
NON_RENEWING | Will not renew, still paid up. | Yes, until period end |
COMPLETED | Hit its invoice limit. | Until period end |
CANCELLED | Ended. | No |
NON_RENEWING is the one that gets mishandled. The customer has paid for the current period and is owed service until it ends. Revoking immediately on cancellation means taking money for nothing.
Invoice
| Status | Meaning |
|---|---|
CREATED | Generated, not yet attempted. |
PAYMENT_PENDING | Awaiting payment. |
PAYMENT_SUCCEEDED | Paid. |
PAYMENT_FAILED | Failed; retries may continue. |
CANCELLED | Will not be collected. |
Payment attempt
Each try against an invoice records PENDING, SUCCEEDED, FAILED, or REQUIRES_ACTION. See Statuses And Lifecycles.
Dunning: What Happens When A Renewal Fails
This is the part worth reading twice.
When a charge fails, Kyshi categorises the failure and decides whether a retry can succeed:
| Failure | Retry? | Next attempt |
|---|---|---|
INSUFFICIENT_FUNDS | Yes | ~24 hours |
CARD_DECLINED | Yes | ~24 hours |
PROVIDER_TEMPORARY | Yes | ~1 hour |
UNKNOWN | Yes | ~24 hours |
CARD_EXPIRED | No | — |
INVALID_CARD | No | — |
AUTHENTICATION_REQUIRED | No | — |
PROVIDER_PERMANENT | No | — |
Retries stop at maxRetryCount (default 3), and the next retry is clamped to the end of the grace period — so gracePeriodDays is a hard ceiling on the whole dunning window, not just a label. A 3-day grace with 24-hour backoff gives you roughly three attempts.
Non-retryable failures produce REQUIRES_ACTION rather than FAILED. Nothing Kyshi does will fix them; the customer must supply new details. That is your cue to email them, not to retry.
See Failure Codes.
Recovering a past-due subscription
Have the customer update their card, then retry:
POST {{host}}/v1/subscriptions/{id}/update-card
POST {{host}}/v1/subscriptions/{id}/retry-paymentUpdating the card alone does not trigger a charge. You must retry.
Managing A Subscription
PATCH {{host}}/v1/subscriptions/{id}/manageaction | Effect |
|---|---|
cancel | Ends immediately. |
cancel_at_period_end | Moves to NON_RENEWING; ends at period end. |
activate | Reactivates. |
Prefer cancel_at_period_end for voluntary cancellations. Immediate cancel takes access the customer has already paid for.
When It Fails
Access granted too early
Granting on the create response means granting before the first charge settles. Wait for subscription.active.
Access revoked too early
Revoking the moment you see PAST_DUE cancels customers whose payment recovers hours later. Most businesses allow the grace period to run first.
Manual subscriptions silently never pay
If you do not handle subscription.invoice_payment_link, nobody tells the customer to pay. The subscription goes PAST_DUE and cancels, and the customer never knew.
Retrying a non-retryable failure
Calling retry-payment repeatedly on CARD_EXPIRED burns attempts against the limit and can count against the customer with their bank. Check the failure category first.
The amount changes between cycles
A plan priced in settlement currency is converted each cycle, so the customer's local amount moves with the rate. If a stable local price matters more, price the plan in local. See FX And Rates.
Reconcile
async function reconcileSubscriptions() {
for (const sub of await db.subscriptions.activeLocally()) {
const remote = await kyshi.subscriptions.retrieve(sub.id);
switch (remote.status) {
case 'ACTIVE':
await ensureAccess(sub);
break;
case 'PAST_DUE':
await applyGracePolicy(sub, remote);
break;
case 'NON_RENEWING':
await keepAccessUntil(sub, remote.currentPeriodEnd);
break;
case 'CANCELLED':
case 'COMPLETED':
await revokeAccess(sub);
break;
}
}
}Reconcile subscription state on a schedule, not only on webhooks. A missed subscription.cancelled means someone keeps access indefinitely.
Test It
Do not wait for real billing cycles. Drive the lifecycle directly:
POST {{host}}/v1/subscriptions/{id}/simulate| Event | Proves |
|---|---|
due_charge | A renewal succeeds and access continues. |
payment_failed | Your PAST_DUE policy behaves. |
retry_due | Recovery restores access. |
retry_failed_cancel | Final failure revokes access. |
manual_invoice_due | You deliver the invoice link. |
manual_overdue_reminder | You chase an overdue invoice. |
Next
- Plans — define price and interval first
- Subscription Invoices · Payment Attempts
- Failure Codes · Statuses And Lifecycles
Updated 5 days ago
