Stripe in C#, part 3: customers, saved payment methods, and charging while they sleep
Part 3from the Stripe in C# series · 6 parts in all
One-off charges were 2020's story. Real commerce means customers who come back: save a card once, then charge it later without the customer present ("off-session"). That means Customer objects, the SetupIntent (a PaymentIntent that moves no money), and the future-usage flags that decide what authentication is possible.
Customer + PaymentMethod: the pairing
var customerService = new CustomerService();
var pmService = new PaymentMethodService();
// 1. Create (or look up) your customer. Your invoice id in Description saves
// hours of dashboard archaeology later.
var customer = customerService.Create(new CustomerCreateOptions
{
Email = "[email protected]",
Name = "Beth Carpenter",
Description = "MapleCart customer 8891",
Metadata = new Dictionary<string, string> { { "userId", "8891" } },
});
// 2. Attach the payment method (created client-side) to the customer.
pmService.Attach(pmId, new PaymentMethodAttachOptions { Customer = customer.Id });
// 3. Make it the default so future integrations have one obvious answer.
customerService.Update(customer.Id, new CustomerUpdateOptions
{
InvoiceSettings = new CustomerInvoiceSettingsOptions
{
DefaultPaymentMethod = pmId
}
});
The SetupIntent: saving a card is its own transaction
Saving a card for future use is a regulated act too. The SetupIntent is a PaymentIntent whose amount is zero and whose purpose is consent: the customer completes 3DS now, while present, and the saved method becomes usable off-session later.
var setupService = new SetupIntentService();
var setup = setupService.Create(new SetupIntentCreateOptions
{
Customer = customer.Id,
PaymentMethodTypes = new List<string> { "card" },
Usage = "off_session", // declare the intent: we will charge without them
});
// Client confirms with Stripe.js (confirmCardSetup) exactly like part 2.
Charging off-session, and the one error you must handle
var intent = intentService.Create(new PaymentIntentCreateOptions
{
Amount = 2999, Currency = "usd",
Customer = customer.Id,
PaymentMethod = customer.InvoiceSettings.DefaultPaymentMethod,
OffSession = true, // "the customer is not here"
Confirm = true, // create + confirm in one call
Metadata = new Dictionary<string, string> { { "orderId", "2210" } },
});
if (intent.Status == "requires_payment_method")
{
// Off-session charge declined - issuer demands customer interaction
// (3DS) or plain refusal. Email the customer; never retry blindly.
SendPaymentNeededEmail("8891", "2210");
}
The pattern to internalize: on-session = customer present, any card, full authentication possible. Off-session = saved card, issuer may still demand interaction, and the only graceful response is "ask the customer to come back". Subscription engines (Stripe Billing) are this loop with retries and dunning baked in. Next: Checkout Sessions — the fastest legal way to take money on the web.