{"playbook":"Payments","url":"https://agentwranglers.se/playbook/payments","updated":"2026-10-21","license":"https://creativecommons.org/licenses/by/4.0/","credit":"Required by the licence: credit Agent Wranglers with a link to the rule or playbook you used.","how":"Each rule's How text is on its own page: add .md to its url.","rules":[{"chapter":"Checkout and card data","name":"Choose a checkout that keeps you on SAQ A","level":"Required","risk":"law","applies":["payments"],"rule":"Take card details only through the provider's hosted payment page or its iframe fields, so you qualify for the shortest PCI DSS self-assessment, SAQ A.","check":"Search the frontend for card inputs outside the provider's iframe, such as `autocomplete=\"cc-number\"`, `cc-csc` or inputs named card, cvc or cvv. None exist. Every page that loads the provider's script loads it from the provider's domain. The provider's confirmation, or your script inventory for 6.4.3, is stored in the repository.","url":"https://agentwranglers.se/playbook/payments/checkout-and-card-data/pci-saq-a-integration","related":["/playbook/adversarial-testing/data-and-secrets/hosted-payment-fields","/playbook/payments/checkout-and-card-data/pci-annual-attestation","/playbook/stack/product-services/stripe-payments","/playbook/stack/product-services/no-own-card-handling"],"sources":[{"label":"PCI SSC document library with PCI DSS and SAQ A","url":"https://www.pcisecuritystandards.org/document_library/"}]},{"chapter":"Checkout and card data","name":"Handle 3-D Secure in every card flow","level":"Required","risk":"law","applies":["payments","eu-users"],"rule":"Send every card payment the customer starts through the provider's 3-D Secure step, and handle the result that asks for customer action.","check":"Run checkout on staging with a test card that always asks for authentication, such as Stripe's 4000 0027 6000 3184. The challenge appears, and the order becomes paid only after you approve it. Failing the challenge leaves the order unpaid.","url":"https://agentwranglers.se/playbook/payments/checkout-and-card-data/strong-customer-authentication","related":["/playbook/payments/checkout-and-card-data/payment-test-scenarios"],"sources":[{"label":"PSD2 Directive (EU) 2015/2366, Article 97","url":"https://eur-lex.europa.eu/eli/dir/2015/2366/oj"},{"label":"Regulation (EU) 2018/389 on strong customer authentication","url":"https://eur-lex.europa.eu/eli/reg_del/2018/389/oj"}]},{"chapter":"Checkout and card data","name":"Save cards with consent and charge renewals off-session","level":"Recommended","applies":["payments","eu-users"],"rule":"To charge a card later, save it at the first payment with SCA and the customer's consent. Mark each later charge as merchant-initiated.","check":"On staging, start a subscription with a card that needs authentication unless it was set up, such as Stripe's 4000 0025 0000 3155. The first payment shows a challenge. The renewal a month later, run with the provider's test clock, succeeds without one.","url":"https://agentwranglers.se/playbook/payments/checkout-and-card-data/off-session-payments","related":["/playbook/payments/subscriptions/recurring-consent","/playbook/payments/failed-payments-and-dunning/card-updates"],"sources":[{"label":"Regulation (EU) 2018/389 on strong customer authentication","url":"https://eur-lex.europa.eu/eli/reg_del/2018/389/oj"},{"label":"PSD2 Directive (EU) 2015/2366, Article 97","url":"https://eur-lex.europa.eu/eli/dir/2015/2366/oj"}]},{"chapter":"Checkout and card data","name":"Keep prices in a versioned catalogue","level":"Recommended","applies":["payments"],"rule":"Keep every price in one server-side catalogue with a stable ID, and create a new price instead of changing the amount of one in use.","check":"The app's database role cannot update the amount or currency of a price. Search the client code for amounts sent to the checkout endpoint. None are sent.","url":"https://agentwranglers.se/playbook/payments/checkout-and-card-data/price-catalogue","related":["/playbook/adversarial-testing/abuse-and-limits/server-side-business-rules","/playbook/payments/subscriptions/price-increase-notice","/playbook/payments/tax/tax-inclusive-prices"],"sources":[]},{"chapter":"Checkout and card data","name":"Keep test and live modes apart","level":"Required","risk":"money","applies":["payments"],"rule":"Use the provider's test mode in every environment except production, and make the app refuse to start with a live key anywhere else.","check":"Start staging with a live key. It exits with an error. Search the repository for `sk_live_` and `rk_live_`. Nothing is found.","url":"https://agentwranglers.se/playbook/payments/checkout-and-card-data/test-and-live-keys","related":["/playbook/engineering/running-in-production/secrets-store","/playbook/infrastructure/environments/account-per-environment"],"sources":[]},{"chapter":"Checkout and card data","name":"Test every payment outcome with test cards","level":"Recommended","applies":["payments"],"rule":"Keep one automated test for each payment outcome, using the provider's test cards and test clocks, and run them before every release that touches payments.","check":"The suite has one named test for each outcome on the list. The CI log for the last payment change shows them all passing.","url":"https://agentwranglers.se/playbook/payments/checkout-and-card-data/payment-test-scenarios","related":["/playbook/testing/end-to-end-and-browser-tests/critical-journeys","/playbook/payments/checkout-and-card-data/strong-customer-authentication","/playbook/testing/test-data-and-environments/isolated-test-data"],"sources":[]},{"chapter":"Checkout and card data","name":"Say on the order button that the order costs money","level":"Required","risk":"law","applies":["payments","eu-users"],"rule":"Label the button that places a paid order \"Order with obligation to pay\", or words just as plain, such as \"Pay 129.50 SEK now\". Show the main terms right beside it.","check":"A browser test reaches the final step of every paid flow, including trials that turn paid. The button's accessible name contains \"pay\", \"buy\" or \"order with obligation to pay\" in the page language. The price and contract length appear within the same form.","url":"https://agentwranglers.se/playbook/payments/checkout-and-card-data/order-button-label","related":["/playbook/payments/subscriptions/recurring-consent"],"sources":[{"label":"Consumer Rights Directive 2011/83/EU, Article 8","url":"https://eur-lex.europa.eu/eli/dir/2011/83/oj"}]},{"chapter":"Checkout and card data","name":"Confirm your PCI DSS compliance every year","level":"Recommended","applies":["payments"],"rule":"File the PCI DSS self-assessment questionnaire that fits your checkout once every 12 months, and keep the provider's own attestation of compliance beside it.","check":"The repository or the compliance folder holds a signed questionnaire dated within the last 12 months. It also holds a provider attestation dated within the last 12 months. The compliance calendar has the next due date.","url":"https://agentwranglers.se/playbook/payments/checkout-and-card-data/pci-annual-attestation","related":["/playbook/payments/checkout-and-card-data/pci-saq-a-integration","/playbook/compliance/scope-and-ownership/compliance-calendar"],"sources":[{"label":"PCI SSC document library with PCI DSS and SAQ A","url":"https://www.pcisecuritystandards.org/document_library/"},{"label":"Stripe integration security guide","url":"https://docs.stripe.com/security/guide"}]},{"chapter":"Checkout and card data","name":"Show the lowest earlier price beside a discount","level":"Recommended","applies":["payments","eu-users"],"rule":"When you announce a price cut on goods to EU consumers, show as prior price the lowest price of the 30 days before.","check":"A unit test sets a price of 200, then 150 for 10 days, then 200 again, then a sale at 120. The page shows 150 as the prior price.","url":"https://agentwranglers.se/playbook/payments/checkout-and-card-data/price-reduction-prior-price","related":["/playbook/payments/invoices-and-records/money-audit-log"],"sources":[{"label":"Directive (EU) 2019/2161, Article 2 adding Article 6a","url":"https://eur-lex.europa.eu/eli/dir/2019/2161/oj"}]},{"chapter":"Amounts and orders","name":"Hold money as integer minor units everywhere","level":"Recommended","applies":["payments"],"rule":"Hold every amount as an integer count of the currency's minor unit, with its ISO 4217 code, in code, database, provider calls and stored events.","check":"A unit test adds SEK to EUR and expects an error. Search the payment code for `parseFloat`, `float(` and `Number(` applied to amounts. Nothing is found.","url":"https://agentwranglers.se/playbook/payments/amounts-and-orders/money-in-minor-units","related":["/playbook/api-design/requests-and-responses/money","/playbook/data/schema-design/exact-types","/playbook/architecture/data-shapes/value-objects"],"sources":[]},{"chapter":"Amounts and orders","name":"Split amounts without losing a cent","level":"Recommended","applies":["payments"],"rule":"When you split an amount, such as instalments or a discount across lines, hand out the leftover minor units in a fixed order. The parts must sum exactly to the total.","check":"A property test splits 10,000 random totals by random ratios. The parts always sum to the total, and no part is a whole minor unit or more away from its exact share.","url":"https://agentwranglers.se/playbook/payments/amounts-and-orders/allocate-remainders","related":["/playbook/testing/unit-tests/property-tests","/playbook/api-design/requests-and-responses/money"],"sources":[]},{"chapter":"Amounts and orders","name":"Freeze what the customer agreed to on the order","level":"Recommended","applies":["payments"],"rule":"When the order is created, copy the unit price, quantity, discount, tax rate, tax amount and currency onto each line. Never recompute them from the catalogue.","check":"On staging, change a price and a tax rate, then open an older order and refund it. The order, the invoice and the refund all show the old amounts.","url":"https://agentwranglers.se/playbook/payments/amounts-and-orders/order-snapshot","related":["/playbook/data/schema-design/normalise","/playbook/payments/subscriptions/plan-change-proration"],"sources":[]},{"chapter":"Amounts and orders","name":"Move orders through a fixed set of states","level":"Recommended","applies":["payments"],"rule":"Give orders a fixed list of states and allowed transitions in one place, and refuse any other change with a conditional database update.","check":"A test tries every pair of states. Only the listed transitions succeed. Two identical `paid` events sent at once change the order once.","url":"https://agentwranglers.se/playbook/payments/amounts-and-orders/payment-state-machine","related":["/playbook/architecture/coupling/no-hidden-temporal-coupling","/playbook/architecture/data-shapes/states-as-types","/playbook/data/transactions-and-concurrency/conditional-writes","/playbook/testing/integration-and-contract-tests/outside-service-failures"],"sources":[]},{"chapter":"Amounts and orders","name":"Create each charge with an idempotency key","level":"Required","risk":"money","applies":["payments"],"rule":"Send an idempotency key, built from your own stable IDs, on every provider request that creates a payment, a refund or a payout.","check":"In a test, make the provider call time out after the request was sent, then retry. The provider shows one payment, and the order shows one charge.","url":"https://agentwranglers.se/playbook/payments/amounts-and-orders/idempotent-charge-creation","related":["/playbook/api-design/reliability/idempotency-keys","/playbook/adversarial-testing/abuse-and-limits/race-conditions"],"sources":[{"label":"Expired IETF draft, The Idempotency-Key HTTP Header Field","url":"https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/"}]},{"chapter":"Amounts and orders","name":"Keep one open payment per order","level":"Required","risk":"money","applies":["payments"],"rule":"Allow at most one open payment attempt per order, and reuse it when the customer comes back to checkout.","check":"Open checkout for one order in two tabs and pay in both with a test card. One payment succeeds, and the provider shows no second charge.","url":"https://agentwranglers.se/playbook/payments/amounts-and-orders/one-open-payment-per-order","related":["/playbook/adversarial-testing/abuse-and-limits/race-conditions"],"sources":[]},{"chapter":"Amounts and orders","name":"Adding a fee for paying by consumer card in the EU","level":"Avoid","applies":["payments","eu-users"],"rule":"Never add a charge for paying with an EU consumer card or a SEPA transfer or direct debit. Build the payment cost into the price instead.","url":"https://agentwranglers.se/playbook/payments/amounts-and-orders/no-card-surcharge","related":["/playbook/payments/tax/tax-inclusive-prices"],"sources":[{"label":"PSD2 Directive (EU) 2015/2366, Article 62","url":"https://eur-lex.europa.eu/eli/dir/2015/2366/oj"}]},{"chapter":"Amounts and orders","name":"Collect money for other sellers through a platform product","level":"Required","risk":"law","applies":["payments"],"rule":"When buyers pay you for what other sellers provide, take the payments through a platform product, such as Stripe Connect or Adyen for Platforms. Never hold the sellers' money in your own account.","check":"Search the code for transfers from your own bank account to sellers. None exist. Every payout to a seller goes to a verified connected account at the provider.","url":"https://agentwranglers.se/playbook/payments/amounts-and-orders/marketplace-funds","related":["/playbook/compliance/scope-and-ownership/obligations-register","/playbook/compliance/online-platforms/trader-traceability"],"sources":[{"label":"PSD2 Directive (EU) 2015/2366, Article 3(b) and recital 11","url":"https://eur-lex.europa.eu/eli/dir/2015/2366/oj"},{"label":"Stripe Connect charge types","url":"https://docs.stripe.com/connect/charges"}]},{"chapter":"Payment events and reconciliation","name":"Let provider events move the order","level":"Required","risk":"money","applies":["payments"],"rule":"Fulfil, grant access or mark a refund only when a verified provider event or API read says the money moved. Map each event type to one order transition.","check":"Send a signed test event for a payment whose amount differs from the order. The handler refuses to mark the order paid. Every event type enabled on the webhook endpoint appears in the mapping table or in an explicit ignore list.","url":"https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation/events-drive-fulfilment","related":["/playbook/adversarial-testing/abuse-and-limits/confirm-payment-server-side","/playbook/api-design/webhooks-and-events/signed-webhooks","/playbook/api-design/webhooks-and-events/idempotent-receivers","/playbook/api-design/webhooks-and-events/event-order","/playbook/payments/payment-events-and-reconciliation/delayed-payment-methods","/playbook/payments/subscriptions/provider-billing-engine"],"sources":[]},{"chapter":"Payment events and reconciliation","name":"Wait for payment methods that settle later","level":"Recommended","applies":["payments"],"rule":"Keep an order in a processing state while a slow payment method settles, and fulfil only on the final success event.","check":"Pay with the provider's test bank account that fails after a delay. The order moves from processing to payment failed, nothing ships, and the customer gets a message.","url":"https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation/delayed-payment-methods","related":["/playbook/payments/payment-events-and-reconciliation/events-drive-fulfilment"],"sources":[]},{"chapter":"Payment events and reconciliation","name":"Sweep for missed events every hour","level":"Recommended","applies":["payments"],"rule":"Run a job at least every hour that lists recent payments and refunds at the provider and fixes orders whose state disagrees.","check":"On staging, turn off the webhook endpoint, pay for an order, and turn the endpoint on again. Within an hour the order is paid, and the job reports one fix.","url":"https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation/missed-event-sweep","related":["/playbook/api-design/webhooks-and-events/webhook-retries","/playbook/operations/alerts-and-on-call/job-heartbeats"],"sources":[]},{"chapter":"Payment events and reconciliation","name":"Record every money movement in an append-only ledger","level":"Recommended","applies":["payments"],"rule":"Write each charge, refund, fee, dispute and payout as a new ledger row with the provider's object ID, and never update or delete a row.","check":"As the app role, run an UPDATE and a DELETE on the ledger. Both fail with permission denied. For every order, the sum of its charge and refund rows equals the paid amount minus refunds on the order.","url":"https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation/payment-ledger","related":["/playbook/data/transactions-and-concurrency/transactions","/playbook/payments/payment-events-and-reconciliation/payout-reconciliation","/playbook/payments/refunds-disputes-and-fraud/provider-refunds","/playbook/compliance/audit-logs-and-access/audit-log-events"],"sources":[]},{"chapter":"Payment events and reconciliation","name":"Reconcile every payout","level":"Recommended","applies":["payments"],"rule":"Within 2 business days of each provider payout, match it to the transactions it contains, to your ledger and to the bank statement.","check":"The reconciliation job fails and alerts when a payout differs from the sum of its matched rows by one minor unit or more. It also alerts when any item stays unmatched for more than 7 days.","url":"https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation/payout-reconciliation","related":["/playbook/payments/payment-events-and-reconciliation/payment-ledger"],"sources":[]},{"chapter":"Payment events and reconciliation","name":"Alert when payments start failing","level":"Recommended","applies":["payments"],"rule":"Alert when the payment success rate, failed webhook deliveries or the time since the last successful payment leave their normal range.","check":"On staging, make the webhook endpoint answer 500 for 30 minutes. An alert fires within that time. Then send 20 payments with a declining test card. The success rate alert fires within 15 minutes.","url":"https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation/payment-health-alerts","related":["/playbook/operations/service-levels/burn-rate-alerts","/playbook/operations/alerts-and-on-call/alert-on-symptoms"],"sources":[]},{"chapter":"Payment events and reconciliation","name":"Give each person the smallest role in the provider dashboard","level":"Required","risk":"takeover","applies":["payments"],"rule":"Give every person their own login to the payment provider's dashboard, with two-step login and the smallest role that does their job. Alert on every change to payout bank details or team members.","check":"The dashboard's team list matches the current staff list, and at most 2 people hold an admin role. The account setting requires two-step login. The provider's notification settings send bank account and team changes to the shared inbox.","url":"https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation/provider-dashboard-access","related":["/playbook/compliance/audit-logs-and-access/access-reviews","/playbook/infrastructure/identity-and-access/single-sign-on-for-people"],"sources":[{"label":"Stripe user roles","url":"https://docs.stripe.com/get-started/account/teams/roles"}]},{"chapter":"Payment events and reconciliation","name":"Pin the provider's API version","level":"Recommended","applies":["payments"],"rule":"Set the provider's API version in your code and on each webhook endpoint, and keep both the same. Upgrade it on purpose after the tests pass on the new version.","check":"The code sets the API version in one constant, and the provider's webhook endpoint list shows that same version. A test fails when the SDK reports a different version than the constant.","url":"https://agentwranglers.se/playbook/payments/payment-events-and-reconciliation/provider-api-version","related":["/playbook/engineering/version-control-and-review/automated-dependency-updates","/playbook/stack/product-services/stripe-payments"],"sources":[{"label":"Stripe API versioning","url":"https://docs.stripe.com/api/versioning"}]},{"chapter":"Subscriptions","name":"Let the provider run the billing schedule","level":"Recommended","applies":["payments"],"rule":"Use the payment provider's subscription engine for renewals, proration and retries, and mirror its state in your database from events.","check":"Search the code for scheduled jobs that create charges. None exist. Every write to the subscriptions table comes from the event handler or the sweep job.","url":"https://agentwranglers.se/playbook/payments/subscriptions/provider-billing-engine","related":["/playbook/payments/payment-events-and-reconciliation/events-drive-fulfilment","/playbook/api-design/webhooks-and-events/event-order"],"sources":[]},{"chapter":"Subscriptions","name":"Give access from the subscription status","level":"Recommended","applies":["payments"],"rule":"Decide access in one function from the stored subscription status, period end and first failed payment time, with a fixed grace period for failed renewals.","check":"A table test covers every status on the day before and the day after the period end and the grace end. A past due subscription whose period end lies a month ahead loses access 7 days after its failed payment. Search the code for direct reads of the status field outside that function. None are found.","url":"https://agentwranglers.se/playbook/payments/subscriptions/entitlements-from-status","related":["/playbook/payments/failed-payments-and-dunning/dunning-schedule"],"sources":[]},{"chapter":"Subscriptions","name":"Show the terms and record consent before the first charge","level":"Recommended","applies":["payments"],"rule":"Before a subscription or trial starts, show the price, interval, first charge date and how to cancel beside the pay button. Store proof of consent.","check":"An end-to-end test signs up and finds the price, interval, first charge date and cancel instructions on the page and in the email. The database holds a consent row with the terms version.","url":"https://agentwranglers.se/playbook/payments/subscriptions/recurring-consent","related":["/playbook/payments/checkout-and-card-data/off-session-payments","/playbook/payments/checkout-and-card-data/order-button-label","/playbook/website/accessibility/error-prevention"],"sources":[{"label":"ROSCA, 15 U.S.C. 8403","url":"https://www.govinfo.gov/content/pkg/USCODE-2023-title15/html/USCODE-2023-title15-chap110-sec8403.htm"},{"label":"Consumer Rights Directive 2011/83/EU, Article 8","url":"https://eur-lex.europa.eu/eli/dir/2011/83/oj"}]},{"chapter":"Subscriptions","name":"Remind people before a trial or renewal charges","level":"Recommended","applies":["payments","email"],"rule":"When a trial or introductory price of over 7 days ends, send a reminder 7 days before. Give the amount, date and a cancel link. Send one 15 to 45 days before each yearly renewal too.","check":"Use a test clock to move a 14-day trial to 7 days before its end. One reminder goes out with the amount, date and cancel link. Running the job again sends nothing. A yearly subscription moved to 30 days before renewal gets one reminder.","url":"https://agentwranglers.se/playbook/payments/subscriptions/renewal-reminders","related":["/playbook/payments/subscriptions/online-cancellation"],"sources":[]},{"chapter":"Subscriptions","name":"Preview the amount before a plan change","level":"Recommended","applies":["payments"],"rule":"Before the customer confirms a plan change, show the exact amount due now and at the next renewal, taken from the provider's preview.","check":"Upgrade a test subscription halfway through a month. The charged amount equals the previewed amount to the minor unit.","url":"https://agentwranglers.se/playbook/payments/subscriptions/plan-change-proration","related":["/playbook/payments/amounts-and-orders/order-snapshot"],"sources":[]},{"chapter":"Subscriptions","name":"Tell subscribers before a price rise","level":"Recommended","applies":["payments","email"],"rule":"Email every affected subscriber at least 30 days before a higher price applies. Give the old and new price, the date and a cancel link.","check":"A test runs the price move with one subscriber notified 31 days ago and one notified 5 days ago. Only the first moves. The job refuses any subscription without a notice row.","url":"https://agentwranglers.se/playbook/payments/subscriptions/price-increase-notice","related":["/playbook/payments/checkout-and-card-data/price-catalogue"],"sources":[]},{"chapter":"Subscriptions","name":"Let people cancel online as easily as they signed up","level":"Recommended","applies":["payments","email"],"rule":"Offer cancellation online, with no more steps than signing up took, and confirm it at once by email with the date access ends.","check":"A browser test signs in and cancels in no more clicks than the signup took. A confirmation email with the end date arrives, and the subscription is set to end at the period end.","url":"https://agentwranglers.se/playbook/payments/subscriptions/online-cancellation","related":["/playbook/payments/subscriptions/renewal-reminders","/playbook/accounts/profile-and-account-lifecycle/account-deletion"],"sources":[{"label":"ROSCA, 15 U.S.C. 8403","url":"https://www.govinfo.gov/content/pkg/USCODE-2023-title15/html/USCODE-2023-title15-chap110-sec8403.htm"},{"label":"Digital Markets, Competition and Consumers Act 2024","url":"https://www.legislation.gov.uk/ukpga/2024/13/contents"}]},{"chapter":"Subscriptions","name":"Add the EU withdrawal button","level":"Required","risk":"law","applies":["payments","eu-users"],"rule":"For online contracts with EU consumers, offer a withdrawal function labelled \"withdraw from contract here\" or similar, for the whole withdrawal period. Add a confirm step and an emailed receipt.","check":"A browser test, as an EU consumer within 14 days of buying, finds the button, confirms, and receives an email with the date and time. After 14 days the button no longer offers that contract.","url":"https://agentwranglers.se/playbook/payments/subscriptions/eu-withdrawal-button","related":["/playbook/payments/refunds-disputes-and-fraud/withdrawal-refund-deadline"],"sources":[{"label":"Directive (EU) 2023/2673, Article 11a withdrawal function","url":"https://eur-lex.europa.eu/eli/dir/2023/2673/oj"}]},{"chapter":"Failed payments and dunning","name":"Retry only declines that can succeed","level":"Recommended","applies":["payments"],"rule":"Sort every decline into retry or do not retry by its decline code. Never retry a card the bank reports as lost, stolen, closed or not to be tried again.","check":"A unit test feeds every decline code from the provider's list and checks its category. A renewal declined as stolen schedules no retry.","url":"https://agentwranglers.se/playbook/payments/failed-payments-and-dunning/decline-categories","related":["/playbook/payments/failed-payments-and-dunning/dunning-schedule"],"sources":[]},{"chapter":"Failed payments and dunning","name":"Retry failed renewals on a fixed schedule","level":"Recommended","applies":["payments"],"rule":"Retry a soft-declined renewal a fixed number of times over a fixed window, such as 4 times in 14 days. Then move the subscription to a planned end state.","check":"With a test clock and a card that declines for insufficient funds, the provider shows exactly the planned retries. Then the subscription ends in the planned state.","url":"https://agentwranglers.se/playbook/payments/failed-payments-and-dunning/dunning-schedule","related":["/playbook/payments/subscriptions/entitlements-from-status","/playbook/payments/failed-payments-and-dunning/decline-categories","/playbook/payments/failed-payments-and-dunning/dunning-messages"],"sources":[]},{"chapter":"Failed payments and dunning","name":"Ask the customer to fix the payment","level":"Recommended","applies":["payments","email"],"rule":"After each failed renewal, email the customer a link to a secure page to update the card, and show a notice in the app.","check":"Fail a test renewal. An email with an update link arrives within an hour, and the in-app notice shows. Both stop after a successful retry.","url":"https://agentwranglers.se/playbook/payments/failed-payments-and-dunning/dunning-messages","related":["/playbook/payments/failed-payments-and-dunning/dunning-schedule","/playbook/email/email-security/phishing-resistance"],"sources":[]},{"chapter":"Failed payments and dunning","name":"Keep saved cards up to date","level":"Recommended","applies":["payments"],"rule":"Turn on network tokens and the card account updater at your provider, so renewals keep working after a bank reissues a card.","check":"The provider's settings show the account updater and network tokens switched on. A monthly job lists cards that expire next month with no update, and each of those customers got an email.","url":"https://agentwranglers.se/playbook/payments/failed-payments-and-dunning/card-updates","related":["/playbook/payments/checkout-and-card-data/off-session-payments"],"sources":[]},{"chapter":"Refunds, disputes and fraud","name":"Refund through the provider with a key and a reason","level":"Recommended","applies":["payments"],"rule":"Issue every refund through the provider's API with an idempotency key, a reason and the person who asked. Track it until it succeeds or fails.","check":"Send the same refund request twice at once. The provider shows one refund. A refund larger than the remaining amount is rejected before any provider call.","url":"https://agentwranglers.se/playbook/payments/refunds-disputes-and-fraud/provider-refunds","related":["/playbook/payments/payment-events-and-reconciliation/payment-ledger","/playbook/api-design/reliability/idempotency-keys","/playbook/payments/refunds-disputes-and-fraud/withdrawal-refund-deadline"],"sources":[]},{"chapter":"Refunds, disputes and fraud","name":"Refund EU withdrawals within 14 days","level":"Required","risk":"law","applies":["payments","eu-users"],"rule":"When an EU consumer withdraws from a contract, refund every payment, including standard delivery, within 14 days of being told, on the same payment method.","check":"A daily query lists withdrawals older than 10 days without a refund. It returns no rows.","url":"https://agentwranglers.se/playbook/payments/refunds-disputes-and-fraud/withdrawal-refund-deadline","related":["/playbook/payments/subscriptions/eu-withdrawal-button","/playbook/payments/refunds-disputes-and-fraud/provider-refunds"],"sources":[{"label":"Consumer Rights Directive 2011/83/EU, Article 13","url":"https://eur-lex.europa.eu/eli/dir/2011/83/oj"}]},{"chapter":"Refunds, disputes and fraud","name":"Answer every dispute before its deadline","level":"Recommended","applies":["payments"],"rule":"Answer every dispute before the provider's deadline with evidence that fits its reason code, or accept it on purpose.","check":"A query lists open disputes due within 3 days without submitted evidence. It returns no rows. Each dispute event creates a task within an hour.","url":"https://agentwranglers.se/playbook/payments/refunds-disputes-and-fraud/dispute-evidence","related":["/playbook/website/privacy/data-minimization"],"sources":[]},{"chapter":"Refunds, disputes and fraud","name":"Refund on a dispute alert before it becomes a chargeback","level":"Recommended","applies":["payments"],"rule":"Subscribe to the card networks' pre-dispute alerts through your provider, and refund or settle each alert automatically within its deadline.","check":"Send a test alert from the provider's sandbox for a paid order. The order is refunded once, a ledger row appears, and no chargeback follows.","url":"https://agentwranglers.se/playbook/payments/refunds-disputes-and-fraud/dispute-alerts","related":["/playbook/payments/refunds-disputes-and-fraud/dispute-ratio"],"sources":[]},{"chapter":"Refunds, disputes and fraud","name":"Keep your dispute ratio well below the network limits","level":"Recommended","applies":["payments"],"rule":"Track fraud reports and disputes as a share of card payments every month, per card network, and alert at half of the network's monitoring limit.","check":"A monthly job computes the ratio for each network and alerts above 0.75%. Its last run appears in the job log.","url":"https://agentwranglers.se/playbook/payments/refunds-disputes-and-fraud/dispute-ratio","related":["/playbook/payments/refunds-disputes-and-fraud/dispute-alerts","/playbook/payments/refunds-disputes-and-fraud/fraud-screening","/playbook/payments/refunds-disputes-and-fraud/statement-descriptor"],"sources":[]},{"chapter":"Refunds, disputes and fraud","name":"Stop card testing on your checkout","level":"Recommended","applies":["payments"],"rule":"Limit payment attempts per account, IP address and card, and ask for a challenge after repeated declines. Then attackers cannot test stolen cards on your checkout.","check":"A script sends 30 payments with a declining test card from one IP address within a minute. After the limit, your server answers 429, and the provider receives no further attempts.","url":"https://agentwranglers.se/playbook/payments/refunds-disputes-and-fraud/card-testing","related":["/playbook/adversarial-testing/abuse-and-limits/limit-requests","/playbook/adversarial-testing/abuse-and-limits/free-tier-abuse","/playbook/payments/refunds-disputes-and-fraud/fraud-screening"],"sources":[{"label":"OWASP OAT-001, Carding","url":"https://owasp.github.io/www-project-automated-threats-to-web-applications/assets/oats/EN/OAT-001_Carding"}]},{"chapter":"Refunds, disputes and fraud","name":"Screen risky payments before fulfilment","level":"Recommended","applies":["payments"],"rule":"Use the provider's risk score on every payment. Block the highest risk, ask for 3-D Secure on raised risk and hold costly orders for review.","check":"A test payment with the provider's highest-risk test card is blocked. One with a raised-risk test card triggers 3-D Secure.","url":"https://agentwranglers.se/playbook/payments/refunds-disputes-and-fraud/fraud-screening","related":["/playbook/payments/refunds-disputes-and-fraud/dispute-ratio","/playbook/payments/refunds-disputes-and-fraud/card-testing"],"sources":[]},{"chapter":"Refunds, disputes and fraud","name":"Use a statement name customers recognise","level":"Recommended","applies":["payments"],"rule":"Set the card statement text to the brand name customers know, and add a short hint such as the plan or order.","check":"The provider's settings hold a statement text that contains the brand name. A test charge shows it in the payment details.","url":"https://agentwranglers.se/playbook/payments/refunds-disputes-and-fraud/statement-descriptor","related":["/playbook/payments/refunds-disputes-and-fraud/dispute-ratio"],"sources":[]},{"chapter":"Tax","name":"Calculate tax with a tax service","level":"Recommended","applies":["payments"],"rule":"Calculate VAT and sales tax with a tax service, or sell through a merchant of record, instead of rates written in code.","check":"Search the code for tax rates near the word tax or vat, such as `0.25`, `25`, `2500` or `1.25`. None remain. Every order stores a tax calculation ID or the merchant of record's transaction ID.","url":"https://agentwranglers.se/playbook/payments/tax/tax-service","related":["/playbook/payments/tax/customer-location-evidence"],"sources":[]},{"chapter":"Tax","name":"Keep evidence of where the customer is","level":"Recommended","applies":["payments"],"rule":"For digital services sold to consumers, store two pieces of evidence of the customer's country that agree, with each order.","check":"A query over last month's consumer orders finds none with fewer than two matching evidence items.","url":"https://agentwranglers.se/playbook/payments/tax/customer-location-evidence","related":["/playbook/payments/tax/tax-service","/playbook/website/privacy/data-minimization","/playbook/payments/tax/eu-vat-oss"],"sources":[{"label":"Implementing Regulation (EU) No 282/2011 on VAT","url":"https://eur-lex.europa.eu/eli/reg_impl/2011/282/oj"}]},{"chapter":"Tax","name":"Charge EU VAT by the customer's country","level":"Required","risk":"law","applies":["payments","eu-users"],"rule":"For digital services sold to EU consumers, charge the VAT rate of the consumer's country and report it through the One Stop Shop.","check":"Checkout tests for consumers in three EU countries each show that country's standard rate. The OSS return total for the quarter equals the VAT in the ledger.","url":"https://agentwranglers.se/playbook/payments/tax/eu-vat-oss","related":["/playbook/payments/tax/customer-location-evidence"],"sources":[{"label":"VAT Directive 2006/112/EC, Article 58","url":"https://eur-lex.europa.eu/eli/dir/2006/112/oj"},{"label":"European Commission VAT One Stop Shop","url":"https://vat-one-stop-shop.ec.europa.eu/index_en"}]},{"chapter":"Tax","name":"Reverse-charge B2B sales with a checked VAT number","level":"Recommended","applies":["payments","eu-users"],"rule":"For services sold to a business in another EU country, check the buyer's VAT number in VIES. Charge no VAT and print \"Reverse charge\" on the invoice.","check":"A checkout test with an invalid VAT number charges VAT. A valid number from another EU country gives zero VAT, a stored consultation number and the reverse charge note on the invoice.","url":"https://agentwranglers.se/playbook/payments/tax/reverse-charge-vies","related":["/playbook/payments/invoices-and-records/invoice-required-fields"],"sources":[{"label":"VAT Directive 2006/112/EC, Articles 196 and 226","url":"https://eur-lex.europa.eu/eli/dir/2006/112/oj"},{"label":"European Commission VIES VAT number check","url":"https://ec.europa.eu/taxation_customs/vies/"}]},{"chapter":"Tax","name":"Track US sales tax nexus by state","level":"Recommended","applies":["payments"],"rule":"Track sales and transaction counts per US state every month, and register to collect tax in a state before you pass its economic nexus threshold.","check":"A monthly job compares each state's sales and transaction count over the state's measuring period with its thresholds. It alerts at 80% of either one.","url":"https://agentwranglers.se/playbook/payments/tax/us-sales-tax-nexus","related":["/playbook/compliance/scope-and-ownership/obligations-register"],"sources":[]},{"chapter":"Tax","name":"Show prices the way each market expects","level":"Recommended","applies":["payments"],"rule":"Show consumer prices in the EU and UK with VAT included and US prices before sales tax, and store the tax behaviour with each price.","check":"A browser test for an EU consumer shows the same total on the product page and the payment page.","url":"https://agentwranglers.se/playbook/payments/tax/tax-inclusive-prices","related":["/playbook/payments/amounts-and-orders/no-card-surcharge","/playbook/payments/checkout-and-card-data/price-catalogue"],"sources":[{"label":"Price Indication Directive 98/6/EC","url":"https://eur-lex.europa.eu/eli/dir/1998/6/oj"},{"label":"UK Price Marking Order 2004","url":"https://www.legislation.gov.uk/uksi/2004/102/contents"}]},{"chapter":"Invoices and records","name":"Number invoices in one unbroken sequence","level":"Required","risk":"law","applies":["payments"],"rule":"Give each invoice and credit note a unique number from a sequence with no gaps and no reuse. Take it in the transaction that issues the invoice.","check":"A query over each series finds no gaps and no duplicates. A test that issues 50 invoices at the same moment gets 50 different numbers in a row.","url":"https://agentwranglers.se/playbook/payments/invoices-and-records/sequential-invoice-numbers","related":["/playbook/data/transactions-and-concurrency/transactions","/playbook/payments/invoices-and-records/credit-notes"],"sources":[{"label":"VAT Directive 2006/112/EC, Article 226","url":"https://eur-lex.europa.eu/eli/dir/2006/112/oj"}]},{"chapter":"Invoices and records","name":"Put every required field on the invoice","level":"Required","risk":"law","applies":["payments"],"rule":"Build invoices from one template that holds every field the law requires where you are registered, and test that each field is present.","check":"A unit test renders one invoice for each case, which are domestic, EU business, EU consumer and outside the EU. It asserts that every required field is present and not empty.","url":"https://agentwranglers.se/playbook/payments/invoices-and-records/invoice-required-fields","related":["/playbook/email/automation-and-agents/readable-receipts","/playbook/payments/tax/reverse-charge-vies"],"sources":[{"label":"VAT Directive 2006/112/EC, Article 226","url":"https://eur-lex.europa.eu/eli/dir/2006/112/oj"}]},{"chapter":"Invoices and records","name":"Correct invoices with credit notes","level":"Recommended","applies":["payments"],"rule":"Never change or delete an issued invoice. Correct it with a credit note that refers to the original, then issue a new invoice when needed.","check":"The app's database role cannot UPDATE or DELETE issued invoices. Every refund row in the ledger for an invoiced order has a credit note that refers to an invoice.","url":"https://agentwranglers.se/playbook/payments/invoices-and-records/credit-notes","related":["/playbook/payments/invoices-and-records/sequential-invoice-numbers"],"sources":[{"label":"VAT Directive 2006/112/EC, Article 219","url":"https://eur-lex.europa.eu/eli/dir/2006/112/oj"}]},{"chapter":"Invoices and records","name":"Send e-invoices where the law requires them","level":"Recommended","applies":["payments"],"rule":"Keep a list of the countries where you must send structured e-invoices, and send them in the required format before each rule starts.","check":"A data file lists each country you invoice in, with its rule date and format. A test sends a sample invoice through the service's validator for each country with a rule in force.","url":"https://agentwranglers.se/playbook/payments/invoices-and-records/e-invoicing","related":["/playbook/compliance/scope-and-ownership/compliance-calendar","/playbook/stack/product-services/html-to-pdf"],"sources":[{"label":"Directive (EU) 2025/516 on VAT in the digital age","url":"https://eur-lex.europa.eu/eli/dir/2025/516/oj"},{"label":"Directive 2014/55/EU on electronic invoicing in public procurement","url":"https://eur-lex.europa.eu/eli/dir/2014/55/oj"}]},{"chapter":"Invoices and records","name":"Keep payment records for the legal period","level":"Required","risk":"law","applies":["payments"],"rule":"Keep invoices, credit notes and ledger rows for the longest bookkeeping period of the countries you report in, and keep them out of account deletion.","check":"Delete a test account with paid orders. The invoices and ledger rows remain, and the profile data is gone. The retention job removes records older than the period.","url":"https://agentwranglers.se/playbook/payments/invoices-and-records/payment-record-retention","related":["/playbook/data/personal-data/retention-jobs"],"sources":[{"label":"GDPR Article 17, Right to erasure","url":"https://eur-lex.europa.eu/eli/reg/2016/679/oj"}]},{"chapter":"Invoices and records","name":"Log every staff action on money","level":"Recommended","applies":["payments"],"rule":"Log every manual refund, credit, price change, discount and subscription change with who did it, when, why and the values before and after.","check":"Issue a refund from the admin screen. A log row with the staff ID, reason, amount, time and the values before and after appears. The app role cannot UPDATE or DELETE it.","url":"https://agentwranglers.se/playbook/payments/invoices-and-records/money-audit-log","related":["/playbook/payments/checkout-and-card-data/price-reduction-prior-price","/playbook/adversarial-testing/supply-chain-and-setup/security-logging","/playbook/data/personal-data/access-logging","/playbook/compliance/audit-logs-and-access/audit-log-events"],"sources":[]}]}