Pay
Declines

Handling Payment Declines

Learn how to handle payment declines and provide clear feedback to customers


Payment declines occur when a transaction is declined by Payload, an issuing bank, or a card network. Payload may decline transactions for fraud suspicion, duplicate protection, or account limits before reaching the card network. Other declines come from issuing banks or card networks for reasons like insufficient funds, expired cards, or incorrect card details.

Declines are synchronous responses returned immediately when creating a transaction. Payload returns all decline responses with detailed information to help you provide clear feedback. Properly handling declines ensures a smooth customer experience and helps you recover failed payments.

Prerequisites

Before implementing decline handling, it's helpful to learn about the following topics.


Understanding Payment Declines


When a payment is declined, the transaction status is set to declined and includes a specific decline code that indicates why the payment failed. The decline code helps you understand the reason and guide customers toward resolution.

Common decline scenarios

  • Card Issues: Expired card, incorrect CVV, invalid card number
  • Insufficient Funds: Not enough balance to complete the transaction
  • Bank Restrictions: Transaction exceeds spending limits or triggers fraud prevention
  • Duplicate Detection: Transaction appears to be a duplicate attempt
  • Invalid Information: Billing address or ZIP code doesn't match card on file (see Address Verification)

Decline Codes

When a transaction is declined, the status.code field contains a specific decline code that indicates why the payment failed. Use these codes to provide appropriate messaging to customers.

Card Decline Codes

CodeDescriptionCustomer Action
card_expiredThe card has expiredUse a different card or contact bank for replacement
invalid_card_codeThe security code (CVV) is invalidCheck the 3 or 4-digit code on the back of the card
invalid_card_numberThe card number is invalidVerify the card number is entered correctly
insufficient_balInsufficient funds to complete the paymentUse a different payment method or add funds
exceeded_limitTransaction exceeds the card's spending limitContact bank to increase limit or use different card
invalid_zipZIP code doesn't match the cardEnter the billing ZIP code associated with the card
invalid_addressBilling address doesn't match the cardEnter the correct billing address
general_declineCard declined without specific reasonContact bank for more information
suspicious_activityFlagged as potentially fraudulentContact bank to verify legitimate transaction
too_many_attemptsToo many payment attempts detectedWait before trying again
duplicate_attemptTransaction appears to be duplicateVerify transaction wasn't already processed

Bank Account Decline Codes

CodeDescriptionCustomer Action
insufficient_balInsufficient funds for bank debitEnsure sufficient funds in account
exceeded_limitTransaction exceeds account spending limitContact bank to increase limit or use different account
duplicate_attemptsMultiple identical transactions detectedVerify transaction wasn't already processed
suspicious_activityFlagged as potentially fraudulentContact bank to verify legitimate transaction

Processing Decline Codes

CodeDescriptionResolution
processing_issueTemporary processing problemRetry the transaction
issue_reading_cardCard could not be read (physical terminal)Try swiping or inserting card again
not_supportedPayment method or transaction type not supportedUse a different payment method

Handling Declines

Implement proper decline handling based on how you're processing payments.

Handle declines via Payment API

When processing payments through the Transaction API, declines are returned synchronously with an HTTP 400 status code (by default) and include the transaction object with detailed decline information.

import payload
 
pl = payload.Session('secret_key_3bW9...', api_version='v2')
 
try:
    # Attempt to process a payment
    transaction = pl.Transaction.create(
        type='payment',
        amount=150.00,
        sender={
            'method': {
                'type': 'card',
                'card': {
                    'card_number': '4111111111119903',  # Test card for general_decline
                    'expiry': '12/25',
                    'card_code': '123',
                },
                'billing_address': {'postal_code': '12345'},
            }
        },
        receiver={'account_id': 'acct_receiver123'},
    )
 
    print(f"Payment successful: {transaction.id}")
 
except payload.TransactionDeclined as error:
    # Handle declined payment
    transaction = error.transaction
    decline_code = transaction.status.code
    decline_message = transaction.status.message
 
    print(f"Payment declined: {decline_code}")
    print(f"Message: {decline_message}")
 
    # Provide specific guidance based on decline code
    if decline_code == 'insufficient_bal':
        print("Action: Ask customer to use different payment method")
    elif decline_code == 'card_expired':
        print("Action: Request updated card information")
    elif decline_code == 'invalid_card_code':
        print("Action: Ask customer to verify CVV")
 
except Exception as error:
    # Handle other API errors
    print(f"API Error: {error}")

This example demonstrates proper decline handling:

  1. Catch the API error response (HTTP 400 for declines)
  2. Access the transaction object from the error response
  3. Check transaction.status.value to confirm it's 'declined'
  4. Use transaction.status.code to determine the specific decline reason
  5. Display transaction.status.message to the user or create a custom message based on the code

The error response includes the full transaction object, allowing you to log details for analytics and customer support.

Handle declines in payment forms

When using Payment Forms, decline events are delivered through JavaScript event listeners or React component callbacks.

<!DOCTYPE html>
<html>
<head>
  <title>Payment Form with Decline Handling</title>
  <script src="https://payload.com/Payload.js"></script>
</head>
<body>
  <form id="payment-form" pl-form="payment">
    <!-- Payment amount -->
    <label>
      Amount
      <input type="number" pl-input="amount" value="150.00" readonly>
    </label>
 
    <!-- Secure card input field -->
    <label>
      Card Information
      <div pl-input="card"></div>
    </label>
 
    <!-- Error message container -->
    <div id="error-message" style="color: red; display: none;"></div>
 
    <button type="submit">Pay Now</button>
  </form>
 
  <script src="form-handling.js"></script>
</body>
</html>
// Initialize Payload with your client token
const payload = Payload('client_key_...')
const form = new payload.Form('#payment-form')
 
// Handle declined payments
form.on('declined', (event) => {
  const declineCode = event.status_code
  const declineMessage = event.message
 
  // Show error message to user
  const errorDiv = document.getElementById('error-message')
  errorDiv.textContent = declineMessage || 'Payment failed. Please try again.'
  errorDiv.style.display = 'block'
 
  console.log('Decline details:', {
    code: declineCode,
    message: declineMessage,
    transactionId: event.transaction_id
  })
})
 
// Handle successful payments
form.on('processed', (event) => {
  console.log('Payment successful:', event.transaction_id)
  // Redirect to success page or show confirmation
})
 
// Clear error message when user starts typing
form.on('change', () => {
  document.getElementById('error-message').style.display = 'none'
})

This example shows decline handling for payment forms:

  1. Listen for the declined event on the form element (or use onDeclined callback in React)
  2. Access the transaction object from the event detail
  3. Check transaction.status.code to determine the decline reason
  4. Display user-friendly error messages based on the decline code
  5. Allow the customer to retry with corrected information

The declined event fires immediately when Payload returns a decline, allowing you to provide real-time feedback without page reloads.

Handle declines in Checkout Plugin

When using the Checkout Plugin, declined payments are delivered through the onError event callback with transaction details.

<!DOCTYPE html>
<html>
<head>
  <title>Checkout Plugin with Decline Handling</title>
  <script src="https://payload.com/Payload.js"></script>
</head>
<body>
  <button id="checkout-btn" disabled>Pay Now</button>
 
  <script src="plugin-handling.js"></script>
</body>
</html>
const btn = document.getElementById('checkout-btn')
 
// Fetch token and enable button
fetch('/checkout_intent')
  .then(r => r.json())
  .then(data => {
    btn.disabled = false
    btn.onclick = () => {
      Payload(data.token)
      new Payload.Checkout()
        .on('success', evt => {
          console.log('Payment successful!', evt.transaction_id)
          window.location.href = `/payment/success?txn=${evt.transaction_id}`
        })
        .on('declined', evt => {
          console.log('Payment declined', {
            transactionId: evt.transaction_id,
            attempts: evt.payments_attempted,
          })
        })
        .on('closed', () => {
          console.error('Checkout closed before completion')
        })
        .on('closed', () => {
          console.log('Customer closed checkout')
        })
    }
  })
  .catch(err => console.error('Failed to load checkout token:', err))

This example shows decline handling for the Checkout Plugin:

  1. Use the onError callback when initializing the Checkout Plugin
  2. Check if the error contains a transaction object (indicates a payment decline)
  3. Access transaction.status.code to determine the specific decline reason
  4. Display user-friendly error messages based on the decline code
  5. The plugin automatically allows customers to retry with corrected information

When a payment is declined, the Checkout Plugin remains open and displays an error message, allowing customers to update their payment information and retry immediately.


Testing Declines

Use test card numbers to trigger specific decline codes in the test environment. These cards allow you to verify your decline handling logic works correctly.

Test Card Numbers

Use these card numbers in test mode to simulate specific declines:

Decline CodeTest Card NumberExpiryCVV
card_expired4111111111119900AnyAny
duplicate_attempt4111111111119901AnyAny
exceeded_limit4111111111119902AnyAny
general_decline4111111111119903AnyAny
insufficient_bal4111111111119904AnyAny
invalid_card_code4111111111119905AnyAny
invalid_card_number4111111111119906AnyAny
invalid_zip4111111111119907AnyAny
suspicious_activity4111111111119908AnyAny
too_many_attempts4111111111119909AnyAny
invalid_address4111111111119910AnyAny

Testing Workflow

  1. Test Each Decline Code: Verify your application handles each decline code appropriately
  2. Check Error Messages: Ensure user-facing messages are clear and helpful
  3. Verify Retry Flow: Test that customers can retry after correcting information
  4. Test Event Handling: Confirm decline events fire correctly in payment forms
  5. Validate Logging: Ensure decline data is properly logged for analytics
import payload
 
pl = payload.Session('secret_key_3bW9...', api_version='v2')
 
# Test decline codes with specific test card numbers
test_scenarios = [
    {
        'name': 'Card Expired',
        'card_number': '4111111111119900',
        'expected_code': 'card_expired',
    },
    {
        'name': 'Insufficient Balance',
        'card_number': '4111111111119904',
        'expected_code': 'insufficient_bal',
    },
    {
        'name': 'Invalid CVV',
        'card_number': '4111111111119905',
        'expected_code': 'invalid_card_code',
    },
    {'name': 'Invalid ZIP', 'card_number': '4111111111119907', 'expected_code': 'invalid_zip'},
    {
        'name': 'General Decline',
        'card_number': '4111111111119903',
        'expected_code': 'general_decline',
    },
]
 
for scenario in test_scenarios:
    print(f"\n Testing: {scenario['name']}")
    print(f"Expected decline code: {scenario['expected_code']}")
 
    try:
        transaction = pl.Transaction.create(
            type='payment',
            amount=10.00,
            sender={
                'method': {
                    'type': 'card',
                    'card': {
                        'card_number': scenario['card_number'],
                        'expiry': '12/25',
                        'card_code': '123',
                    },
                    'billing_address': {'postal_code': '12345'},
                }
            },
            receiver={'account_id': 'acct_test123'},
        )
 
        print(f"❌ FAILED: Expected decline but payment succeeded")
 
    except payload.TransactionDeclined as error:
        actual_code = error.transaction.status.code
 
        if actual_code == scenario['expected_code']:
            print(f"✓ PASSED: Got expected decline code '{actual_code}'")
        else:
            print(f"❌ FAILED: Expected '{scenario['expected_code']}' but got '{actual_code}'")
 
    except Exception as error:
        print(f"❌ FAILED: Got unexpected error: {error}")

Schema Reference


The following fields are available in the transaction status object when a payment is declined:

Transaction Status

status
object
The status information for this transaction, including the current state, a detailed status code, and a human-readable message explaining the result.
code
enum[string]
A machine-readable code detailing the specific reason or result for the transaction status. This provides granular information about success, failure reasons, or other status conditions.
Values: approved, card_expired, duplicate_attempt, exceeded_limit, general_decline, insufficient_bal, invalid_card_code, invalid_card_number, invalid_zip, invalid_address, invalid_account_number, suspicious_activity, too_many_attempts, processing_issue, issue_reading_card, not_supported, general_reject, general_adjustment, payment_stopped
Read-only permissions: "Undocumented"
message
stringRead-only
A human-readable message describing the transaction status and code. This text is suitable for display to users and provides context about the transaction outcome.
value
enum[string]
The current processing status of the transaction, indicating its lifecycle state such as processing, processed, authorized, declined, voided, or rejected.
Values: processing, authorized, processed, declined, rejected, voided, adjusted

Next Steps

Enhance your decline handling implementation with additional payment features


Reduce Payment Declines

Implement Payment Method Verification to verify cards before charging, use Address Verification to check billing address details against the card on file, and add Fraud Prevention to detect and prevent fraudulent transactions.

Handle Payment Lifecycle

Monitor transaction status with Payment Processing and Transaction Status, receive real-time updates via Webhook Events, and manage payment reversals with Refunds and Voids.

Improve Recurring Billing Success

Set up Autopay for automatic recurring payments, create payment cycles with Billing Schedules, and implement Dunning Management to recover failed recurring payments.


Related articles