Fluent Collective / courses
0-day streak 0 XP Explorer rank
← Get Paid

Module 5: Operations and Protection

Taking payments is the fun part. Keeping the money — handling disputes, issuing refunds without wrecking your margins, reconciling payouts, charging tax correctly, and staying on the right side of PCI compliance — is what separates a real business from a hobby with a checkout button. This module is the operations manual.

Disputes and chargebacks: fighting and winning

A dispute (chargeback) is the customer asking their bank to reverse a charge. The bank pulls the money immediately; you get 7–21 days to respond depending on the card network, and win rates for well-evidenced digital-goods cases run 40–60%. Stripe charges a $15 dispute fee per case, non-refundable even if you win — prevention is cheaper than representment.

The response process is representment: you submit evidence through the Dashboard → Payments → Disputes → select → Submit evidence, or via the API by updating the dispute with an evidence object. Under compelling evidence 3.0 (Visa's current framework), the strongest evidence categories are: proof the customer used the product (login logs, usage timestamps, IP/device matching prior purchases), delivery confirmation (download logs, access grants, shipping tracking), prior undisputed transactions on the same card, and a signed agreement to your terms. What actually wins cases: contracts with the customer's signature, dated delivery emails, support threads where the customer acknowledges receipt, and usage logs showing real engagement. What loses: "they agreed to our terms" with no signature, and screenshots of your refund policy with no proof of delivery.

Timelines are unforgiving — miss the response deadline and you lose by default, so set up the charge.dispute.created webhook and treat it as a pager alert. Prevention beats fighting: use a clear statement descriptor (Settings → Payments → statement descriptor — the text on the customer's card statement; cryptic LLC names are the top cause of "I don't recognize this charge"), send receipts for every payment, and make your refund policy visible pre-purchase so "never received" and "didn't recognize" disputes never start.

A customer disputes a $500 charge claiming they never received the service. You have a signed contract and delivery emails with timestamps. What is your move?

Refunds: the math nobody warns you about

The Refunds API is one call, full or partial, with a reason code for your records:

# Full refund
curl https://api.stripe.com/v1/refunds \
  -u sk_test_YOUR_KEY: \
  -d charge="ch_ABC123" \
  -d reason="requested_by_customer"
// Partial refund — amount in cents
const refund = await stripe.refunds.create({
  charge: 'ch_ABC123',
  amount: 2500,              // $25.00 of a larger charge
  reason: 'requested_by_customer',
  metadata: { ticket: 'SUP-881' },
});

Refunds land in the customer's account in 5–10 business days (bank-dependent; Stripe releases the money immediately, the customer's bank is the slow part — set this expectation in your refund email or you will get "where's my money" tickets). Valid reason values: duplicate, fraudulent, requested_by_customer. Marking fraud refunds as fraudulent feeds Stripe's risk models and helps your Radar standing.

Now the math. When you refund, Stripe does not return the original processing fee. On a $100 sale at 2.9% + 30¢, you paid $3.20 in fees and netted $96.80. A full refund sends the customer $100 — you are out the full $3.20, not just the product. Run the numbers at scale: a business doing $50,000/month with a 4% refund rate eats $2,000 in refunded revenue plus roughly $64 in unreturned fees every month. That is why refund policy is a pricing decision: partial refunds, store credit, and clear no-refund windows on digital goods exist to keep this number sane.

Operational rules: refund to the original payment method only — never "refund" to a different card or by bank transfer, which is a classic fraud vector; issue refunds within the card networks' 120-day window where possible; and log every refund's reason in metadata so you can spot patterns (a spike in requested_by_customer on one product is a product problem, not a payments problem). Dashboard path: Payments → select payment → Refund → full or partial amount.

You refund a $200 charge in full. The original processing fee was 2.9% + 30¢. How much does the refund cost you in total?

Payout reconciliation, Stripe Tax, and the monthly close

Stripe batches your charges into payouts on your schedule (Dashboard → Settings → Payouts — daily automatic is the default; manual payouts let you hold a reserve). Each payout object lists its arrival_date, amount, and the balance_transactions it contains. A balance transaction is the atomic money record: type charge, refund, adjustment, or stripe_fee, each with amount, fee, and net. Reconciliation means proving payout = sum of nets.

The spreadsheet method that works: export Dashboard → Payments → Payouts → select payout → Export, which gives you every balance transaction. Sum the net column — it must equal the payout's amount. Then pivot by type: total stripe_fee is your processing cost for the period, total refund is your refund volume. Do this monthly and you will catch the two classic leaks: refunds issued but never recorded in your books, and fees drifting upward as your mix shifts toward international cards (which carry higher rates).

Stripe Tax automates sales tax/VAT/GST: Settings → Tax → enable, set your business address and tax registrations, and Stripe calculates the right rate per customer location at checkout. The key setting is tax_behavior on your Prices: exclusive adds tax on top of your price ($100 + 8% = $108 charged); inclusive treats your price as tax-included ($100 includes the tax). Pick one per market and be consistent — mixing them confuses customers and accountants equally. Assign product tax codes (Dashboard → Products → select product → Tax code) so digital goods, SaaS, and physical products are classified correctly; the wrong code is the most common Tax misconfiguration. Watch the threshold monitoring tab — Stripe warns you when you approach economic-nexus thresholds in new states or countries, which is your cue to register before you owe back taxes.

Your monthly close checklist:

Your exported payout shows $9,850 arrived, but the summed net column of its balance transactions is $9,900. What is the most likely explanation?

Security: PCI scope, keys, webhooks, and Radar

PCI DSS is the card industry's security standard, and its scope depends entirely on whether card data touches your servers. If you use Checkout, Payment Links, or Elements, card numbers go directly from the customer's browser to Stripe — your server never sees the PAN (primary account number). That puts you under SAQ A, the shortest self-assessment questionnaire: roughly two dozen questions, no network scans, no penetration test.

The moment raw card data touches your server — a form that posts card numbers to your backend, logging request bodies that include PANs — your compliance burden explodes to SAQ A-EP or SAQ D: hundreds of requirements, quarterly scans, annual audits, and breach liability that can end a small company. Rule: never let card data touch your infrastructure. Elements and Checkout exist precisely so you do not have to. Audit your logs too — a debug log that captures a full request body can silently pull you into scope.

Keys and webhooks: use restricted API keys (Dashboard → Developers → API keys → Create restricted key) scoped to exactly the permissions each service needs. Rotate keys on a schedule and immediately after any team change; Stripe shows each key's last-used date so you can find dead ones. For webhooks, rotate the signing secret periodically (Developers → Webhooks → select endpoint → Signing secret → roll the secret) and always verify signatures in code — an unverified webhook endpoint is a free "mark order as paid" button for attackers:

// Verify every webhook before trusting it
const event = stripe.webhooks.constructEvent(
  rawBody,                                    // raw body, not parsed JSON
  req.headers['stripe-signature'],
  process.env.STRIPE_WEBHOOK_SECRET
);

Radar is Stripe's fraud layer: every payment gets a risk score from 0–100 based on network-wide signals. Start with Radar's default rules (they block the obvious fraud with minimal false positives), then add one custom rule at a time and review the blocked-payments log weekly. The classic mistake is stacking aggressive rules on day one and declining legitimate customers; fraud prevention that costs you real sales is a net loss.

Your developer proposes posting raw card numbers to your own server "just to validate them" before sending them to Stripe. What is the correct response?