Payments & Billing
When to use
- Selecting or integrating a payment provider (Stripe, Adyen, PayPal, Square, Braintree, Razorpay, Mercado Pago, regional PSPs).
- Building checkout: one-time charges, saved cards, guest vs. authenticated, 3DS/SCA, buy-now-pay-later.
- Implementing subscription billing: plans, trials, metered usage, proration, dunning, grace periods, reactivation.
- Implementing invoicing, tax calculation (Stripe Tax, Avalara, TaxJar), coupons, promotions.
- Wiring payment webhooks with idempotency and retry safety.
- Implementing refunds, disputes/chargebacks, and partial captures.
- Reconciling provider payouts with internal ledger; handling failed payouts.
- Reducing PCI DSS scope; meeting SCA/3DS2 requirements for EU/UK.
- Supporting regional/alternative payment methods (iDEAL, SEPA Direct Debit, ACH, PIX, UPI, Alipay, WeChat Pay, BNPL).
Applies to web, mobile (native + webview), backend APIs, platforms (SaaS, marketplace, e-commerce), and internal billing tools.
Workflow
Phase 1 — Scope & Compliance First
1. Classify the payment use case.
Determine: one-time vs. recurring, platform (collect on behalf of others?) vs. direct, B2C vs. B2B, geographies and currencies, required alternative payment methods. A marketplace collecting on behalf of sellers is fundamentally different from a SaaS billing its own customers — provider features and contract structures differ.
2. Assess PCI scope and SCA obligations.
- Use a provider-hosted payment element (Stripe Elements / PaymentElement, Adyen Drop-in, Braintree Hosted Fields) to keep card data out of your servers and reach PCI DSS SAQ A (the lowest scope). Never accept raw card numbers server-side unless you have a full PCI ROC.
- For EU/UK transactions, 3DS2/SCA is mandatory for most consumer card transactions. Use a provider that handles SCA authentication automatically (Stripe's PaymentIntents API, Adyen's 3DS2 server, etc.).
- Classify card data sensitivity: PANs must never appear in logs, analytics, error messages, or client-side JS outside the secure iframe.
3. Choose the provider and integration model.
Consult .claude/stack-matrix/payments.md. Key decision axes: global coverage, split-pay/connect for marketplaces, SCA support, local payment methods, contract/pricing model, and SDK maturity for your platform. Document the choice via .claude/templates/decision-record.md.
Phase 2 — Design the Payment Model
4. Design the data model before writing code.
Minimum entities for any payment integration:
Customer → maps your user to provider customer ID (idempotent creation)
PaymentMethod → stored card/bank/wallet token (never raw PAN)
Order / Cart → what is being purchased, amounts, tax, currency
PaymentIntent → one payment attempt lifecycle (created → processing → succeeded / failed)
Subscription → recurring plan: plan_id, status, current_period, trial, cancel_at
Invoice → line items, amounts, tax, due date, status
Refund → linked to PaymentIntent, amount, reason, status
WebhookEvent → idempotency_key, provider_event_id, type, payload, processed_at, status
Record in docs/specs/payment-data-model.md using .claude/templates/data-model.md. Include the currency precision rule: store all amounts as integers in the smallest currency unit (cents, pence, øre). Never float.
5. Map the happy path and every failure path.
Draw out: checkout initiation → payment intent creation → 3DS challenge (if required) → webhook confirmation → order fulfillment. Then map: declined card, 3DS failure, network timeout, duplicate submission, refund requested, dispute opened, payout failed. Each failure path needs a defined user-facing message, a retry policy, and an idempotency strategy.
6. Design webhook processing as a queue, not a synchronous handler.
Webhooks must be: (a) received and immediately ACKed with HTTP 200, (b) stored in a webhook_events table with the raw payload and idempotency key, (c) processed asynchronously by a worker, (d) idempotent — running the same event twice must be safe. See Standards.
Phase 3 — Implement
7. Implement server-side PaymentIntent / charge creation.
- Always create the PaymentIntent server-side, never client-side. The client only confirms.
- Pass
idempotency_key on every mutating API call (use order ID + attempt number, or a UUID stored with the order). Stripe, Adyen, and Braintree all support this header.
- Set
capture_method: manual if you need to authorize now and capture after fulfillment (physical goods, reservations).
- For subscriptions, use the provider's subscription API (Stripe Subscriptions + Invoices, Adyen Recurring) rather than rolling your own. Provider handles dunning, retry logic, and SCA re-authentication.
8. Implement the client-side payment UI.
- Embed the provider's hosted element (Stripe PaymentElement, Adyen Drop-in) inside your checkout UI. This element handles card input, 3DS flow, and APM rendering without your page ever touching raw card data.
- On confirmation, the client calls
stripe.confirmPayment() (or equivalent), which handles the 3DS redirect and returns a result. Your client then polls your backend for order status — never trust client-side confirmation alone; always confirm via webhook.
9. Implement webhook handler.
POST /webhooks/stripe (or /webhooks/adyen etc.)
1. Verify signature (Stripe-Signature header / HMAC)
2. Insert into webhook_events (event_id, type, payload) ON CONFLICT DO NOTHING
3. Return 200 immediately
(worker) Process event by type:
payment_intent.succeeded → fulfill order
payment_intent.payment_failed → update order, notify user
invoice.payment_failed → dunning flow
charge.dispute.created → alert ops, freeze fulfillment if needed
customer.subscription.deleted → revoke access
10. Implement refunds and disputes.
- Refunds are always initiated server-side via the provider API. Partial refunds require tracking refunded amount on the order.
- Store dispute metadata; chargebacks can arrive months after payment. Do not delete order/payment records based on data-retention policies that would prevent dispute evidence submission.
- Wire
charge.dispute.created webhook to alert ops within minutes.
11. Implement reconciliation.
- Daily/hourly: fetch provider payout reports (Stripe Sigma, Adyen Settlement Detail Report) and reconcile against your internal order/payment records.
- Check: amount transferred = sum(succeeded charges) - fees - refunds - disputes for that payout window. Any gap is a bug or fraud signal.
- Store provider fee per transaction to enable margin analysis.
Phase 4 — Harden & Audit
12. Run the payment security checklist (cross-ref .claude/checklists/security.md).
- Webhook signature verification in place and tested.
- No card data in logs, analytics, Sentry, or error messages.
- Idempotency key on every mutating provider API call.
- Provider API key in secret manager, not in code or
.env committed to VCS.
- Test mode vs. live mode keys are different; only live keys are in production secrets.
- Stripe Radar / Adyen fraud rules configured.
- Amount mismatch validation: confirm server-side that the amount on the PaymentIntent matches your order total before fulfilling.
13. Test in the provider's sandbox using test card suites.
Test: success, generic decline, insufficient funds, 3DS required, 3DS failed, network error, duplicate idempotency key, refund, partial refund, dispute. For subscriptions: trial end, invoice payment, failed renewal, dunning retry, cancellation, reactivation.
Standards
Do:
- Store amounts as integer cents/smallest-unit. Convert to display format only in the UI layer. Use a
Money value object or dedicated library (dinero.js, py-moneyed) for arithmetic to avoid float rounding.
- Verify webhook signatures on every inbound event. Reject without a valid signature.
- Use idempotency keys on all mutating provider API calls; include the attempt number if supporting retries.
- Fulfill orders only after receiving the
payment_intent.succeeded (or equivalent) webhook — never on client-side confirmation alone.
- Keep provider keys in a secret manager (AWS Secrets Manager, Vault, Doppler); rotate on suspected exposure.
- Use
HTTPS everywhere, enforce TLS 1.2+ on webhook endpoints.
- Handle
402, 429 (rate limit), 500-range provider errors: retry with exponential backoff, log the error with the provider's request ID.
- Scope PCI surface to provider-hosted fields only (SAQ A). If you ever need to accept raw card data, engage a qualified security assessor.
- Support 3DS2/SCA for EU/UK from day one — retrofitting later is expensive.
- Test cancellations, refunds, and failed charges in staging before every release that touches billing.
Do not:
- Log, store, or transmit PANs, CVVs, or full card numbers. Not in application logs, analytics events, Sentry breadcrumbs, or support tickets.
- Trust the client's reported payment status — always confirm server-side via webhook.
- Use floating-point numbers for monetary amounts anywhere in the stack.
- Retry webhook processing without idempotency guards (double-fulfillment risk).
- Use test API keys in production or production keys in test/staging.
- Hard-code prices or amounts client-side where they could be tampered; validate server-side.
- Roll your own recurring billing logic (dunning, proration) when the provider offers it.
- Delete payment or order records while a chargeback window is still open (typically 120 days for Visa/Mastercard, 180 for Amex).
Common mistakes to avoid
- Double-fulfillment: processing
payment_intent.succeeded twice because the worker isn't idempotent. Fix: INSERT ... ON CONFLICT DO NOTHING on webhook_events.event_id, process only once.
- Trusting client redirect: fulfilling an order because the browser returned to the success URL — not because the webhook confirmed. Redirects can be spoofed.
- Currency mismatch: creating a PaymentIntent for
$10.00 but storing 10.00 (float) in the database, then comparing to 1000 cents. Use one canonical representation.
- Missing SCA handling for saved cards: off-session payments (subscription renewals, saved-card reuse) require
setup_future_usage: off_session during initial setup and proper handling of requires_action on renewal.
- Webhook secret not verified: skipping signature verification because "we'll add it later" — this allows anyone to send fake fulfillment events.
- Provider rate limits in refund storms: bulk refund flows hit rate limits. Implement a queue with back-pressure.
- No payout reconciliation: assuming the provider deposits exactly what you expect. Fees, disputes, and processing errors cause discrepancies; catch them daily.
- Tax not collected correctly: treating price as inclusive of tax everywhere vs. exclusive in some jurisdictions. Use a tax calculation service (Stripe Tax, Avalara) rather than hard-coding rates.
Output format
Primary deliverables:
- Payment integration spec:
docs/specs/payment-integration.md (use .claude/templates/feature-spec.md + data model section from .claude/templates/data-model.md). Cover: provider choice rationale, data model, event flow diagrams, PCI scope decision, SCA strategy, webhook contract, reconciliation approach, test plan.
- Decision record:
docs/decisions/payment-provider-<name>.md (use .claude/templates/decision-record.md).
- Runbook for payment incidents (webhook processing failures, payout failures, dispute surges):
docs/runbooks/payments.md (use .claude/templates/runbook.md).
Supporting code artifacts (in the project source):
- Server-side payment service (PaymentIntent creation, webhook handling, refunds) with idempotency.
- Webhook event table migration + idempotent worker.
- Reconciliation job/script.
- Test suite covering all card scenarios.
Related checklists
.claude/checklists/security.md — webhook signature verification, secrets, PCI surface.
.claude/checklists/production.md — payment keys in secret manager, monitoring on failed payments.
.claude/checklists/qa.md — payment test coverage (success, decline, 3DS, refund, dispute, dunning).
Related agents
.claude/agents/engineering/payments-engineer.md — primary implementation owner.
.claude/agents/quality/security-auditor.md — PCI scope review, webhook security, secret management.
.claude/agents/quality/production-readiness-auditor.md — reconciliation, alerting on payment failures.
.claude/agents/domain/fintech-domain-expert.md — regulatory nuances, regional payment methods, compliance.
.claude/agents/domain/ecommerce-domain-expert.md — checkout UX, cart abandonment, refund policies.
1---2name: payments3description: Use when integrating payments (Stripe, Adyen, PayPal, Square, regional PSPs) — checkout, subscriptions, webhooks, refunds, disputes, reconciliation, PCI/SCA, iDEAL/SEPA/PIX.4---56# Payments & Billing78## When to use9- Selecting or integrating a payment provider (Stripe, Adyen, PayPal, Square, Braintree, Razorpay, Mercado Pago, regional PSPs).10- Building checkout: one-time charges, saved cards, guest vs. authenticated, 3DS/SCA, buy-now-pay-later.11- Implementing subscription billing: plans, trials, metered usage, proration, dunning, grace periods, reactivation.12- Implementing invoicing, tax calculation (Stripe Tax, Avalara, TaxJar), coupons, promotions.13- Wiring payment webhooks with idempotency and retry safety.14- Implementing refunds, disputes/chargebacks, and partial captures.15- Reconciling provider payouts with internal ledger; handling failed payouts.16- Reducing PCI DSS scope; meeting SCA/3DS2 requirements for EU/UK.17- Supporting regional/alternative payment methods (iDEAL, SEPA Direct Debit, ACH, PIX, UPI, Alipay, WeChat Pay, BNPL).1819Applies to web, mobile (native + webview), backend APIs, platforms (SaaS, marketplace, e-commerce), and internal billing tools.2021---2223## Workflow2425### Phase 1 — Scope & Compliance First2627**1. Classify the payment use case.**28Determine: one-time vs. recurring, platform (collect on behalf of others?) vs. direct, B2C vs. B2B, geographies and currencies, required alternative payment methods. A marketplace collecting on behalf of sellers is fundamentally different from a SaaS billing its own customers — provider features and contract structures differ.2930**2. Assess PCI scope and SCA obligations.**31- Use a provider-hosted payment element (Stripe Elements / PaymentElement, Adyen Drop-in, Braintree Hosted Fields) to keep card data out of your servers and reach **PCI DSS SAQ A** (the lowest scope). Never accept raw card numbers server-side unless you have a full PCI ROC.32- For EU/UK transactions, 3DS2/SCA is mandatory for most consumer card transactions. Use a provider that handles SCA authentication automatically (Stripe's PaymentIntents API, Adyen's 3DS2 server, etc.).33- Classify card data sensitivity: PANs must never appear in logs, analytics, error messages, or client-side JS outside the secure iframe.3435**3. Choose the provider and integration model.**36Consult `.claude/stack-matrix/payments.md`. Key decision axes: global coverage, split-pay/connect for marketplaces, SCA support, local payment methods, contract/pricing model, and SDK maturity for your platform. Document the choice via `.claude/templates/decision-record.md`.3738---3940### Phase 2 — Design the Payment Model4142**4. Design the data model before writing code.**43Minimum entities for any payment integration:4445```46Customer → maps your user to provider customer ID (idempotent creation)47PaymentMethod → stored card/bank/wallet token (never raw PAN)48Order / Cart → what is being purchased, amounts, tax, currency49PaymentIntent → one payment attempt lifecycle (created → processing → succeeded / failed)50Subscription → recurring plan: plan_id, status, current_period, trial, cancel_at51Invoice → line items, amounts, tax, due date, status52Refund → linked to PaymentIntent, amount, reason, status53WebhookEvent → idempotency_key, provider_event_id, type, payload, processed_at, status54```5556Record in `docs/specs/payment-data-model.md` using `.claude/templates/data-model.md`. Include the currency precision rule: store all amounts as integers in the smallest currency unit (cents, pence, øre). Never float.5758**5. Map the happy path and every failure path.**59Draw out: checkout initiation → payment intent creation → 3DS challenge (if required) → webhook confirmation → order fulfillment. Then map: declined card, 3DS failure, network timeout, duplicate submission, refund requested, dispute opened, payout failed. Each failure path needs a defined user-facing message, a retry policy, and an idempotency strategy.6061**6. Design webhook processing as a queue, not a synchronous handler.**62Webhooks must be: (a) received and immediately ACKed with HTTP 200, (b) stored in a webhook_events table with the raw payload and idempotency key, (c) processed asynchronously by a worker, (d) idempotent — running the same event twice must be safe. See Standards.6364---6566### Phase 3 — Implement6768**7. Implement server-side PaymentIntent / charge creation.**69- Always create the PaymentIntent server-side, never client-side. The client only confirms.70- Pass `idempotency_key` on every mutating API call (use order ID + attempt number, or a UUID stored with the order). Stripe, Adyen, and Braintree all support this header.71- Set `capture_method: manual` if you need to authorize now and capture after fulfillment (physical goods, reservations).72- For subscriptions, use the provider's subscription API (Stripe Subscriptions + Invoices, Adyen Recurring) rather than rolling your own. Provider handles dunning, retry logic, and SCA re-authentication.7374**8. Implement the client-side payment UI.**75- Embed the provider's hosted element (Stripe PaymentElement, Adyen Drop-in) inside your checkout UI. This element handles card input, 3DS flow, and APM rendering without your page ever touching raw card data.76- On confirmation, the client calls `stripe.confirmPayment()` (or equivalent), which handles the 3DS redirect and returns a result. Your client then polls your backend for order status — never trust client-side confirmation alone; always confirm via webhook.7778**9. Implement webhook handler.**79```80POST /webhooks/stripe (or /webhooks/adyen etc.)81 1. Verify signature (Stripe-Signature header / HMAC)82 2. Insert into webhook_events (event_id, type, payload) ON CONFLICT DO NOTHING83 3. Return 200 immediately84 (worker) Process event by type:85 payment_intent.succeeded → fulfill order86 payment_intent.payment_failed → update order, notify user87 invoice.payment_failed → dunning flow88 charge.dispute.created → alert ops, freeze fulfillment if needed89 customer.subscription.deleted → revoke access90```9192**10. Implement refunds and disputes.**93- Refunds are always initiated server-side via the provider API. Partial refunds require tracking refunded amount on the order.94- Store dispute metadata; chargebacks can arrive months after payment. Do not delete order/payment records based on data-retention policies that would prevent dispute evidence submission.95- Wire `charge.dispute.created` webhook to alert ops within minutes.9697**11. Implement reconciliation.**98- Daily/hourly: fetch provider payout reports (Stripe Sigma, Adyen Settlement Detail Report) and reconcile against your internal order/payment records.99- Check: amount transferred = sum(succeeded charges) - fees - refunds - disputes for that payout window. Any gap is a bug or fraud signal.100- Store provider fee per transaction to enable margin analysis.101102---103104### Phase 4 — Harden & Audit105106**12. Run the payment security checklist (cross-ref `.claude/checklists/security.md`).**107- Webhook signature verification in place and tested.108- No card data in logs, analytics, Sentry, or error messages.109- Idempotency key on every mutating provider API call.110- Provider API key in secret manager, not in code or `.env` committed to VCS.111- Test mode vs. live mode keys are different; only live keys are in production secrets.112- Stripe Radar / Adyen fraud rules configured.113- Amount mismatch validation: confirm server-side that the amount on the PaymentIntent matches your order total before fulfilling.114115**13. Test in the provider's sandbox using test card suites.**116Test: success, generic decline, insufficient funds, 3DS required, 3DS failed, network error, duplicate idempotency key, refund, partial refund, dispute. For subscriptions: trial end, invoice payment, failed renewal, dunning retry, cancellation, reactivation.117118---119120## Standards121122**Do:**123- Store amounts as integer cents/smallest-unit. Convert to display format only in the UI layer. Use a `Money` value object or dedicated library (dinero.js, py-moneyed) for arithmetic to avoid float rounding.124- Verify webhook signatures on every inbound event. Reject without a valid signature.125- Use idempotency keys on all mutating provider API calls; include the attempt number if supporting retries.126- Fulfill orders only after receiving the `payment_intent.succeeded` (or equivalent) webhook — never on client-side confirmation alone.127- Keep provider keys in a secret manager (AWS Secrets Manager, Vault, Doppler); rotate on suspected exposure.128- Use `HTTPS` everywhere, enforce TLS 1.2+ on webhook endpoints.129- Handle `402`, `429` (rate limit), `500`-range provider errors: retry with exponential backoff, log the error with the provider's request ID.130- Scope PCI surface to provider-hosted fields only (SAQ A). If you ever need to accept raw card data, engage a qualified security assessor.131- Support 3DS2/SCA for EU/UK from day one — retrofitting later is expensive.132- Test cancellations, refunds, and failed charges in staging before every release that touches billing.133134**Do not:**135- Log, store, or transmit PANs, CVVs, or full card numbers. Not in application logs, analytics events, Sentry breadcrumbs, or support tickets.136- Trust the client's reported payment status — always confirm server-side via webhook.137- Use floating-point numbers for monetary amounts anywhere in the stack.138- Retry webhook processing without idempotency guards (double-fulfillment risk).139- Use test API keys in production or production keys in test/staging.140- Hard-code prices or amounts client-side where they could be tampered; validate server-side.141- Roll your own recurring billing logic (dunning, proration) when the provider offers it.142- Delete payment or order records while a chargeback window is still open (typically 120 days for Visa/Mastercard, 180 for Amex).143144---145146## Common mistakes to avoid147148- **Double-fulfillment**: processing `payment_intent.succeeded` twice because the worker isn't idempotent. Fix: `INSERT ... ON CONFLICT DO NOTHING` on `webhook_events.event_id`, process only once.149- **Trusting client redirect**: fulfilling an order because the browser returned to the success URL — not because the webhook confirmed. Redirects can be spoofed.150- **Currency mismatch**: creating a PaymentIntent for `$10.00` but storing `10.00` (float) in the database, then comparing to `1000` cents. Use one canonical representation.151- **Missing SCA handling for saved cards**: off-session payments (subscription renewals, saved-card reuse) require `setup_future_usage: off_session` during initial setup and proper handling of `requires_action` on renewal.152- **Webhook secret not verified**: skipping signature verification because "we'll add it later" — this allows anyone to send fake fulfillment events.153- **Provider rate limits in refund storms**: bulk refund flows hit rate limits. Implement a queue with back-pressure.154- **No payout reconciliation**: assuming the provider deposits exactly what you expect. Fees, disputes, and processing errors cause discrepancies; catch them daily.155- **Tax not collected correctly**: treating price as inclusive of tax everywhere vs. exclusive in some jurisdictions. Use a tax calculation service (Stripe Tax, Avalara) rather than hard-coding rates.156157---158159## Output format160161Primary deliverables:162- **Payment integration spec**: `docs/specs/payment-integration.md` (use `.claude/templates/feature-spec.md` + data model section from `.claude/templates/data-model.md`). Cover: provider choice rationale, data model, event flow diagrams, PCI scope decision, SCA strategy, webhook contract, reconciliation approach, test plan.163- **Decision record**: `docs/decisions/payment-provider-<name>.md` (use `.claude/templates/decision-record.md`).164- **Runbook** for payment incidents (webhook processing failures, payout failures, dispute surges): `docs/runbooks/payments.md` (use `.claude/templates/runbook.md`).165166Supporting code artifacts (in the project source):167- Server-side payment service (PaymentIntent creation, webhook handling, refunds) with idempotency.168- Webhook event table migration + idempotent worker.169- Reconciliation job/script.170- Test suite covering all card scenarios.171172---173174## Related checklists175- `.claude/checklists/security.md` — webhook signature verification, secrets, PCI surface.176- `.claude/checklists/production.md` — payment keys in secret manager, monitoring on failed payments.177- `.claude/checklists/qa.md` — payment test coverage (success, decline, 3DS, refund, dispute, dunning).178179## Related agents180- `.claude/agents/engineering/payments-engineer.md` — primary implementation owner.181- `.claude/agents/quality/security-auditor.md` — PCI scope review, webhook security, secret management.182- `.claude/agents/quality/production-readiness-auditor.md` — reconciliation, alerting on payment failures.183- `.claude/agents/domain/fintech-domain-expert.md` — regulatory nuances, regional payment methods, compliance.184- `.claude/agents/domain/ecommerce-domain-expert.md` — checkout UX, cart abandonment, refund policies.