Failure Codes
Why a payment attempt failed, whether it is worth retrying, and what to tell the customer.
When a payment attempt fails, Kyshi records a failure category alongside the raw provider message. Use the category for logic and the provider message for your own logs — never show the raw provider message to a customer.
The single most important distinction is whether a retry can succeed. Retrying a permanently failed payment wastes an attempt and, on cards, can count against the customer with their bank.
Failure Categories
| Code | What happened | Retry? | Tell the customer |
|---|---|---|---|
INSUFFICIENT_FUNDS | The account or card did not have enough money. | Yes, later | "Your payment was declined for insufficient funds. Please top up or use another method." |
CARD_DECLINED | The issuer declined the charge without a more specific reason. | Yes, later | "Your bank declined the payment. Please try another card." |
CARD_EXPIRED | The card is past its expiry date. | No | "Your card has expired. Please update your card details." |
INVALID_CARD | The card details are not usable. | No | "Those card details could not be used. Please check them or use another card." |
AUTHENTICATION_REQUIRED | The customer must complete an authentication step. | Not by you | "Your bank needs to confirm this payment. Please complete the verification." |
PROVIDER_TEMPORARY | A transient problem at the payment provider. | Yes | "We could not process the payment just now. We will try again shortly." |
PROVIDER_PERMANENT | A non-recoverable problem at the payment provider. | No | "We could not process the payment. Please use another method." |
UNKNOWN | No category could be determined. | Once | "Something went wrong with the payment. Please try again." |
Retry Logic
The categories divide cleanly into three behaviours:
const RETRYABLE = ['INSUFFICIENT_FUNDS', 'CARD_DECLINED', 'PROVIDER_TEMPORARY'];
const NEEDS_NEW_DETAILS = ['CARD_EXPIRED', 'INVALID_CARD', 'PROVIDER_PERMANENT'];
function onFailedAttempt(attempt) {
const code = attempt.failureCategory;
if (code === 'AUTHENTICATION_REQUIRED') {
// The customer has to act. Do not retry; prompt them.
return promptCustomerToAuthenticate(attempt);
}
if (NEEDS_NEW_DETAILS.includes(code)) {
// Retrying the same instrument cannot succeed.
return askForNewPaymentMethod(attempt);
}
if (RETRYABLE.includes(code)) {
return scheduleRetry(attempt);
}
// UNKNOWN: one retry, then treat as permanent.
return attempt.attemptNumber < 2
? scheduleRetry(attempt)
: askForNewPaymentMethod(attempt);
}Retry Timing
For subscriptions, Kyshi schedules the next attempt from the failure category:
| Code | Next attempt |
|---|---|
PROVIDER_TEMPORARY | ~1 hour |
INSUFFICIENT_FUNDS | ~24 hours |
CARD_DECLINED | ~24 hours |
UNKNOWN | ~24 hours |
| Everything else | No retry |
PROVIDER_TEMPORARY is the only category worth retrying quickly — the problem is on the provider's side and may clear in minutes. The rest depend on the customer's balance or bank changing their mind, which is measured in days. Retrying an insufficient-funds decline three times in an hour will fail three times.
Two limits apply on top of the schedule. Attempts stop at the subscription's maxRetryCount, and the next retry is clamped to the end of the grace period — so gracePeriodDays caps the entire dunning window regardless of how many attempts remain.
Kyshi runs this schedule for you and moves the subscription to PAST_DUE while it does. You do not need your own retry loop on top; you need to decide what access the customer keeps while it runs. See Subscriptions and Statuses And Lifecycles.
Where Failure Categories Appear
Failure categories are recorded on subscription payment attempts. Retrieve them with:
GET {{host}}/v1/subscriptions/{id}/payment-attempts
GET {{host}}/v1/subscriptions/invoices/{invoiceId}/payment-attemptsThey also accompany the invoice.payment_failed webhook. See Webhook Events.
One-Time Payment Failures
One-time transactions report failure through the transaction status rather than a failure category. A transaction that reaches FAILED carries the provider's message, but not a normalised category — so for one-time payments, branch on status and treat every failure as "let the customer try again".
Updated 2 days ago
