Blog/Use Cases

How to Use WhatsApp API for Failed Payment Recovery

Recover failed payments automatically using WhatsApp API and Rapiwa. Send payment failure alerts, retry links, and dunning sequences via WhatsApp. Reduces involuntary churn by 30–40%.

by Abida
How to Use WhatsApp API for Failed Payment Recovery

You can recover failed payments automatically by sending WhatsApp messages via Rapiwa API when a payment fails — for subscriptions, e-commerce orders, or invoice payments. WhatsApp recovery messages have a 98% open rate versus 20% for email dunning sequences, making them 3–5x more effective at recovering failed charges. Rapiwa costs $5/month flat with no per-message fees.

Why Failed Payment Recovery Matters

Involuntary churn (churn from failed payments, not customer intent) accounts for 20–40% of all SaaS and subscription churn. An email dunning sequence recovers ~15% of failed payments. A WhatsApp dunning sequence recovers 40–60% — because the messages are actually read.

For e-commerce, payment failures cost stores 3–5% of total revenue from orders that simply never completed.

Failed Payment Recovery Architecture

Payment processor webhook (Stripe, PayPal, etc.)
  → Payment failure event
  → n8n or your server
  → Rapiwa API
  → WhatsApp message to customer
  → Wait 3 days → Second reminder
  → Wait 7 days → Final notice
  → Update CRM/account status

Step 1: Set Up the Stripe Webhook

# Stripe sends webhook events for payment failures
# Register at: Stripe Dashboard → Developers → Webhooks

# Event: payment_intent.payment_failed
# or: invoice.payment_failed (for subscriptions)

Step 2: Detect Failed Payments and Send WhatsApp

Python (Flask webhook handler)

# payment_recovery.py
# pip install flask stripe requests

from flask import Flask, request, jsonify
import stripe
import requests
import time
from datetime import datetime, timedelta

app = Flask(__name__)

STRIPE_WEBHOOK_SECRET = 'whsec_...'
RAPIWA_API_KEY = 'YOUR_API_KEY'

@app.route('/webhooks/stripe', methods=['POST'])
def stripe_webhook():
    payload = request.data
    sig_header = request.headers.get('Stripe-Signature')
    
    try:
        event = stripe.Webhook.construct_event(payload, sig_header, STRIPE_WEBHOOK_SECRET)
    except ValueError:
        return jsonify({'error': 'Invalid payload'}), 400
    except stripe.error.SignatureVerificationError:
        return jsonify({'error': 'Invalid signature'}), 400
    
    if event['type'] == 'invoice.payment_failed':
        handle_subscription_payment_failed(event['data']['object'])
    
    elif event['type'] == 'payment_intent.payment_failed':
        handle_order_payment_failed(event['data']['object'])
    
    return jsonify({'status': 'ok'})


def handle_subscription_payment_failed(invoice: dict) -> None:
    """Handle failed subscription payment from Stripe invoice event."""
    customer_id = invoice['customer']
    attempt_count = invoice.get('attempt_count', 1)
    
    # Fetch customer from Stripe to get metadata (phone number)
    customer = stripe.Customer.retrieve(customer_id)
    phone = customer.get('metadata', {}).get('whatsapp_phone')
    name = customer.get('name', 'there')
    
    if not phone:
        return  # No WhatsApp number — fall back to email only
    
    # Get the payment update URL
    payment_url = f"https://app.yourproduct.com/billing/update-card"
    
    message = build_payment_failed_message(
        name=name,
        attempt=attempt_count,
        amount=invoice['amount_due'] / 100,
        currency=invoice['currency'].upper(),
        payment_url=payment_url
    )
    
    send_whatsapp(phone, message)
    
    # Schedule follow-up if first attempt
    if attempt_count == 1:
        schedule_payment_reminder(
            phone=phone,
            name=name,
            payment_url=payment_url,
            send_at=datetime.utcnow() + timedelta(days=3)
        )


def build_payment_failed_message(name: str, attempt: int, 
                                  amount: float, currency: str, 
                                  payment_url: str) -> str:
    """Build the payment failure WhatsApp message based on attempt count."""
    
    if attempt == 1:
        return (
            f"Payment Failed ⚠️\n\n"
            f"Hi {name}! We couldn't charge your card for ${amount:.2f} {currency}.\n\n"
            f"Don't worry — your account is still active. "
            f"Please update your payment method:\n\n"
            f"💳 Update card: {payment_url}\n\n"
            f"Need help? Just reply here!"
        )
    
    elif attempt == 2:
        return (
            f"Second Payment Attempt Failed ⚠️\n\n"
            f"Hi {name}! We tried again but couldn't process your ${amount:.2f} {currency} payment.\n\n"
            f"Your account will be paused in 3 days if payment isn't received.\n\n"
            f"💳 Update now: {payment_url}\n\n"
            f"Or reply HELP if you're having trouble."
        )
    
    else:  # 3rd+ attempt
        return (
            f"⚠️ FINAL NOTICE — Payment Required\n\n"
            f"Hi {name}! This is our final payment notice.\n\n"
            f"Amount due: ${amount:.2f} {currency}\n"
            f"Your account will be suspended if not paid by [DATE+7 days].\n\n"
            f"💳 Resolve now: {payment_url}\n\n"
            f"Or reply to speak with our billing team."
        )


def send_whatsapp(phone: str, message: str) -> dict:
    return requests.post(
        'https://app.rapiwa.com/send-message',
        headers={'Authorization': f'Bearer {RAPIWA_API_KEY}'},
        json={'number': phone, 'message': message}
    ).json()

Step 3: The 3-Message Dunning Sequence

For subscriptions — space messages 3 days apart:

Day 0 (immediately after failure):
⚠️ Payment Failed — $15.00
Friendly tone, single CTA to update card
"Don't worry — your account stays active."

Day 3 (second notice):
⚠️ Second Attempt Failed
Moderate urgency
"Account will be paused in 3 days"

Day 7 (final notice):
⚠️ FINAL NOTICE
Firm tone
"Account suspended on [date] without payment"

Day 14 (if still unpaid):
✅ Suspension notice + winback offer
"Your account has been paused. Come back with 20% off."

Python — Dunning Sequence Runner

def run_payment_dunning_sequence():
    """
    Daily job: Check for customers in the dunning sequence and send follow-up messages.
    """
    conn = get_db()
    
    # Find customers on Day 3 of dunning
    day_3_dunning = conn.execute("""
        SELECT phone, name, amount, payment_url
        FROM payment_failures
        WHERE attempt_count = 1
          AND initial_failure_date = NOW() - INTERVAL '3 days'
          AND day3_sent = FALSE
    """).fetchall()
    
    for customer in day_3_dunning:
        message = build_payment_failed_message(
            name=customer['name'],
            attempt=2,
            amount=customer['amount'],
            currency='USD',
            payment_url=customer['payment_url']
        )
        
        result = send_whatsapp(customer['phone'], message)
        
        if result.get('status') == 'success':
            conn.execute(
                "UPDATE payment_failures SET day3_sent = TRUE WHERE phone = %s",
                [customer['phone']]
            )
    
    conn.close()

Step 4: E-Commerce Order Payment Failure

For one-time purchases:

# Test cURL — verify your setup
curl -X POST https://app.rapiwa.com/send-message \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "number": "8801234567890",
    "message": "Payment Failed ⚠️\n\nHi Sarah! We could not process your payment for order #12345 ($49.99).\n\nYour order is saved — please complete payment:\n💳 https://yourstore.com/orders/12345/pay\n\nYour cart expires in 24 hours. Need help? Just reply!"
  }'

Expected response:

{
  "status": "success",
  "messageId": "msg_payment_abc123",
  "timestamp": "2026-07-14T10:30:00Z"
}

Collecting Phone Numbers for Payment Recovery

Store the customer's WhatsApp phone in your payment processor:

Stripe — store in customer metadata:

stripe.Customer.modify(
    customer_id,
    metadata={'whatsapp_phone': '8801234567890'}
)

Checkout page — collect phone at payment:

<input type="tel" name="whatsapp_phone" 
       placeholder="WhatsApp number (for payment alerts)" 
       required>
<p>We'll send payment confirmation and alerts via WhatsApp.</p>

Results You Can Expect

  • 40–60% payment recovery rate (WhatsApp dunning vs 10–15% email-only)
  • 3–5x faster customer response to payment update requests
  • 20–35% reduction in involuntary churn for subscription businesses
  • 90%+ open rate on payment failure messages (customers don't ignore payment alerts)

Common Errors and Fixes

  • No phone number available: Store phone at signup/checkout. Retroactively collect via "Please add your WhatsApp for account alerts" prompt.
  • 401 from Rapiwa: API key expired — regenerate in Dashboard → API Keys
  • Customer responds but payment not updated: Route WhatsApp replies to your billing team via n8n → Slack notification

FAQ

Does WhatsApp payment recovery work better than email? Yes — consistently. WhatsApp has a 98% open rate vs 20% email. Payment failure messages sent via WhatsApp are read within minutes; email payment reminders are often delayed by hours or days.

Does Rapiwa charge per payment recovery message? No. Rapiwa charges $5/month flat with no per-message fees. Recover payments without worrying about per-message costs.

What if the customer replies to my WhatsApp payment message? Set up a Rapiwa webhook to receive incoming replies. Route "HELP" and other replies to your billing support team via Slack or email notification.

Can I integrate this with PayPal, Paddle, or Chargebee? Yes. Any payment processor that sends webhooks can trigger the Rapiwa API call. The pattern is the same: webhook event → extract customer phone → POST to Rapiwa.

What is the maximum number of dunning messages before it becomes spam? 3 messages over 14 days is the industry standard: Day 0, Day 3, Day 7. Beyond 3 messages, open rates drop and the risk of the customer blocking your number increases.