Stripe Refunds and Disputes — Handling Post-Payment Issues
In this tutorial, you will learn about Stripe Refunds and Disputes. We cover key concepts, practical examples, and best practices to help you master this topic.
Stripe refunds return funds to customers for full or partial amounts, while disputes (chargebacks) occur when customers challenge a charge with their bank requiring evidence submission to resolve.
What You'll Learn
By the end of this lesson you will understand how to issue refunds, handle disputes, submit evidence, prevent chargebacks, and manage the dispute lifecycle.
Why It Matters
Refunds and disputes are inevitable in any payment system. Handling them efficiently maintains customer trust and minimizes financial losses. Proper dispute response can save significant revenue.
Real-World Use
DodaZIP's support team can issue refunds through the admin dashboard. For disputes, an automated system collects delivery evidence and submits it to Stripe within the required timeframe.
flowchart TD
R[Refund Request] -->|Full| F[Full Refund]
R -->|Partial| P[Partial Refund]
D[Dispute Filed] --> E[Evidence Collection]
E --> S[Submit to Stripe]
S -->|Win| W[Funds Returned]
S -->|Lose| L[Funds Deducted]
L --> PR[Prevent Future Disputes]
style R fill:#6772e5,color:#fff
Issuing Refunds
Refunds can be issued for the full amount or a partial amount within the refund window.
import stripe
import os
stripe.api_key = os.environ.get("STRIPE_SECRET_KEY", "sk_test_placeholder")
def create_refund(charge_id, amount_cents=None, reason="requested_by_customer"):
refund = stripe.Refund.create(
charge=charge_id,
amount=amount_cents, # None for full refund
reason=reason,
)
return refund
stripe.api_key = "sk_test_placeholder"
class MockRefund:
id = "ref_test_abc"
status = "succeeded"
amount = 1999
stripe.Refund.create = lambda **kwargs: MockRefund()
full_refund = create_refund("ch_test_abc")
print(f"Full refund: {full_refund.id} - ${full_refund.amount/100:.2f} - {full_refund.status}")
partial_refund = create_refund("ch_test_abc", amount_cents=500)
print(f"Partial refund: ${partial_refund.amount/100:.2f} - {partial_refund.status}")
Expected output:
Full refund: ref_test_abc - $19.99 - succeeded
Partial refund: $5.00 - succeeded
Handling Disputes
When a customer disputes a charge, Stripe notifies you via Webhook. You have a limited time to respond with evidence.
# disputes.py
# Handling Stripe disputes
def handle_dispute_webhook(dispute):
dispute_id = dispute.get("id")
amount = dispute.get("amount", 0)
reason = dispute.get("reason", "unknown")
status = dispute.get("status", "needs_response")
print(f"Dispute received: {dispute_id}")
print(f"Amount: ${amount/100:.2f}")
print(f"Reason: {reason}")
print(f"Status: {status}")
print(f"Response due: Within 7 days")
if status == "needs_response":
evidence = {
"customer_name": "Alice Smith",
"customer_email": "alice@example.com",
"billing_address": "123 Main St, City, State 12345",
"shipping_address": "123 Main St, City, State 12345",
"shipping_tracking_number": "1Z999AA10123456784",
"shipping_date": "2026-06-25",
"service_date": "2026-06-20",
"service_documentation": "Subscription access logs showing daily usage",
}
print("\nSubmitting evidence:")
for key, value in evidence.items():
print(f" {key}: {value}")
dispute = {"id": "dp_test_abc", "amount": 2999, "reason": "product_not_received", "status": "needs_response"}
handle_dispute_webhook(dispute)
Preventing Disputes
Use Stripe Radar with custom rules to block fraudulent transactions before they happen.
# prevention.py
# Preventing disputes with Radar
def configure_radar_rules():
rules = [
("Block", "card_country != ip_country AND amount > 10000"),
("Block", "card_funding == 'prepaid' AND amount > 5000"),
("Review", "email_domain_velocity > 3 IN 1 hour"),
("Block", "card_velocity > 5 IN 15 minutes"),
("Review", "amount > 50000"),
]
print("Stripe Radar Rules:")
for action, rule in rules:
print(f" {action:8s} | {rule}")
configure_radar_rules()
Common Mistakes
Not responding to disputes in time: You have 7 days to respond. Missing the deadline automatically loses the dispute.
Submitting insufficient evidence: Vague evidence loses disputes. Provide concrete proof like delivery tracking or service logs.
Issuing refunds outside Stripe: Refunding via bank transfer without updating Stripe leaves the charge as completed. Always refund through Stripe.
Not having clear refund policies: Clearly communicate refund terms before purchase to reduce dispute risk.
Ignoring dispute patterns: Multiple disputes from similar patterns indicate a systemic issue. Investigate and fix root causes.
Practice Questions
What is the difference between a refund and a dispute? A refund is initiated by you. A dispute is initiated by the customer through their bank.
How long do you have to respond to a dispute? 7 days from the dispute notification. Missing the deadline automatically loses.
What evidence should you submit for a product_not_received dispute? Shipping tracking number, delivery confirmation, and customer communication.
How does Stripe Radar help prevent disputes? Radar uses Machine Learning and custom rules to block fraudulent transactions before they happen.
Challenge: Create a dispute management system that collects evidence automatically, submits it to Stripe via API, and tracks the outcome.
FAQ
Mini Project
Create a refund and dispute handler that processes refund requests, handles dispute Webhooks, and submits evidence automatically.
import json
def lambda_handler(event, context):
body = json.loads(event.get("body", "{}"))
action = body.get("action", "refund")
charge_id = body.get("charge_id", "ch_test")
amount = body.get("amount")
if action == "refund":
refund_amount = f"${amount/100:.2f}" if amount else "Full"
print(f"Processing {refund_amount} refund for {charge_id}")
return {"statusCode": 200, "body": json.dumps({"status": "refund_succeeded", "refund_id": "ref_test"})}
if action == "dispute_response":
dispute_id = body.get("dispute_id")
print(f"Submitting evidence for dispute {dispute_id}")
return {"statusCode": 200, "body": json.dumps({"status": "evidence_submitted"})}
return {"statusCode": 400, "body": json.dumps({"error": "Invalid action"})}
print(lambda_handler({"body": json.dumps({"action": "refund", "charge_id": "ch_abc", "amount": 1999})}, None)["body"])
What's Next
Next: Invoices for billing documentation.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro