Trust is earned, not given

A different perspective

2024-03-19 · Projects

Stripe in C#, part 5: refunds, disputes, and invoices — the money comes back

Part 5from the Stripe in C# series · 6 parts in all

Taking money is half the API. The other half is what happens after: refunds, chargebacks, and recurring invoices. These operations are asynchronous by nature — which is why this part keeps pointing at part 6's webhook receiver.

Refunds

var refundService = new RefundService();

// Full refund of a PaymentIntent (or partial, by amount).
var refund = refundService.Create(new RefundCreateOptions
{
    PaymentIntent = "pi_3Oabc...",
    // Amount = 1500,             // omit = full refund; present = partial
    Reason = "requested_by_customer",
    Metadata = new Dictionary<string, string> { { "rmaId", "5501" } },
});
// Status walks created -> pending -> succeeded (or failed). The charge
// object's amount_refunded is the source of truth; bank arrival is days later.

Disputes: the 7-to-21-day clock

A chargeback reverses the money immediately and starts a clock. You have a fixed window to submit evidence; Stripe forwards it to the issuer; the network decides weeks later. The API role is narrow but critical: never miss the deadline.

var disputeService = new DisputeService();
var d = disputeService.Get("dp_...");   // fetched on dispute.created webhook

// Evidence is per-field per network; this is the shape, not the full schema.
disputeService.Update(d.Id, new DisputeUpdateOptions
{
    Evidence = new DisputeEvidenceOptions
    {
        ProductDescription = "Canvas tote bag, shipped 2024-03-02",
        ShippingDocumentation = "file_...",      // uploaded via FileService
        CustomerCommunication = "file_...",      // the receipt email thread
    },
    Submit = true,    // false = save draft, true = submit to the network
});
// dispute.closed webhook arrives with status won / lost / charge_refunded.
// Note: losing a dispute whose refund also landed = "charge_refunded".

Invoices: recurring billing without building a subscription engine

// The Stripe Billing triangle: Price (what and how much) + Customer
// + Subscription (the schedule). Invoices are generated automatically.
var priceOptions = new PriceCreateOptions
{
    Product = "prod_...",                 // create product+price once
    UnitAmount = 1900, Currency = "usd",
    Recurring = new PriceRecurringOptions { Interval = "month" },
};
var subService = new SubscriptionService();
var sub = subService.Create(new SubscriptionCreateOptions
{
    Customer = "cus_...",
    Items = new List<SubscriptionItemOptions>
    {
        new SubscriptionItemOptions { Price = "price_..." },
    },
    // Off-session by construction; retry schedule and dunning are configured
    // in the dashboard and arrive in your webhook stream as invoice events.
});
// invoice.payment_failed + customer.subscription.* are the events you build
// around: that pair IS the dunning workflow's API surface.

Everything here is asynchronous

Refund statuses drift, disputes open while you sleep, invoices retry for days. Any code that only reads the synchronous API response will miss most of reality. That is the case for webhooks — and they deserve more than a paragraph, so next part is all of them: every event family, the signature scheme, idempotency, and retries.