smaple.tr
payment system integration

Payment System Integration: PSP Orchestration, PCI DSS, and Webhook Architecture [2026]

Mehmet Kurtipek
April 22, 2026
12 min read
payment system integration
PSP orchestration
PCI DSS
3DS2
webhook architecture
payment gateway

Payment system integration is where fintech infrastructure meets transaction economics. A platform processing $10M/month at 2.9% + $0.30 per transaction pays $290,000+ in processing fees annually. Optimizing authorization rates by 3 percentage points, routing 20% of transactions to a lower-cost processor, and recovering 40% of failed recurring payments through smart retry logic can recover $50,000-$80,000 of that cost annually — without adding a single new customer.

This guide focuses on the backend engineering of payment system integration: PSP orchestration and multi-processor routing, 3DS2 technical implementation, PCI DSS scope reduction, idempotency and webhook reliability, and the reconciliation pipeline that ensures your financial books are accurate at the end of every settlement period.

Payment provider selection, commission comparisons, and customer-facing checkout optimization are covered in a companion guide to this one. This guide is for the engineers building the payment infrastructure.

Payment System Integration: Architecture Overview

A production payment processing backend is not a single API integration — it is an orchestration layer that abstracts multiple payment service providers (PSPs), applies routing logic, handles failures, and produces a unified transaction record regardless of which underlying provider processed the payment.

The core architectural components:

Customer Request
    → Payment Intent Creation
    → PSP Router (selects optimal provider)
    → PSP API Call (with idempotency key)
    → Webhook Consumer (receives async status updates)
    → Ledger Update (double-entry transaction recording)
    → Reconciliation Pipeline (daily settlement verification)

Each of these components has specific engineering requirements that determine whether the payment system is robust or fragile.

PSP Orchestration and Multi-Processor Routing

Running a single payment processor is a concentration risk. Any PSP has downtime, maintenance windows, and country-specific authorization rate variability. Enterprise payment platforms route across multiple PSPs to optimize authorization rates and costs.

The Routing Decision

PSP routing decisions happen in real time (< 50ms) and consider:

Authorization rate by BIN/country/processor: historical authorization rates for specific BIN ranges (first 6-8 digits of card number indicate issuing bank and country) on each processor. A UK-issued Visa card may authorize at 95% on Adyen (which has direct Visa network relationships) but only 89% on Stripe in certain markets. The routing engine should select the processor with the highest expected authorization rate for that BIN/country combination.

Processing cost: interchange++ pricing on Adyen vs. blended rate on Stripe. For most BIN/transaction type combinations, one is cheaper. Route to the cheaper option when authorization rates are within 1-2 percentage points.

Processor health: real-time health status (via processor status APIs or your own latency/error rate monitoring). If one processor's error rate spikes above 5%, stop routing to it until it recovers.

Geographic constraints: some processors have limitations on certain markets or currencies. Routing logic must respect these constraints — attempting to route a transaction through a processor that doesn't support the merchant's country is always a failure.

Routing Implementation

interface RoutingDecision {
  psp: PSP;
  expectedAuthRate: number;
  processingCost: number;
  reason: string;
}

class PaymentRouter {
  async route(transaction: TransactionIntent): Promise<RoutingDecision> {
    const eligiblePSPs = this.filterEligible(transaction);

    const scored = await Promise.all(
      eligiblePSPs.map(psp => this.score(psp, transaction))
    );

    return scored.sort((a, b) =>
      // Weighted combination: auth rate weighted 70%, cost 30%
      (b.expectedAuthRate * 0.7 - b.processingCost * 0.3) -
      (a.expectedAuthRate * 0.7 - a.processingCost * 0.3)
    )[0];
  }
}

Routing rules should be stored in the database (not hardcoded) and updatable without deployments. Auth rate data should be updated from settlement data daily; routing changes should be gradual (shift 5% of traffic, measure impact, shift more) rather than all-at-once.

Failover

When the primary PSP returns an error, automatic failover to a secondary PSP recovers the transaction without customer impact:

async function processWithFailover(
  intent: PaymentIntent,
  psps: PSP[]
): Promise<PaymentResult> {
  for (const psp of psps) {
    try {
      return await psp.charge(intent);
    } catch (error) {
      if (isRetriableError(error) && psps.indexOf(psp) < psps.length - 1) {
        logger.warn({ psp: psp.id, error }, 'Payment failed, trying next PSP');
        continue;
      }
      throw error;
    }
  }
}

Retriable errors: network timeout, 500/502/503 from PSP, rate limit. Non-retriable errors: card declined (specific decline codes), card expired, insufficient funds, do-not-honor — these should not be retried on a different PSP (the same card will decline there too).

PCI DSS Scope Reduction

PCI DSS compliance requirements scale with how much card data your systems touch. The simplest and most cost-effective approach: touch as little card data as possible.

Scope Tiers

SAQ-A (lowest scope): all card data is processed by a third-party PCI-compliant PSP. Your systems never see card numbers, CVVs, or expiry dates. You only handle payment tokens. Applicable when using: Stripe Checkout, Adyen Web Drop-in, iyzico hosted page. Ongoing compliance work: annual self-assessment questionnaire, quarterly ASV scans of your internet-facing infrastructure.

SAQ-A-EP: your payment page is served from your domain, but card entry fields are iframes from the PSP (Stripe Elements, Adyen Components). Card data flows directly from the customer's browser to the PSP — not through your servers. PCI scope is minimal but slightly broader than SAQ-A. Requires documented evidence that card data never transits your servers.

SAQ-D (highest scope): your servers process, store, or transmit raw card data. Requires full annual assessment by a Qualified Security Assessor (QSA), quarterly penetration testing, file integrity monitoring, and 12 specific technical requirements. Applicable if you build your own payment form, store card numbers, or proxy card data through your servers.

Strong recommendation: design to achieve SAQ-A or SAQ-A-EP. The operational cost of SAQ-D compliance (QSA assessment: $15,000-$50,000/year; penetration testing: $10,000-$30,000/year; ongoing staff) is rarely justified for platforms that can use PSP-hosted input components.

Tokenization Architecture

Even under SAQ-A, you need a token management layer:

  • PSP issues a payment method token (e.g., pm_1234567890) that represents the customer's stored card
  • Your system stores only the token — never the underlying card data
  • For recurring payments, you use the token to charge without the customer re-entering their card
  • Tokens are PSP-specific; migrating between PSPs requires token migration (most PSPs support this via migration API)

Network Tokens vs. PSP Tokens

Network tokenization (DPAN — Device Primary Account Number) from Visa or Mastercard provides higher authorization rates than PSP vault tokens:

  • Network tokens are recognized across acquirers — if your PSP changes, the token remains valid
  • Issuers assign higher trust to network tokens (they know the token represents a device-bound credential)
  • Authorization rates for network tokens run 2-4 percentage points higher than PSP vault tokens for recurring payments

Adyen and Stripe both support network tokenization — they handle the enrollment with Visa/Mastercard automatically when a customer saves their card. Opt-in at the PSP configuration level.

Idempotency: Preventing Double Charges

Network failures are not exceptional events — they are routine. A payment request that times out at the network layer may or may not have been processed by the PSP. Without idempotency, the safe choice (retry) becomes dangerous (possible double charge). With idempotency, retry is always safe.

Idempotency Key Management

async function createPayment(
  customerId: string,
  amount: number,
  currency: string
): Promise<PaymentResult> {
  // Generate a deterministic idempotency key from payment parameters
  const idempotencyKey = generateUUID(); // One UUID per payment attempt

  // Store the key before the API call
  await db.paymentAttempts.create({
    idempotencyKey,
    customerId,
    amount,
    currency,
    status: 'PENDING',
    createdAt: new Date()
  });

  try {
    const result = await stripe.paymentIntents.create(
      { amount, currency, customer: customerId },
      { idempotencyKey }
    );

    await db.paymentAttempts.update(idempotencyKey, {
      status: 'SUCCEEDED',
      pspPaymentId: result.id
    });

    return result;
  } catch (error) {
    if (isNetworkError(error)) {
      // Safe to retry — same idempotency key will be returned
      throw new RetriableError(idempotencyKey);
    }
    await db.paymentAttempts.update(idempotencyKey, { status: 'FAILED' });
    throw error;
  }
}

Key practices:

  • Generate the idempotency key before the API call, not inline
  • Store the key in your database before making the PSP call (so if the process crashes after the call succeeds, you can check your database and know the key was used)
  • Use the same key for all retries of the same payment attempt
  • Generate a new key for genuinely new payment attempts (customer retrying after a decline)

Webhook Architecture

PSPs deliver payment state changes via webhooks — HTTP POST requests to an endpoint you define. Webhook processing is where most payment integration bugs live.

The Core Reliability Pattern

// Webhook receiver — must respond 200 immediately
app.post('/webhooks/stripe', async (req, res) => {
  // 1. Verify webhook signature first (prevent spoofing)
  const event = stripe.webhooks.constructEvent(
    req.rawBody,
    req.headers['stripe-signature'],
    process.env.STRIPE_WEBHOOK_SECRET
  );

  // 2. Store the raw event (for debugging and replay)
  await db.webhookEvents.upsert({
    id: event.id,
    type: event.type,
    payload: event.data,
    receivedAt: new Date(),
    processed: false
  });

  // 3. Enqueue for async processing — do NOT process inline
  await queue.add('processStripeWebhook', { eventId: event.id });

  // 4. Respond immediately — before processing
  res.status(200).json({ received: true });
});

// Async webhook processor
async function processWebhook(eventId: string): Promise<void> {
  const event = await db.webhookEvents.findById(eventId);

  // 5. Check idempotency — event may have been delivered twice
  if (event.processed) return;

  await db.transaction(async (trx) => {
    await handleWebhookEvent(event, trx);
    await trx.webhookEvents.update(eventId, { processed: true });
  });
}

Why async processing is mandatory: PSPs retry webhook delivery if your endpoint doesn't respond within their timeout (Stripe: 30 seconds; Adyen: 10 seconds). If you process inline and your database operation takes 25 seconds, you get a timeout, the PSP retries, and you process the same event twice (double ledger update). Async + idempotency check is the correct pattern.

Webhook Failure Handling

When webhook processing fails (database error, external API unavailable):

  • Retry with exponential backoff: 2s, 4s, 8s, 16s, 32s
  • After 5 retries, move to dead letter queue (DLQ) with full context
  • Monitor DLQ depth; alert when non-zero
  • Manual review and replay from DLQ for each failed event

Dead letter events require human review because the cause may require a code fix, not just a retry.

Daily Reconciliation Pipeline

Your internal transaction records and PSP settlement records will not always agree. The causes: webhook delivery failures, race conditions in concurrent requests, processing errors, and PSP system issues. Daily reconciliation catches discrepancies before they become accounting problems.

Reconciliation Process

Daily Reconciliation Job (runs 06:00 UTC, after all PSPs settle):

1. Pull settlement report from each PSP for the previous calendar day
2. Load all internal transaction records for the same period
3. Match records by PSP transaction ID
4. Identify discrepancies:
   a. Internal success, PSP failure: manual review (rare — usually webhook lag)
   b. PSP success, no internal record: critical — possible missing transaction
   c. Amount mismatch: accounting issue — investigate and adjust
   d. Fee discrepancy: check PSP pricing changes
5. Generate reconciliation report
6. Alert on-call engineer if unresolved discrepancies > 0
7. Post summary metrics to operational dashboard

Reconciliation reports should be retained for 7 years minimum (standard accounting records retention). Store them in S3 with Glacier lifecycle policy for cost-effective long-term storage.

Settlement Lag

PSPs do not settle immediately. Settlement timelines:

  • Stripe: T+2 business days (standard), T+1 available at higher volume
  • Adyen: T+2 for most markets
  • PayPal: varies by account status, 1-3 business days

Your internal accounting must account for settlement lag — you have a receivable from the PSP for settled-but-not-received funds. This affects cash flow forecasting and should be reflected in your financial model from day one.

Testing Payment Integrations

Payment system testing requires specific approaches:

Sandbox environments: all major PSPs provide sandbox environments with test card numbers. Test cards simulate specific scenarios: successful payment, card declined, insufficient funds, 3DS required, 3DS challenge failed. Test all scenarios explicitly — do not assume success path testing is sufficient.

Edge cases to test explicitly:

  • Network timeout during payment creation (should trigger idempotency retry)
  • Webhook delivery failure followed by retry (should not produce duplicate records)
  • Concurrent payment attempts from the same customer (race condition test)
  • Refund on a partially-refunded payment
  • Subscription renewal with an expired card followed by customer card update
  • Chargeback notification followed by PSP requery

Load testing: determine your peak transaction rate requirement and test 150% of that rate. Identify bottlenecks at the database layer (connection pool exhaustion, lock contention on balance updates), the webhook processing queue (DLQ depth under load), and the reconciliation job (completion time under high volume).

Monitoring and Alerting

Financial system monitoring requires specific metrics beyond standard application monitoring:

Metric Alert Threshold Severity
Authorization rate < 88% (15-min rolling) P1
Webhook processing lag > 60 seconds (queue depth) P2
Reconciliation discrepancy Any unresolved after 2 hours P1
3DS challenge rate > 30% (spike detection) P2
Settlement anomaly Amount deviation > 1% from expected P1
Failed webhook DLQ depth > 0 P2

P1 = page on-call immediately. P2 = alert in Slack, investigate within 2 hours.

Conclusion

Payment system integration is a precision engineering domain. The tolerances are tighter than in standard web application development: a double-charge is a customer support incident; a missing transaction record is an accounting discrepancy; a reconciliation failure is a financial reporting problem.

The investments that pay the highest dividends in production: idempotency from the first API call (not added after the first double-charge incident), webhook processing via async queue (not processing inline and discovering the retry behavior under load), and daily automated reconciliation (not discovering settlement discrepancies during a monthly audit).

These are not particularly complex to implement correctly from the start. They are universally expensive to retrofit after an incident has already occurred.

For payment system integration architecture and development, visit smart-maple.com.

Related Articles

August 11, 2026

MLOps Guide: Taking Machine Learning Models to Production [2026]

87% of machine learning models built by data science teams never reach production. The models work — they pass cross-validation, they score well on holdout sets, they demonstrate genuine predictive value. The problem is not the modeling. The problem is everything that happens between a notebook experiment and a reliable, monitored, production system. MLOps is the discipline that closes that gap. This guide covers the full MLOps stack: maturity levels, tooling choices (MLflow, DVC, Kubeflow

Read More: MLOps Guide: Taking Machine Learning Models to Production [2026]
August 10, 2026

LLM Fine-Tuning Guide: Custom Model Training with LoRA and QLoRA [2026]

General-purpose LLMs are impressive. They can write code, summarize documents, answer questions, and translate between languages with reasonable accuracy. But "reasonable" is not good enough when your application requires consistent output format, domain-specific terminology, a particular tone, or behavior that the base model was never trained to exhibit. That gap is where fine-tuning matters. Fine-tuning updates a model's weights on your specific data, changing how the model behaves — not

Read More: LLM Fine-Tuning Guide: Custom Model Training with LoRA and QLoRA [2026]
August 9, 2026

Computer Vision Applications: Object Detection, OCR, and Industrial AI [2026]

Computer vision has moved well past the research phase. The models are trained, the frameworks are mature, the hardware is accessible, and the use cases are generating measurable returns. What was a specialized capability requiring deep expertise in 2018 is now deployable infrastructure — if you know which component to reach for and where the real complexity lives. This guide covers computer vision applications across industrial, medical, logistics, and document processing domains. It expl

Read More: Computer Vision Applications: Object Detection, OCR, and Industrial AI [2026]