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

CodeWhat happenedRetry?Tell the customer
INSUFFICIENT_FUNDSThe 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_DECLINEDThe issuer declined the charge without a more specific reason.Yes, later"Your bank declined the payment. Please try another card."
CARD_EXPIREDThe card is past its expiry date.No"Your card has expired. Please update your card details."
INVALID_CARDThe card details are not usable.No"Those card details could not be used. Please check them or use another card."
AUTHENTICATION_REQUIREDThe customer must complete an authentication step.Not by you"Your bank needs to confirm this payment. Please complete the verification."
PROVIDER_TEMPORARYA transient problem at the payment provider.Yes"We could not process the payment just now. We will try again shortly."
PROVIDER_PERMANENTA non-recoverable problem at the payment provider.No"We could not process the payment. Please use another method."
UNKNOWNNo 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:

CodeNext attempt
PROVIDER_TEMPORARY~1 hour
INSUFFICIENT_FUNDS~24 hours
CARD_DECLINED~24 hours
UNKNOWN~24 hours
Everything elseNo 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-attempts

They 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".


Did this page help you?