Handling Failed First Payments

Learn how to handle subscription failures when the initial payment doesn't go through.

Overview

When a customer attempts to subscribe to a plan, the first payment is processed immediately. If this initial payment fails, the subscription is not created. Understanding how to handle these failures is crucial for providing a smooth customer experience and maximizing successful conversions.

How First Payment Works

The Subscription Creation Flow

  1. Customer submits subscription purchase
  2. First payment is processed immediately
  3. If payment succeeds: Subscription is created with Active status
  4. If payment fails: Subscription is not created, and an error is returned

Important: Unlike recurring payments, if the first payment fails, no subscription record is created. The customer must retry the entire subscription purchase process.

Common Failure Reasons

Payment Method Issues

ErrorDescriptionCustomer Action
Insufficient FundsNot enough money in accountAdd funds or try different payment method
Card DeclinedIssuing bank declined the transactionContact bank or use different card
Invalid Card DetailsIncorrect card number, CVV, or expirationRe-enter correct card information
Expired CardCard has passed expiration dateUse a different, valid card
AVS MismatchAddress doesn’t match card on fileEnter correct billing address
3DS Authentication FailedCustomer failed 3D Secure challengeRetry 3DS authentication

Account Issues

ErrorDescriptionCustomer Action
Bank Account Not VerifiedACH account needs verificationComplete account verification via Plaid
Account ClosedBank account has been closedUse different account
ACH Not SupportedBank doesn’t support ACHUse card payment instead

Technical Issues

ErrorDescriptionAction
Network TimeoutConnection to payment processor failedRetry the subscription purchase
Invalid Session KeyAuthentication token expiredGenerate new session key and retry
Plan Not FoundSubscription plan doesn’t existVerify plan code is correct

Error Response Format

When first payment fails, you’ll receive an error response:

1{
2 "error": {
3 "code": "PAYMENT_FAILED",
4 "message": "Payment declined: Insufficient funds",
5 "details": {
6 "paymentMethod": "card",
7 "declineCode": "insufficient_funds",
8 "canRetry": true
9 }
10 }
11}

Handling Failures in Your Application

Using Prebuilt UI

The CoinflowPurchase component automatically handles errors:

1<CoinflowPurchase
2 sessionKey={sessionKey}
3 merchantId={merchantId}
4 planCode={planCode}
5 env="sandbox"
6 onSuccess={(data) => {
7 // Subscription created successfully
8 console.log('Subscription ID:', data.subscriptionId);
9 redirectToSuccessPage();
10 }}
11 onError={(error) => {
12 // First payment failed - subscription not created
13 console.error('Subscription failed:', error);
14
15 // Display user-friendly error message
16 if (error.details?.declineCode === 'insufficient_funds') {
17 showError('Payment declined due to insufficient funds. Please try a different payment method.');
18 } else if (error.details?.declineCode === 'card_declined') {
19 showError('Your card was declined. Please contact your bank or try a different card.');
20 } else {
21 showError('Unable to process payment. Please try again or use a different payment method.');
22 }
23 }}
24/>

Using API Integration

Handle errors in your custom implementation:

1async function createSubscription(planCode, paymentDetails) {
2 try {
3 const response = await fetch(
4 'https://api-sandbox.coinflow.cash/api/subscription/{merchantId}/subscribers/card',
5 {
6 method: 'POST',
7 headers: {
8 'Content-Type': 'application/json',
9 'x-coinflow-auth-session-key': sessionKey
10 },
11 body: JSON.stringify({
12 planCode,
13 card: paymentDetails,
14 chargebackProtectionData: [/* ... */]
15 })
16 }
17 );
18
19 if (!response.ok) {
20 const error = await response.json();
21 throw error;
22 }
23
24 const subscriptionId = await response.text();
25 return { success: true, subscriptionId };
26
27 } catch (error) {
28 return {
29 success: false,
30 error: error.error || error,
31 canRetry: error.error?.details?.canRetry !== false
32 };
33 }
34}
35
36// Usage
37const result = await createSubscription('premium-plan', cardDetails);
38
39if (!result.success) {
40 // Handle failure
41 handleSubscriptionError(result.error);
42} else {
43 // Success - subscription created
44 showSuccessMessage(result.subscriptionId);
45}

User Experience Best Practices

Provide Clear, Actionable Error Messages

Always show specific error messages that help customers understand what went wrong and how to fix it:

1function getErrorMessage(errorCode) {
2 const messages = {
3 insufficient_funds: {
4 title: 'Insufficient Funds',
5 message: 'Your payment method doesn\'t have enough funds to complete this purchase.',
6 action: 'Please add funds to your account or try a different payment method.'
7 },
8 card_declined: {
9 title: 'Card Declined',
10 message: 'Your card issuer declined this transaction.',
11 action: 'Please contact your bank for more information or use a different card.'
12 },
13 expired_card: {
14 title: 'Card Expired',
15 message: 'The card you entered has expired.',
16 action: 'Please use a different card with a valid expiration date.'
17 },
18 invalid_cvv: {
19 title: 'Invalid Security Code',
20 message: 'The CVV/security code you entered is incorrect.',
21 action: 'Please check your card and re-enter the 3-digit code on the back.'
22 },
23 authentication_failed: {
24 title: 'Authentication Failed',
25 message: 'Card authentication was not completed successfully.',
26 action: 'Please try again and complete the verification with your bank.'
27 },
28 default: {
29 title: 'Payment Failed',
30 message: 'We were unable to process your payment.',
31 action: 'Please try again or contact support if the problem persists.'
32 }
33 };
34
35 return messages[errorCode] || messages.default;
36}

Implement a Smooth Retry Experience

Make it easy for customers to try again after a failed payment:

1function SubscriptionPurchaseFlow() {
2 const [attempt, setAttempt] = useState(0);
3 const [error, setError] = useState(null);
4
5 const handlePurchase = async (paymentMethod) => {
6 try {
7 const result = await createSubscription(planCode, paymentMethod);
8
9 if (result.success) {
10 // Success!
11 onSubscriptionCreated(result.subscriptionId);
12 } else {
13 // Failed
14 setError(result.error);
15 setAttempt(prev => prev + 1);
16 }
17 } catch (err) {
18 setError(err);
19 }
20 };
21
22 return (
23 <div>
24 {error && (
25 <ErrorAlert error={error} attempt={attempt}>
26 <button onClick={() => setError(null)}>Try Again</button>
27 <button onClick={switchPaymentMethod}>Use Different Payment Method</button>
28 </ErrorAlert>
29 )}
30
31 <PaymentForm onSubmit={handlePurchase} />
32 </div>
33 );
34}

Offer Multiple Payment Options

Give customers flexibility to choose their preferred payment method:

1function PaymentMethodSelector({ onSelect }) {
2 return (
3 <div>
4 <h3>Choose Payment Method</h3>
5 <button onClick={() => onSelect('card')}>
6 Credit/Debit Card
7 </button>
8 <button onClick={() => onSelect('ach')}>
9 Bank Account (ACH)
10 </button>
11 <button onClick={() => onSelect('saved')}>
12 Use Saved Payment Method
13 </button>
14 </div>
15 );
16}

Monitoring and Analytics

Track failed first payments to identify patterns:

1// Log failed subscription attempts
2analytics.track('Subscription Purchase Failed', {
3 planCode: 'premium-plan',
4 paymentMethod: 'card',
5 errorCode: error.code,
6 declineReason: error.details?.declineCode,
7 attemptNumber: attempt,
8 userId: customerId,
9 timestamp: new Date().toISOString()
10});

Key Metrics to Track

  • Failure Rate: Percentage of first payments that fail
  • Failure Reasons: Most common decline codes
  • Retry Success Rate: How often customers succeed on retry
  • Payment Method Performance: Success rate by payment type
  • Conversion After Failure: Do customers eventually subscribe?

Webhooks for Failed Attempts

While subscriptions aren’t created for failed first payments, you can still log these attempts:

1// Your webhook handler
2app.post('/webhooks/coinflow', (req, res) => {
3 const event = req.body;
4
5 if (event.event === 'subscription.payment_failed') {
6 const { customerId, planCode, errorCode } = event.data;
7
8 // Log the failed attempt
9 await db.failedSubscriptions.create({
10 customerId,
11 planCode,
12 errorCode,
13 attemptedAt: new Date()
14 });
15
16 // Send follow-up email
17 if (errorCode === 'insufficient_funds') {
18 await sendEmail(customerId, {
19 subject: 'Complete Your Subscription',
20 body: 'We noticed you tried to subscribe but the payment didn\'t go through...'
21 });
22 }
23 }
24
25 res.sendStatus(200);
26});

Recovery Strategies

Send Targeted Follow-up Emails

After a failed payment attempt, send personalized emails to help customers complete their subscription:

1async function sendRecoveryEmail(customerId, failureReason) {
2 const templates = {
3 insufficient_funds: {
4 subject: 'Complete Your Premium Subscription',
5 body: `
6 Hi there,
7
8 We noticed you tried to subscribe to our Premium plan, but the payment couldn't be processed due to insufficient funds.
9
10 We'd love to have you as a subscriber! Here's what you can do:
11
12 1. Add funds to your account
13 2. Try a different payment method
14 3. Use a credit card instead of bank account
15
16 [Complete Your Subscription]
17
18 If you have questions, reply to this email!
19 `
20 },
21 card_declined: {
22 subject: 'Let\'s Get Your Subscription Started',
23 body: `
24 Hi there,
25
26 Your card issuer declined the subscription payment. This can happen for various reasons:
27
28 - Daily spending limit reached
29 - International transaction restrictions
30 - Suspicious activity flags
31
32 Try these solutions:
33
34 1. Contact your bank to authorize the charge
35 2. Use a different card
36 3. Try again in a few hours
37
38 [Try Again]
39 `
40 }
41 };
42
43 const template = templates[failureReason] || templates.card_declined;
44 await sendEmail(customerId, template);
45}

Retargeting Campaigns

Set up retargeting for failed subscription attempts:

  • Show ads reminding them of the subscription benefits
  • Offer a limited-time discount
  • Highlight social proof and testimonials
  • Make it easy to retry with one click

Testing Failed Payments

Test failure scenarios in sandbox:

1// Use test cards that simulate specific failures
2const testCards = {
3 insufficientFunds: '4000000000009995',
4 cardDeclined: '4000000000000002',
5 expiredCard: '4000000000000069',
6 invalidCVV: '4000000000000127',
7 processingError: '4000000000000119'
8};
9
10// Test with specific decline
11<CoinflowPurchase
12 // ... other props
13 testMode={{
14 card: testCards.insufficientFunds
15 }}
16/>

For more testing scenarios, see our testing guide.