โ† Back to Articlesยทยท5 min readยทFinTech & Product Architecture

Debugging International Payment Failures & Multi-Currency Routing

How we engineered a pre-auth BIN gateway router, optimized 3DS2 authentication flows, and boosted cross-border checkout conversion by +24.5%.

View Markdown
Summary: Detailed case study on diagnosing cross-border payment failures, building a dynamic BIN-based gateway router, and boosting international transaction success rates.
Problem: International users were experiencing elevated drop-off rates due to 3D Secure 2 authentication friction and cross-border bank rejections
Solution: Engineered a pre-auth gateway router that checks card country BINs, dynamically selects local gateway partners, and executes graceful fallback retries

Debugging International Payment Failures & Multi-Currency Routing

Cross-border payment processing is one of the most unpredictable engineering challenges in consumer FinTech platforms. During rapid international scaling on AstroKarma, we observed a concerning trend: while domestic transaction authorization rates hovered above 92%, international transactions were dropping to 64.2%, leading to lost revenue and customer frustration.

In this deep-dive case study, I outline how we diagnosed the underlying issuer friction, engineered a pre-authentication gateway router in Python / Django, and used Mixpanel analytics to increase international authorization success rates to 88.7% (+24.5% net improvement).


1. Problem Identification: Where Transactions Failed

When inspecting drop-off logs and analytics funnels, generic decline codes like do_not_honor and generic_decline masked the true underlying friction points.

By querying transaction logs grouped by card issuer country and 3D Secure challenge state, we isolated three root causes:

Code Snippet
-- Query to identify failure distribution by card issuing country & 3DS status
SELECT
  card_country,
  three_ds_status,
  COUNT(*) AS total_intents,
  COUNT(CASE WHEN status = 'succeeded' THEN 1 END) AS successful,
  COUNT(CASE WHEN status = 'failed' THEN 1 END) AS failed,
  ROUND(COUNT(CASE WHEN status = 'failed' THEN 1 END) * 100.0 / COUNT(*), 2) AS failure_rate_pct
FROM payment_transactions
WHERE created_at >= NOW() - INTERVAL '30 days'
  AND is_international = TRUE
GROUP BY card_country, three_ds_status
HAVING COUNT(*) > 50
ORDER BY failure_rate_pct DESC;

Core Findings:

  1. 3DS2 Friction & Challenge Drops: European (SCA) and Indian (RBI) issuing banks strictly require 3D Secure 2 authentication. Generic gateway requests were either missing mandatory fields (e.g. billing_address, email) or incorrectly requesting no_challenge_requested, triggering immediate bank declines.
  2. Cross-Border Interchange Fees: Single-gateway routing through US/EU acquirers led to foreign transaction surcharge blocks by local banks in Southeast Asia and LATAM.
  3. Lack of Graceful Fallback: When a primary gateway timed out or rejected a card, the mobile client immediately presented an error instead of attempting a secondary acquirer route.

2. Solution: Building the Pre-Auth BIN Gateway Router

To resolve cross-border bank friction without introducing unnecessary 3DS authentication steps for domestic users, I engineered a Pre-Auth BIN Router in our Django backend microservice.

System Architecture Flow:

Code Snippet
[Mobile App (iOS/Android)]
       โ”‚
       โ”‚  1. Submit Card Token + BIN (First 6-8 digits)
       โ–ผ
[Django Gateway Router]
       โ”‚
       โ”œโ”€โ–บ 2. Query Card BIN Database (Identify Country, Card Type, Issuer)
       โ”œโ”€โ–บ 3. Evaluate Regional 3DS Requirement Rules
       โ”‚
       โ”œโ”€โ”€โ”€ (If Local/EU) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ [Stripe Local Acquirer + 3DS2 Challenge]
       โ”‚
       โ””โ”€โ”€โ”€ (If LatAm / Asia) โ”€โ”€โ”€โ”€โ”€โ–บ [Regional Gateway Partner + Multi-Currency]

3. Implementation: Dynamic Gateway Selection in Django

Code Snippet
import requests
from django.conf import settings
from typing import Dict, Any

class PaymentGatewayRouter:
    """
    Dynamically routes charge requests based on Bank Identification Number (BIN)
    and regional 3DS compliance requirements.
    """
    
    HIGH_SCA_COUNTRIES = {"DE", "FR", "GB", "IN", "ES", "IT", "NL", "SE"}
    
    @classmethod
    def get_bin_metadata(cls, bin_number: str) -> Dict[str, Any]:
        """Lookup BIN country code and card brand from Redis cache or provider."""
        cache_key = f"bin_meta:{bin_number[:8]}"
        # ... Redis lookup logic ...
        return {"country": "IN", "card_type": "credit", "brand": "visa"}

    @classmethod
    def select_optimal_gateway(cls, card_bin: str, amount: int, currency: str) -> Dict[str, Any]:
        bin_info = cls.get_bin_metadata(card_bin)
        country = bin_info.get("country")
        
        # 1. Require explicit 3DS2 challenge for SCA / Regional compliance
        requires_3ds = country in cls.HIGH_SCA_COUNTRIES or currency != "USD"
        
        # 2. Select Acquirer Gateway based on regional authorization rates
        if country in cls.HIGH_SCA_COUNTRIES:
            primary_gateway = "stripe_eu_local"
            secondary_fallback = "adyen_global"
        elif country in {"IN", "SG", "AE"}:
            primary_gateway = "razorpay_int"
            secondary_fallback = "stripe_global"
        else:
            primary_gateway = "stripe_global"
            secondary_fallback = "checkout_com"

        return {
            "primary_gateway": primary_gateway,
            "secondary_fallback": secondary_fallback,
            "requires_3ds": requires_3ds,
            "bin_country": country,
        }

4. Fallback Retries & Client Re-authentication

If the primary gateway returns a soft decline (e.g. try_another_card or network_timeout), the backend automatically attempts the pre-configured secondary acquirer before notifying the mobile app:

Code Snippet
def process_transaction(user, amount: int, currency: str, payment_method_id: str, card_bin: str):
    route = PaymentGatewayRouter.select_optimal_gateway(card_bin, amount, currency)
    
    try:
        # Attempt Primary Gateway Charge
        return execute_charge(route["primary_gateway"], payment_method_id, amount, currency, route["requires_3ds"])
    except SoftDeclineException as err:
        # Graceful Failover to Secondary Acquirer
        logger.warning(f"Primary gateway {route['primary_gateway']} failed. Executing fallback to {route['secondary_fallback']}")
        return execute_charge(route["secondary_fallback"], payment_method_id, amount, currency, route["requires_3ds"])

5. Measurable Impact & Key Results

After deploying the BIN router and optimizing 3DS2 challenge prompts across iOS and Android builds:

| Business & Technical Metric | Before Optimization | After Optimization | Net Improvement | | --- | --- | --- | --- | | International Success Rate | 64.2% | 88.7% | +24.5% net gain | | 3DS2 Challenge Completion | 68.4% | 92.8% | +24.4% | | Checkout Abandonment | 31.5% | 11.2% | -20.3% drop-off | | Average Checkout Latency | 3.1s | 1.6s | ~48% faster |


6. Key Takeaways for Engineering Leaders

  1. Don't Treat All Declines Equally: Differentiating between hard declines (insufficient funds) and soft declines (3DS protocol errors) allows you to implement automatic fallback retries without disrupting the user.
  2. BIN-Aware Routing Saves Conversions: Passing card issuer country metadata to gateway endpoints dramatically increases bank authorization approval rates for international cards.
  3. Data-Driven Funnel Tracking: Instrumenting custom Mixpanel events (Checkout Started, 3DS Prompt Shown, Gateway Handshake Complete) is essential for identifying geographic checkout drop-offs.