Skill — Payment Integration (Idempotent Payment Architecture)
When this skill activates
When building or modifying payment flows, integrating payment providers (Stripe,
PayPal, Braintree), handling subscriptions, processing refunds, or dealing with
any money movement in the system. Also activates for PCI compliance considerations.
Core principle: Idempotency is life — every payment operation must be safe to
retry without charging the customer twice. When in doubt, err on the side of NOT
charging.
Mandatory actions when this skill is active
Payment State Machine
Every payment has a well-defined state machine:
States:
created → processing → succeeded
→ failed → (retry) → processing
succeeded → refund_pending → refunded
succeeded → disputed → dispute_won (funds returned)
→ dispute_lost (funds lost)
State transitions:
- created → processing: charge initiated with provider
- processing → succeeded: provider confirms capture
- processing → failed: provider declines or errors
- succeeded → refund_pending: refund initiated
- refund_pending → refunded: provider confirms refund
Rules:
- State transitions are APPEND-ONLY (never delete payment records)
- Every transition logged with timestamp, actor, and reason
- Failed payments can retry (max 3 attempts with exponential backoff)
- Terminal states: succeeded, refunded, dispute_won, dispute_lost
Idempotency
Idempotency key on every charge call:
Idempotency key format: [user_id]-[order_id]-[attempt_number]
Example: usr_abc123-ord_xyz789-1
Rules:
- Generate idempotency key BEFORE calling payment provider
- Store key in database alongside payment intent
- If retry needed: increment attempt number, generate new key
- Provider stores result by key — retrying same key returns same result
- Key expiry: 24 hours (Stripe default) — don't retry after that
Critical: If the client retries (network timeout, unclear response), the
idempotency key ensures no double charge. This is non-negotiable.
Webhook Processing
Webhook handler requirements:
1. Verify signature FIRST (reject if invalid — no processing)
2. Respond 200 immediately (within 5 seconds)
3. Process the event ASYNCHRONOUSLY (queue for background processing)
4. Process IDEMPOTENTLY (same webhook delivered twice = same outcome)
5. Handle OUT-OF-ORDER delivery (payment_intent.succeeded before payment_intent.created)
Implementation:
POST /webhooks/stripe
1. Verify: stripe.webhooks.constructEvent(body, sig, secret)
2. Dedup: check event.id against processed_events table
3. If already processed: return 200 (idempotent)
4. Queue: enqueue event for async processing
5. Return 200
6. [Async worker]: process event, update payment state, mark as processed
Rules:
- NEVER do business logic synchronously in the webhook handler
- Store raw webhook payload for debugging/replay
- Implement webhook replay for missed events (fetch from provider API)
- Monitor webhook lag (time between event creation and processing)
PCI Scope Minimization
Never touch raw card numbers:
Client-side tokenization flow:
1. User enters card → Stripe.js/Elements captures it
2. Card data goes DIRECTLY to Stripe (never touches your server)
3. Stripe returns a token/PaymentMethod ID
4. Your server uses the token to create charges
Your server NEVER sees: card number, CVV, expiration date
Your PCI scope: SAQ-A (lowest level — just a questionnaire)
Rules:
- Use Stripe Elements, PayPal JS SDK, or equivalent client-side tokenization
- Never log request bodies that might contain card data
- Never store card data in your database (only token references)
- If using iframes: ensure they're from the payment provider's domain
- PCI-DSS audit not required if you stay at SAQ-A level
Subscription Billing
Subscription lifecycle:
States: trial → active → past_due → canceled → expired
trial → active: trial period ends, first charge succeeds
active → past_due: renewal charge fails
past_due → active: retry succeeds
past_due → canceled: all retries exhausted + grace period ended
canceled → active: user resubscribes (new subscription)
Dunning (failed payment recovery):
Day 0: Charge fails → retry immediately
Day 1: Second retry
Day 3: Third retry + email notification ("update payment method")
Day 7: Final retry + urgent email + in-app banner
Day 14: Cancel subscription + final email ("your subscription has ended")
Rules:
- Dunning schedule is configurable per plan tier
- Always give users a way to update payment method without re-subscribing
- Prorate upgrades/downgrades (charge difference immediately or credit)
- Webhook: handle invoice.payment_failed for dunning triggers
Reconciliation
Daily reconciliation process:
Every 24 hours:
1. Fetch all payments from provider API (last 48 hours, overlap for safety)
2. Match against internal payment records
3. Flag discrepancies:
- Payment in provider but not in our DB (missed webhook)
- Payment in our DB but not in provider (ghost record)
- Amount mismatch (partial capture, currency conversion)
- Status mismatch (we say succeeded, provider says failed)
4. Auto-resolve simple cases (missed webhook → replay)
5. Alert on unresolvable discrepancies (requires human review)
Rules:
- Reconciliation runs daily minimum, hourly for high-volume systems
- Use 48-hour overlap window to catch delayed settlements
- Discrepancy alerts go to finance + engineering
- Never auto-resolve amount mismatches (always flag for human)
Self-check before task completion
Before marking a task done when this skill was active:
1---2name: payment-integration3description: Skill — Payment Integration (Idempotent Payment Architecture)4---56# Skill — Payment Integration (Idempotent Payment Architecture)78## When this skill activates9When building or modifying payment flows, integrating payment providers (Stripe,10PayPal, Braintree), handling subscriptions, processing refunds, or dealing with11any money movement in the system. Also activates for PCI compliance considerations.1213Core principle: **Idempotency is life** — every payment operation must be safe to14retry without charging the customer twice. When in doubt, err on the side of NOT15charging.1617## Mandatory actions when this skill is active1819### Payment State Machine20211. **Every payment has a well-defined state machine:**22 ```23 States:24 created → processing → succeeded25 → failed → (retry) → processing26 succeeded → refund_pending → refunded27 succeeded → disputed → dispute_won (funds returned)28 → dispute_lost (funds lost)2930 State transitions:31 - created → processing: charge initiated with provider32 - processing → succeeded: provider confirms capture33 - processing → failed: provider declines or errors34 - succeeded → refund_pending: refund initiated35 - refund_pending → refunded: provider confirms refund36 ```3738 Rules:39 - State transitions are APPEND-ONLY (never delete payment records)40 - Every transition logged with timestamp, actor, and reason41 - Failed payments can retry (max 3 attempts with exponential backoff)42 - Terminal states: succeeded, refunded, dispute_won, dispute_lost4344### Idempotency45462. **Idempotency key on every charge call:**47 ```48 Idempotency key format: [user_id]-[order_id]-[attempt_number]49 Example: usr_abc123-ord_xyz789-15051 Rules:52 - Generate idempotency key BEFORE calling payment provider53 - Store key in database alongside payment intent54 - If retry needed: increment attempt number, generate new key55 - Provider stores result by key — retrying same key returns same result56 - Key expiry: 24 hours (Stripe default) — don't retry after that57 ```5859 Critical: If the client retries (network timeout, unclear response), the60 idempotency key ensures no double charge. This is non-negotiable.6162### Webhook Processing63643. **Webhook handler requirements:**65 ```66 1. Verify signature FIRST (reject if invalid — no processing)67 2. Respond 200 immediately (within 5 seconds)68 3. Process the event ASYNCHRONOUSLY (queue for background processing)69 4. Process IDEMPOTENTLY (same webhook delivered twice = same outcome)70 5. Handle OUT-OF-ORDER delivery (payment_intent.succeeded before payment_intent.created)71 ```7273 Implementation:74 ```75 POST /webhooks/stripe76 1. Verify: stripe.webhooks.constructEvent(body, sig, secret)77 2. Dedup: check event.id against processed_events table78 3. If already processed: return 200 (idempotent)79 4. Queue: enqueue event for async processing80 5. Return 20081 6. [Async worker]: process event, update payment state, mark as processed82 ```8384 Rules:85 - NEVER do business logic synchronously in the webhook handler86 - Store raw webhook payload for debugging/replay87 - Implement webhook replay for missed events (fetch from provider API)88 - Monitor webhook lag (time between event creation and processing)8990### PCI Scope Minimization91924. **Never touch raw card numbers:**93 ```94 Client-side tokenization flow:95 1. User enters card → Stripe.js/Elements captures it96 2. Card data goes DIRECTLY to Stripe (never touches your server)97 3. Stripe returns a token/PaymentMethod ID98 4. Your server uses the token to create charges99100 Your server NEVER sees: card number, CVV, expiration date101 Your PCI scope: SAQ-A (lowest level — just a questionnaire)102 ```103104 Rules:105 - Use Stripe Elements, PayPal JS SDK, or equivalent client-side tokenization106 - Never log request bodies that might contain card data107 - Never store card data in your database (only token references)108 - If using iframes: ensure they're from the payment provider's domain109 - PCI-DSS audit not required if you stay at SAQ-A level110111### Subscription Billing1121135. **Subscription lifecycle:**114 ```115 States: trial → active → past_due → canceled → expired116117 trial → active: trial period ends, first charge succeeds118 active → past_due: renewal charge fails119 past_due → active: retry succeeds120 past_due → canceled: all retries exhausted + grace period ended121 canceled → active: user resubscribes (new subscription)122 ```123124 Dunning (failed payment recovery):125 ```126 Day 0: Charge fails → retry immediately127 Day 1: Second retry128 Day 3: Third retry + email notification ("update payment method")129 Day 7: Final retry + urgent email + in-app banner130 Day 14: Cancel subscription + final email ("your subscription has ended")131 ```132133 Rules:134 - Dunning schedule is configurable per plan tier135 - Always give users a way to update payment method without re-subscribing136 - Prorate upgrades/downgrades (charge difference immediately or credit)137 - Webhook: handle invoice.payment_failed for dunning triggers138139### Reconciliation1401416. **Daily reconciliation process:**142 ```143 Every 24 hours:144 1. Fetch all payments from provider API (last 48 hours, overlap for safety)145 2. Match against internal payment records146 3. Flag discrepancies:147 - Payment in provider but not in our DB (missed webhook)148 - Payment in our DB but not in provider (ghost record)149 - Amount mismatch (partial capture, currency conversion)150 - Status mismatch (we say succeeded, provider says failed)151 4. Auto-resolve simple cases (missed webhook → replay)152 5. Alert on unresolvable discrepancies (requires human review)153 ```154155 Rules:156 - Reconciliation runs daily minimum, hourly for high-volume systems157 - Use 48-hour overlap window to catch delayed settlements158 - Discrepancy alerts go to finance + engineering159 - Never auto-resolve amount mismatches (always flag for human)160161## Self-check before task completion162163Before marking a task done when this skill was active:164165- [ ] Is there a well-defined state machine for payment lifecycle?166- [ ] Does every charge call include an idempotency key?167- [ ] Are webhooks verified (signature), deduplicated, and processed async?168- [ ] Is PCI scope minimized (client-side tokenization, no raw card data on server)?169- [ ] For subscriptions: is the dunning sequence defined with escalating notifications?170- [ ] Is daily reconciliation implemented (provider vs internal records)?171- [ ] Are all payment state transitions logged with timestamp and reason?172- [ ] Has the security-review skill been co-activated for this payment code?