Declines & failures

Diagnose why a payment was declined and what to do next.

View as MarkdownInstall skills

When a payment ends in failed, VINR returns a structured failure object explaining why. This page maps those codes to root causes, separates retryable failures from permanent ones, and shows how to retry without burning through a customer's card or your decline rate.

Where the reason livesAsk

Every failed payment carries a failure object with a stable code, a human message, and a category you can branch on programmatically. The same object is attached to the payment.failed webhook.

import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

const payment = await vinr.payments.retrieve('pay_3Kd9aZ2eRb');

if (payment.status === 'failed') {
  const { code, category, message, retryable } = payment.failure;
  console.log(`${category}/${code}: ${message} (retryable=${retryable})`);
}

Never surface the raw issuer message to cardholders for fraud-related codes — it can leak signal to bad actors. Show a generic "Your card was declined" and log the detail on your side.

Decline categoriesAsk

The category field groups every failure into one of five buckets. Branch on category for control flow; use code only for analytics.

Prop

Type

Common codesAsk

CodeCategoryMeaningRetryable
insufficient_fundsissuer_declinedNot enough balance/creditYes, later
do_not_honorissuer_declinedGeneric issuer refusal, no reason givenYes, later
card_lost / card_stolenissuer_declinedReported lost or stolenNo
expired_cardinvalid_requestPast expiry dateNo (need new card)
incorrect_cvcinvalid_requestCVC mismatchNo (re-enter)
authentication_requiredauthentication_failedSCA challenge not completedYes, with 3DS
processing_errorprocessing_errorNetwork timeout / rail errorYes, immediately
velocity_exceededfraud_blockedTripped a Radar or custom ruleNo

Soft vs. hard declinesAsk

The single most important distinction for retry logic:

  • Soft declines are temporary. The card is fundamentally usable but the bank said no right nowinsufficient_funds, do_not_honor, processing_error. A later retry can succeed.
  • Hard declines are permanent for that card. card_stolen, expired_card, pickup_card. Retrying the same card will fail again and repeated attempts hurt your authorization rate.

The failure.retryable boolean encodes this so you don't have to maintain the table yourself.

async function handleFailure(payment: any) {
  const { category, retryable } = payment.failure;

  if (!retryable) {
    // Hard decline — ask for a different payment method.
    return promptForNewCard(payment.customer);
  }
  if (category === 'processing_error') {
    return scheduleRetry(payment.id, { delayMs: 30_000 }); // transient: retry soon
  }
  // Soft issuer decline — retry on a smart schedule, not immediately.
  return scheduleRetry(payment.id, { delayMs: 24 * 60 * 60 * 1000 });
}

Retrying intelligentlyAsk

Check retryable first

If failure.retryable is false, stop. Request a new payment method instead of re-submitting the same card.

Reuse a saved method, not a new charge object

Retry against the stored pm_... method with a fresh idempotency key per attempt so a network retry of the same attempt never double-charges.

Space out soft declines

For issuer_declined, retry on a schedule (for example day 1, 3, 5, 7), not in a tight loop. Issuers throttle and may flag merchants who hammer declined cards.

Re-authenticate when SCA is the cause

For authentication_required, re-create the payment and route the customer through a 3DS challenge rather than retrying silently.

curl https://api.vinr.com/v1/payments \
  -H "X-Api-Key: $VINR_SECRET_KEY" \
  -H "Idempotency-Key: retry_pay_3Kd9aZ2eRb_attempt2" \
  -d amount=2500 \
  -d currency=eur \
  -d customer=cust_8Qm2 \
  -d payment_method=pm_1Lz4

Smart-retry behaviour is built into Billing dunning for subscription invoices. Only build your own retry loop for one-off payments.

Reducing decline ratesAsk

  • Send full address and CVC. Issuers approve more often when AVS and CVC data are present.
  • Pass a clear statement_descriptor. Cardholders dispute charges they don't recognise, and unfamiliar descriptors raise issuer suspicion.
  • Use network tokens for saved cards. Tokenised credentials survive card reissues and lift approval rates versus raw PANs.
  • Retry soft declines later, never harder. Volume of retries on a dead card lowers your overall acceptance rate.
  • Test the paths. In sandbox, 4000 0000 0000 0002 forces a decline and 4000 0000 0000 3220 forces a 3DS challenge so you can exercise both branches.

Next stepsAsk

Was this page helpful?
Edit on GitHub

Last updated on

On this page