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.

CardManual
paymentMethodcardbank_transfer, bank, mobile_money
Each cycleCharged automaticallyCustomer pays an invoice link
On failureKyshi retriesReminders; the customer must act
You needA saved cardSomewhere 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
}
FieldRequiredNotes
planCodeYesFrom Create Plan.
customerYesCustomer identifier.
paymentMethodNocard, bank_transfer, bank, mobile_money.
cardNoSaved card authorization. Required for card.
startDateNoDefer the first cycle.
redirectUrlNoWhere the customer returns after paying an invoice.
invoiceLimitNoStop after N invoices. The subscription then COMPLETED.
maxRetryCountNoFailed attempts before giving up. Defaults to 3.
gracePeriodDaysNoDays in PAST_DUE before cancellation. Defaults to 3.

2. Grant access on subscription.active

A 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

Kyshi does not revoke access. That is your decision, and you have to make it explicitly.

Statuses

Subscription

StatusMeaningAccess
PENDINGFirst charge not settled.Not yet
ACTIVEBilling normally.Yes
PAST_DUEA renewal failed; retries in progress.Your call
NON_RENEWINGWill not renew, still paid up.Yes, until period end
COMPLETEDHit its invoice limit.Until period end
CANCELLEDEnded.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

StatusMeaning
CREATEDGenerated, not yet attempted.
PAYMENT_PENDINGAwaiting payment.
PAYMENT_SUCCEEDEDPaid.
PAYMENT_FAILEDFailed; retries may continue.
CANCELLEDWill 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:

FailureRetry?Next attempt
INSUFFICIENT_FUNDSYes~24 hours
CARD_DECLINEDYes~24 hours
PROVIDER_TEMPORARYYes~1 hour
UNKNOWNYes~24 hours
CARD_EXPIREDNo—
INVALID_CARDNo—
AUTHENTICATION_REQUIREDNo—
PROVIDER_PERMANENTNo—

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-payment

Updating the card alone does not trigger a charge. You must retry.

Managing A Subscription

PATCH {{host}}/v1/subscriptions/{id}/manage
actionEffect
cancelEnds immediately.
cancel_at_period_endMoves to NON_RENEWING; ends at period end.
activateReactivates.

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
EventProves
due_chargeA renewal succeeds and access continues.
payment_failedYour PAST_DUE policy behaves.
retry_dueRecovery restores access.
retry_failed_cancelFinal failure revokes access.
manual_invoice_dueYou deliver the invoice link.
manual_overdue_reminderYou chase an overdue invoice.

See Simulate Subscription.

Next


Did this page help you?