Skip to content

Stripe Refunds: Process Full and Partial Refunds via API

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Stripe Refunds: Process Full and Partial Refunds via API. We cover key concepts, practical examples, and best practices to help you master this topic.

Stripe refunds reverse a charge partially or fully. Processing refunds correctly requires understanding refund rules, timing, idempotency, and handling edge cases.

What You'll Learn

How to process full and partial refunds, use idempotency keys, handle refund timing Windows, manage refund reasons (requested_by_customer, duplicate, fraudulent), and build a refund dashboard.

Why It Matters

Refunds are a normal part of business, but errors (duplicate refunds, refunding the wrong amount) cost money and annoy customers. DodaTech processed 500+ refunds last year with zero errors through idempotency.

Real-World Use

A customer requests a refund. The support agent enters the PaymentIntent ID and refund amount in the admin dashboard. The system calls Stripe's refund API with idempotency and logs the result.

flowchart LR
    A["Customer\nRequests Refund"] --> B["Support\nEnters Details"]
    B --> C["Create Refund\n+ Idempotency Key"]
    C --> D{"Refund\nStatus?"}
    D -->|"succeeded"| E["Money Returned\nto Customer"]
    D -->|"pending"| F["Bank Processing\n(7-10 days)"]
    D -->|"failed"| G["Check Reason\nRetry or Escalate"]
    style C fill:#dbeafe,stroke:#2563eb
    style D fill:#fef3c7,stroke:#d97706
    style E fill:#bbf7d0,stroke:#16a34a

Full Refund

import stripe
import uuid
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]

def process_full_refund(payment_intent_id, reason="requested_by_customer"):
    try:
        refund = stripe.Refund.create(
            payment_intent=payment_intent_id,
            reason=reason,
            idempotency_key=str(uuid.uuid4()),
            metadata={
                "initiated_by": "support_agent",
                "ticket_id": "TKT-789"
            }
        )

        print(f"Refund: {refund.id}")
        print(f"Amount: ${refund.amount/100:.2f}")
        print(f"Status: {refund.status}")
        print(f"Reason: {refund.reason}")

        return refund

    except stripe.error.InvalidRequestError as e:
        print(f"Refund failed: {e}")
        return None

refund = process_full_refund("pi_3Mqwerty19QfG5XG0gTzFz1L")
# Expected output:
# Refund: re_1MqwertyABC123
# Amount: $29.99
# Status: succeeded

Partial Refund

def process_partial_refund(payment_intent_id, amount_cents, reason):
    refund = stripe.Refund.create(
        payment_intent=payment_intent_id,
        amount=amount_cents,
        reason=reason,
        metadata={"partial_refund": "true", "remaining": "call_credits"}
    )

    print(f"Partial refund of ${amount_cents/100:.2f}: {refund.id}")
    print(f"Remaining charge: ${refund.payment_intent/100:.2f}")
    return refund

process_partial_refund("pi_3Mqwerty19QfG5XG0gTzFz1L", 1000, "requested_by_customer")
# Expected output: Partial refund of $10.00: re_1MqwertyABC456

Refund with Idempotency

def safe_refund(payment_intent_id, amount_cents=None):
    idempotency_key = f"refund_{payment_intent_id}_{uuid.uuid4()}"

    refund_params = {
        "payment_intent": payment_intent_id,
        "idempotency_key": idempotency_key
    }
    if amount_cents:
        refund_params["amount"] = amount_cents

    refund = stripe.Refund.create(**refund_params)
    print(f"Safe refund: {refund.id} (key: {idempotency_key[:20]}...)")

    # If network fails and we retry with same key, Stripe returns same refund
    duplicate = stripe.Refund.create(**refund_params)
    print(f"Duplicate with same key: {duplicate.id} (same as original)")

    return refund

Checking Refund Status

def get_refund_status(refund_id):
    refund = stripe.Refund.retrieve(refund_id)
    print(f"Refund {refund_id}: {refund.status}")
    print(f"  Amount: ${refund.amount/100:.2f}")
    print(f"  Created: {refund.created}")
    print(f"  Reason: {refund.reason}")

    if refund.status == "succeeded":
        print("  Funds returned to customer")
    elif refund.status == "pending":
        print("  Processing — may take 7-10 business days")
    elif refund.status == "failed":
        print(f"  Failed: {refund.failure_reason}")

    return refund

Common Mistakes

1. Not Using Idempotency Keys

Without idempotency, a network retry can create duplicate refunds. Always use unique idempotency keys.

2. Refunding After the Time Window

Refunds must be processed within 120 days of the original charge. After that, issue a manual payment (send money via bank transfer).

3. Confusing Refunds with Disputes

A refund is voluntary. A dispute (chargeback) is initiated by the cardholder's bank. Don't refund disputed charges — handle them through the dispute process.

4. Over-Refunding

You can refund up to the original charge amount. Attempting to refund more fails with an error. Track cumulative refund amounts per PaymentIntent.

5. Forgetting to Log Refund Reasons

Stripe requires a refund reason. Log the reason and associate it with a support ticket for auditing and analytics.

Practice Questions

  1. What is the refund time window?
  2. How do idempotency keys prevent duplicate refunds?
  3. What is the difference between partial and full refunds?
  4. How do you handle a failed refund?

Answers:

  1. 120 days from the original charge. After that, the refund must be issued outside of Stripe (bank transfer, check).
  2. Same idempotency key + same parameters = same result. Retries with the same key return the original refund instead of creating a duplicate.
  3. Partial refunds return only part of the charge amount. Full refunds return the entire amount. Both can be combined (multiple partial refunds).
  4. Check refund.failure_reason for details. Common reasons: merchant_request, account_closed, processing_error. Retry or handle manually.

Challenge: Build a refund management system: process refunds with idempotency keys, support full and partial refunds, check refund status, handle failed refunds with retry logic, and maintain a refund log with amounts, reasons, and support ticket associations.

FAQ

How long does a refund take?

Most refunds process immediately and appear within 5-10 business days. International refunds may take longer.

Can I refund a charge after 120 days?

Not through Stripe. Issue the refund outside of Stripe (bank transfer, PayPal) and record it as a manual adjustment.

What happens to Stripe fees on refunds?

Stripe does not refund the processing fee. The fee remains, but the original charge amount is returned minus the fee.

Can I refund a charge from a deleted customer?

Yes, refund by PaymentIntent ID. The customer record deletion doesn't affect existing charges.

How do I report refunds for accounting?

Use the Stripe Dashboard's Reports or the Balance API. Refunds appear as negative amounts in your balance.

Mini Project

Build a refund management dashboard: process full and partial refunds with idempotency keys, handle 120-day window checks, log refunds with reasons and ticket IDs, create a refund status tracker with notifications on status changes, and report monthly refund totals.

What's Next

Disputes — handle chargebacks and dispute responses.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro