Payment Integration Best Practices for Fintech Applications

A comprehensive guide to integrating payment gateways securely and reliably in fintech applications.

By SyndiScore.ai Team20 min read

Payment integration is one of the most critical—and complex—aspects of fintech software development. Whether you're processing credit cards, ACH transfers, or wire payments, you need to handle transactions securely, reliably, and in compliance with regulations.

In this guide, we'll cover best practices for integrating payment gateways, handling webhooks, managing errors, and ensuring PCI-DSS compliance.

Choosing a Payment Gateway

1. Stripe

Stripe is the most popular payment gateway for modern fintech applications. It offers credit card processing, ACH transfers, international payments, and subscription billing.

Stripe Features:

  • Credit card processing with tokenization
  • ACH Direct Debit for bank transfers
  • Subscription and recurring billing
  • Fraud detection (Stripe Radar)
  • PCI-DSS Level 1 certified
  • Excellent developer experience with SDKs

2. Plaid

Plaid specializes in bank account verification and ACH processing. It's commonly used for connecting user bank accounts and initiating transfers.

Plaid Features:

  • Bank account verification (instant and micro-deposits)
  • Balance checks and transaction history
  • ACH payment initiation
  • Connects to 12,000+ financial institutions
  • Identity verification

3. Dwolla

Dwolla is a white-label ACH payment platform that gives you more control over the payment experience. It's ideal for high-volume ACH processing.

Implementation Best Practices

1. Never Store Card Data

The #1 rule of payment integration: never store credit card numbers, CVVs, or expiration dates in your database. Use tokenization instead.

Example: Stripe Tokenization (Client-Side)

// Client-side: Create token from card details
const stripe = Stripe('pk_test_...');
const cardElement = elements.create('card');

const { token, error } = await stripe.createToken(cardElement);

if (error) {
  console.error(error.message);
} else {
  // Send token to your server (NOT the card details)
  await fetch('/api/payments', {
    method: 'POST',
    body: JSON.stringify({ token: token.id, amount: 5000 }),
  });
}

Example: Server-Side Payment Processing

// Server-side: Process payment with token
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

app.post('/api/payments', async (req, res) => {
  const { token, amount } = req.body;

  try {
    const charge = await stripe.charges.create({
      amount, // Amount in cents
      currency: 'usd',
      source: token,
      description: 'Loan payment',
    });

    // Save payment record to database
    await db.payments.create({
      stripeChargeId: charge.id,
      amount: charge.amount,
      status: charge.status,
    });

    res.json({ success: true, chargeId: charge.id });
  } catch (error) {
    console.error('Payment failed:', error);
    res.status(400).json({ error: error.message });
  }
});

2. Implement Webhook Handling

Payment gateways use webhooks to notify your application about asynchronous events (payment succeeded, payment failed, refund issued). Proper webhook handling is critical for reliable payment processing.

Example: Stripe Webhook Handler

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

app.post('/webhooks/stripe', async (req, res) => {
  const sig = req.headers['stripe-signature'];
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;

  let event;

  try {
    // Verify webhook signature
    event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
  } catch (err) {
    console.error('Webhook signature verification failed:', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle the event
  switch (event.type) {
    case 'payment_intent.succeeded':
      const paymentIntent = event.data.object;
      await handlePaymentSuccess(paymentIntent);
      break;

    case 'payment_intent.payment_failed':
      const failedPayment = event.data.object;
      await handlePaymentFailure(failedPayment);
      break;

    case 'charge.refunded':
      const refund = event.data.object;
      await handleRefund(refund);
      break;

    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  // Return 200 to acknowledge receipt
  res.json({ received: true });
});

Webhook Best Practices:

  • Verify signatures - Always verify webhook signatures to prevent spoofing
  • Return 200 quickly - Process webhooks asynchronously, return 200 immediately
  • Idempotency - Handle duplicate webhooks gracefully (same event sent multiple times)
  • Retry logic - Payment gateways will retry failed webhooks, so be prepared

3. Implement Idempotency

Idempotency ensures that processing the same request multiple times has the same effect as processing it once. This is critical for payment operations to prevent duplicate charges.

Example: Idempotent Payment Processing

app.post('/api/payments', async (req, res) => {
  const { idempotencyKey, amount, token } = req.body;

  // Check if we've already processed this request
  const existingPayment = await db.payments.findOne({ idempotencyKey });

  if (existingPayment) {
    // Return the existing result
    return res.json({
      success: true,
      chargeId: existingPayment.stripeChargeId,
      cached: true
    });
  }

  // Process new payment
  const charge = await stripe.charges.create({
    amount,
    currency: 'usd',
    source: token,
  }, {
    idempotencyKey, // Stripe also supports idempotency keys
  });

  // Save with idempotency key
  await db.payments.create({
    idempotencyKey,
    stripeChargeId: charge.id,
    amount: charge.amount,
  });

  res.json({ success: true, chargeId: charge.id });
});

4. Handle Errors Gracefully

Payment failures are common (insufficient funds, expired cards, fraud detection). Implement proper error handling and user-friendly error messages.

Common Payment Errors:

  • card_declined - Card was declined by issuer
  • insufficient_funds - Not enough money in account
  • expired_card - Card has expired
  • incorrect_cvc - Wrong CVV code
  • processing_error - Generic processing error

ACH Payment Processing

Bank Account Verification

Before processing ACH payments, you must verify bank account ownership. There are two methods: instant verification (via Plaid) and micro-deposits.

Example: Plaid Bank Verification

// Client-side: Plaid Link integration
const { open, ready } = usePlaidLink({
  token: linkToken,
  onSuccess: async (publicToken, metadata) => {
    // Exchange public token for access token
    const response = await fetch('/api/plaid/exchange', {
      method: 'POST',
      body: JSON.stringify({ publicToken }),
    });

    const { accessToken, accountId } = await response.json();

    // Save bank account for future payments
    await saveBankAccount(accessToken, accountId);
  },
});

// Server-side: Exchange token and get account details
app.post('/api/plaid/exchange', async (req, res) => {
  const { publicToken } = req.body;

  const response = await plaidClient.itemPublicTokenExchange({
    public_token: publicToken,
  });

  const accessToken = response.data.access_token;

  // Get account details
  const accounts = await plaidClient.accountsGet({
    access_token: accessToken,
  });

  res.json({
    accessToken,
    accountId: accounts.data.accounts[0].account_id
  });
});

ACH Payment Timing

Unlike credit cards (instant), ACH payments take 3-5 business days to settle. Your application must handle this delay and notify users appropriately.

Security & Compliance

PCI-DSS Compliance

If you handle credit card data, you must comply with PCI-DSS (Payment Card Industry Data Security Standard). The easiest way to achieve compliance is to use tokenization and never touch card data.

PCI-DSS Compliance Levels:

  • Level 1 - 6M+ transactions/year (most stringent)
  • Level 2 - 1M-6M transactions/year
  • Level 3 - 20K-1M transactions/year
  • Level 4 - <20K transactions/year (SAQ-A if using tokenization)

Fraud Prevention

Implement fraud detection measures like address verification (AVS), CVV checks, velocity limits, and IP geolocation. Stripe Radar provides machine learning-based fraud detection.

Audit Logging

Log all payment events (attempts, successes, failures, refunds) with timestamps, user IDs, and IP addresses. This is critical for compliance and dispute resolution.

Need Help with Payment Integration?

We've integrated payment gateways for lenders, brokers, and fintech companies across the US. Let's discuss your payment processing needs.

Get Started

Related Articles