What payment integration discipline actually is
Payments are what everyone integrates fast and hardens slowly. Get a checkout working; ship. Then a trial + partial refund + mid-period upgrade breaks the model. Then a webhook retry causes duplicate charges. Then a dunning retry after cancellation confuses users. Then a dispute wave hits the rate threshold and processor puts you on chargeback monitoring. Discipline is the framework that anticipates these evolutions from day one: state machine-first thinking, idempotency in every webhook handler, minimized PCI scope, reconciliation with processor, observability of transitions.
Not a “buy Stripe” guide. Stripe is often right; discipline matters more than vendor. Adyen, Braintree, Square, regional processors all benefit from the same discipline. Sana K.; pairs with the payments-architect subagent, the stripe MCP, and the payment state machine blog.
Platform selection framework
Stripe: default for most cases; excellent DX; broad global coverage; ~2.9% + 30¢ typical.
Adyen: enterprise-heavy; global reach with local payment methods (iDEAL, SEPA Direct Debit, WeChat Pay, etc.); complex integration; strong for international expansion.
Braintree: PayPal-owned; native PayPal integration; good when PayPal is critical checkout option.
Square: retail + in-person + online; strong POS integration; fits omnichannel commerce.
Regional processors: often required for local payment methods, tax compliance, currency support in specific markets (Rapyd, Nuvei, dLocal, Adyen for local methods).
Payment orchestrators: Primer, Spreedly, Gr4vy. Abstract across multiple processors for redundancy + smart routing + optimization. Advanced; matters at scale.
Selection framework:
- SaaS / subscription with global card acceptance: Stripe.
- Enterprise + international + local methods: Adyen.
- PayPal-native checkout critical: Braintree.
- In-person + online omnichannel: Square.
- Local markets requiring specific methods: regional processor + Stripe/Adyen.
- Multi-processor optimization at scale: orchestrator.
PCI compliance scope
PCI DSS (Payment Card Industry Data Security Standard) applies to any system handling card data. Scope levels:
SAQ A: card data handled entirely by third-party (Stripe Elements, Adyen Drop-in). Your systems never see card data. Simplest scope; annual self-assessment questionnaire.
SAQ A-EP: your site includes third-party (iframe, redirect) but delivers page. Slightly larger scope; still self-assessment.
SAQ D-Merchant: your systems receive card data (server-side POST from form). Significantly larger scope; more audit requirements; higher cost.
Level 1 (audit): >6 million transactions/year. Formal PCI audit by Qualified Security Assessor (QSA). Substantial ongoing compliance program.
Design goal: minimize scope. SAQ A vs. SAQ D-Merchant is often the difference between annual questionnaire and quarterly formal audit + significant infrastructure requirements.
Minimizing PCI scope via hosted forms
Hosted forms keep card data out of your servers. Options:
- Stripe Elements / Checkout: card fields hosted; token returned to your server. SAQ A scope.
- Adyen Drop-in / Components: same concept; SAQ A scope.
- Braintree Hosted Fields: card fields in iframes; SAQ A-EP scope.
- Redirect flows: user redirected to processor's page; return to yours. SAQ A scope.
Anti-pattern: form on your page POSTs card data to your server, then to processor. SAQ D-Merchant scope. Larger scope; audit-heavier; higher risk.
Even “we log the card number for debugging” is a scope violation. Never log card data. Even 4-digit truncations require care.
Webhook signature verification
Every processor signs webhooks. Verify before processing:
- Stripe:
stripe-signatureheader contains HMAC-SHA256 of raw payload + timestamp. Verify against webhook signing secret. - Adyen: HMAC signature; verify per API.
- Braintree: Notification::parse method verifies signature automatically.
Handler pattern:
def handle_webhook(request):
signature = request.headers['Stripe-Signature']
payload = request.raw_body # NOT parsed JSON; raw bytes
event = stripe.Webhook.construct_event(payload, signature, WEBHOOK_SECRET)
# Signature valid; proceed
Rejected signatures = attacker attempting webhook injection. Log + alert.
Common bug: parsing JSON before signature verification. Signature verifies raw bytes; parsed JSON may differ. Verify against raw payload.
Webhook idempotency pattern
Processor retries webhooks. Duplicate delivery is expected. Idempotency prevents duplicate processing:
def handle_webhook(event):
signature_verify(event)
if is_duplicate(event.id):
return 200 # ack idempotent replay
with transaction():
process(event)
mark_processed(event.id)
return 200
Idempotency store: Redis with TTL (e.g., 7 days), database table with unique index on event ID. Cleanup old entries.
Always return 200. If your handler errors, processor retries; you get more copies. Return 200 for legitimate + duplicate + irrelevant events. Log errors; don't reject.
Common bug: exception in handler causes non-200 response; processor retries; on retry, partial state from first attempt causes different behavior. Transactions + idempotency + always-200 pattern prevents.
Subscription state machine design
Explicit states, not boolean columns:
- PENDING: created; not yet paid or trialing.
- TRIALING: in trial period.
- ACTIVE: paid; current period valid.
- PAST_DUE: charge failed; dunning in progress.
- CANCELED_END_OF_PERIOD: user requested cancel; will end at period end.
- CANCELED: not active; can be reactivated within window.
- UNPAID: dunning exhausted; not active.
Legal transitions explicit; illegal transitions rejected + logged. See the payment state machine blog for full pattern.
Payment records (individual charges) also state-machine: PENDING → SUCCEEDED → REFUNDED_PARTIAL / REFUNDED_FULL / DISPUTED → DISPUTE_LOST / DISPUTE_WON.
Trial handling
Trials vary by strategy:
- No card required: user signs up; trial starts. Higher trial conversion; higher trial signup rate. Some abuse.
- Card required upfront: user provides card; not charged until trial ends. Lower trial signup; higher conversion.
- Card at end: user signs up without card; before trial ends, prompted to add card. Middle ground.
State machine: PENDING → TRIALING on trial start. TRIALING → ACTIVE at trial end + successful charge. TRIALING → CANCELED if user cancels or auto-conversion fails.
Anti-pattern: trial + immediate re-enrollment loop. User signs up, cancels, signs up again with different email. Detect via device fingerprint or email variations; enforce trial limit per user.
Dunning schedule
Charge fails on renewal; retry pattern (dunning):
Standard: retry days 3, 7, 14 after initial failure. Grace period during. State: PAST_DUE. If all retries fail: transition to UNPAID or CANCELED (business decision).
Considerations:
- Grace period access: subscription still active during PAST_DUE? Business decision. Common: yes for first N days.
- Email cadence: notify user before retries + on each retry failure. Careful not to spam.
- Manual intervention: support team can extend grace or attempt retry manually.
- Payment method update flow: user should be able to update card easily. Email links directly to update page.
Recovery rate typical 30-50% for expired cards; lower for insufficient funds. Different failure reasons have different retry likelihood.
Cancellation timing
Two policies:
End of period: user cancels; access continues until end of current billing period; no refund. Standard SaaS.
Immediate: user cancels; access ends now; potentially prorated refund. Common for consumer.
State machine: end-of-period = CANCELED_END_OF_PERIOD state until period ends, then CANCELED. Immediate = ACTIVE → CANCELED.
Reactivation window: how long can user reactivate before permanent cancellation? Common: 30 days. Enables win-back campaigns.
Proration
Upgrade mid-period: charge pro-rata for remaining period. Or credit for remaining period + charge new plan.
Downgrade mid-period: credit for unused period (unusual) OR take effect next period (common).
Processors handle proration (Stripe: automatic; Adyen: manual configuration). Verify math on invoice; edge cases (multiple mid-period changes, upgrade + downgrade) get complex.
UX: preview proration before user confirms. Prevents billing surprises.
Refund flows
You initiate refund via API. Full or partial; money returns to customer's card.
State: SUCCEEDED → REFUNDED_PARTIAL or REFUNDED_FULL.
Considerations:
- Refund fees: some processors keep transaction fee even on refund; some return it.
- Refund window: usually 90-180 days; longer via ACH.
- Multi-currency: refund in same currency as charge; FX impact if you've settled.
- Tax on refunds: refund the tax portion; update tax reporting.
- Automated vs. manual: high-value refunds may require approval workflow.
Distinction from dispute: refund is you giving money back; dispute is customer taking it via card issuer.
Dispute + chargeback handling
Customer files dispute via card issuer. Card issuer sends chargeback to processor; you respond with evidence. Won: money stays. Lost: money returned + additional fees.
Evidence collection:
- Order details: what was purchased.
- Shipping proof: tracking number + delivery confirmation.
- IP + device information: matched to account.
- Terms acceptance timestamp.
- Customer communication history.
- For subscription: usage evidence (active use during billing period).
Automated evidence pipeline: collect this info per transaction upfront; ready to respond within processor deadline (typically 7 days).
Response templates by dispute reason (fraud, product not received, etc.) speed response.
Dispute rate thresholds
Card networks (Visa, Mastercard) monitor dispute rates:
- Below 0.75% (Visa): healthy.
- 0.75% - 1%: chargeback monitoring program; enhanced reporting; possible fees.
- Above 1%: high-risk merchant; significant fees; possible account termination.
Sources of high dispute rate:
- Fraud (stolen cards being used on your site).
- Friendly fraud (real customer disputes legitimate charge).
- Poor customer service (customer disputes instead of refunding).
- Recurring billing surprises (customer forgot subscription).
Mitigation: 3D Secure for suspicious transactions; clear billing descriptor; proactive renewal reminders; easy cancellation; responsive customer service.
Multi-currency handling
Three currency concerns:
- Display currency: what user sees. Usually user preference or geo-based.
- Charge currency: currency card charged in. Local currency preferred (avoids user FX fee).
- Settlement currency: currency you receive. Processor converts + settles to your bank.
Stripe multi-currency: charge in local currency; automatic conversion to your bank currency. FX rate marked-up by processor (~1-2%).
Alternative: charge in USD (or single currency); user's card issuer handles FX (may cost user more). Simpler for you; worse UX.
For subscription: multi-currency subscription = plan priced per currency. Manage price parity or accept FX-drift.
Tax handling
Tax varies by jurisdiction:
- US sales tax: per-state; sometimes per-locality. Nexus rules (economic + physical presence) determine where you must collect.
- EU VAT: destination-based (charge based on customer location); reverse-charge for B2B with valid VAT ID.
- Other jurisdictions: GST (Canada, Australia, India); other consumption taxes.
Manual calculation = mistakes. Recommended:
- Stripe Tax: automatic calculation + collection; integrates with Stripe Billing.
- TaxJar: tax engine; broad; connects to Stripe + others.
- Anrok: SaaS-focused; handles complex SaaS-specific tax rules.
Registration: automated calculation doesn't relieve registration obligation. Register in jurisdictions where you have nexus.
Invoicing (VAT-compliant)
B2B customers expect invoices. VAT-compliant invoices required in EU + other jurisdictions:
- Seller name + address + tax ID.
- Buyer name + address + tax ID (if applicable).
- Invoice number + date.
- Line items with description, quantity, unit price, tax rate, total.
- Currency.
- Tax breakdown.
Stripe Billing generates VAT-compliant invoices. Other processors similar. DIY invoice generation requires jurisdiction-specific requirements review.
Fraud considerations
Fraud detection at multiple layers:
Velocity checks: same card, IP, device used N times in window. Flag or block. In-app or via processor tools.
3D Secure: additional authentication via card issuer (customer receives OTP or biometric). Shifts liability to issuer for approved transactions. Adds friction; conversion impact.
Stripe Radar / Adyen RevenueProtect: ML-based fraud scoring; rules engine; team review queue. Standard for their platforms.
Manual review: high-value or borderline transactions to human reviewer. Common for high-ticket items or new accounts.
Address Verification System (AVS): verify billing address matches card. Fraud indicator; not blocker.
Reconciliation with processor
Internal ledger (your DB) can diverge from processor ledger. Reasons:
- Missed webhook events.
- Bug in state machine transitions.
- Processor-side manual adjustments.
- Currency conversion differences.
Daily reconciliation job:
- Fetch processor events (charges, refunds, subscription events) for previous day.
- Compare to internal records.
- Flag divergence for review.
Divergence rate should be near zero. Non-zero = investigate; automate resolution for common patterns; alert on unusual spike.
Without reconciliation: divergence grows silently. Eventually 6-month audit finds thousands of mismatched records.
Observability that matters
Payment-specific metrics:
- Charge success rate: succeeded / attempted. Overall + by card issuer + by country.
- Decline reasons: breakdown of why charges failed.
- State transitions per second: subscription lifecycle health.
- Illegal transition attempts: should be near zero.
- Webhook processing latency: event to state update.
- Reconciliation divergence: daily count.
- Dispute rate: disputes / charges (window).
- Dunning success rate: PAST_DUE → ACTIVE.
- Refund rate + reasons.
Dashboards for finance + engineering + customer service. Alerts on: charge success rate drop, dispute rate climb, illegal transitions, reconciliation divergence spike, webhook processing lag.
Marketplace patterns
Marketplaces (multiple sellers) add complexity:
- Stripe Connect: Standard, Express, Custom account types. Different levels of onboarding + control.
- Adyen for Platforms: equivalent; enterprise-focused.
Money movement:
- Charge to platform; transfer to seller.
- Charge direct to seller (destination charge).
- Application fee / commission on transfer.
KYC + compliance: sellers must be identity-verified. Automated via processor onboarding. Requirements vary by jurisdiction + volume.
Refunds affect platform + seller: refund reduces future transfers to seller or reverses prior transfer.
Migration between processors
Migration is expensive. Reasons:
- Cost pressure at scale.
- Feature need (specific payment methods, better fraud tools).
- Geographic expansion requiring different processor.
Migration pattern:
- Deploy new processor integration alongside existing.
- New customers on new processor.
- Existing customers migrated in waves (with re-auth for tokenized cards).
- Dual-run for period with reconciliation on both.
- Sunset old processor.
Card tokenization: cards tokenized on old processor may not migrate directly (PCI + processor-specific). Some processors offer card-on-file migration (network-to-network); others require re-collection.
Months, not weeks. Migration during high-volume periods (holidays) not recommended.
The failure patterns you will see
Missing idempotency: webhook retry causes duplicate charges or double refunds.
State transitions out of order: subscription webhook events arrive in wrong order; state corrupt.
PCI scope creep: card data logged accidentally; scope explodes.
Currency mismatch: charge in USD; refund in EUR; FX loss.
Dispute rate uncontrolled: fraud loss + processor penalties.
Tax calculation wrong: legal exposure + customer complaints.
Reconciliation gap: internal ledger diverges from processor ledger; hard to fix later.
Dunning after cancellation: canceled user still receives dunning emails + retries.
Trial abuse: same user re-enrolling in trial with different emails.
Card expiry surge: many cards expire same month; renewal failures spike.
Metrics that matter
- Charge success rate: overall + by segment.
- Recurring charge success: renewal reliability.
- Time to first payment: friction indicator.
- Trial-to-paid conversion rate: TRIALING → ACTIVE.
- Dunning recovery rate: PAST_DUE → ACTIVE.
- Dispute rate: rolling 30/60 day.
- Refund rate: with reason breakdown.
- Reconciliation divergence: daily.
- Illegal transition rate: should be zero.
- Webhook processing latency: event to updated state.
What to do next
If your webhook handlers don't verify signatures: fix that. Not optional.
If your webhook handlers aren't idempotent: fix that. Duplicate events cause duplicate charges.
If your subscription state is boolean columns: consider migrating to explicit state machine. See the state machine blog.
If your PCI scope is SAQ D-Merchant when SAQ A would work: fix that. Substantial audit + risk reduction.
If you don't reconcile daily with your processor: start. Divergence grows silently otherwise.
If your dispute rate is climbing: analyze reasons + implement 3D Secure for suspicious transactions + review customer service response times.
Combined with the payments-architect subagent, this framework is what turns payment integration from “the risky domain we ship carefully” into infrastructure that survives edge cases + processor changes + growth. It works.