Stripe in C#, part 1: keys, Stripe.net, and your first charge
Part 1from the Stripe in C# series · 6 parts in all
This series assumes you have never touched Stripe. By the end of part 1 you will have a test-mode charge running from a C# console app, and — more importantly — the mental model for everything that follows: keys, the test card numbers, and the Charges-vs-PaymentIntent transition that is reshaping the whole platform in 2019.
Keys: four strings that define your world
Every Stripe account (test and live) has a publishable key
(pk_test_...) meant for browsers and phones, and a secret key
(sk_test_...) meant for servers. The secret key is a password that can move
money — it never ships to a client, ever. Test mode is a complete parallel universe with
its own dashboard, its own test cards, and no real money; live mode differs by one letter
in the key prefix and one giant legal reality.
dotnet add package Stripe.net
using Stripe;
// Configure once at startup (in ASP.NET Core, do this in ConfigureServices).
// The SDK pins an API version for you; pin the package version in source control.
StripeConfiguration.ApiKey = "sk_test_51ABC...";
// The oldest, simplest money move: a one-off charge on a raw card token.
// "tok_visa" is a magic test token that stands in for a card from Stripe.js.
var options = new ChargeCreateOptions
{
Amount = 2099, // minor units! $20.99 is 2099, never a float
Currency = "usd", // ISO 4217, lowercase
Description = "MapleCart order 1042",
Source = "tok_visa", // token created client-side by Stripe.js
Metadata = new Dictionary<string, string> // free-form, shows in dashboard
{
{ "orderId", "1042" } // your keys, your correlation IDs
}
};
var service = new ChargeService();
Charge charge = service.Create(options);
Console.WriteLine($"{charge.Id} {charge.Status}"); // ch_... succeeded
The three rules beginners trip on
- Amounts are integers in minor units. Cents for USD. Using a decimal field is the classic double-charge bug.
- Idempotency keys exist and you should use them. A network timeout does
not mean the request failed — it means the request is unknown. Retry with the same
idempotency key and Stripe replays the original response instead of charging twice:
service.Create(options, new RequestOptions { IdempotencyKey = "order-1042" }); - Test cards are documentation, not data.
4242 4242 4242 4242always succeeds;4000 0000 0000 3220forces 3D Secure; any future expiry, any CVC.
2019's transition: Charge → PaymentIntent
Everything above works — and is already legacy. PSD2 in Europe made strong customer authentication (3DS challenges) mandatory, and a Charge has nowhere to put an authentication step. The PaymentIntent (API version 2019-03-14 onward) is the object that tracks a payment through its lifecycle, including "waiting for the customer to authenticate". All new integrations should start there; part 2 is entirely about it.
Next: the PaymentIntent state machine, server and client sides, and how 3DS fits without breaking your checkout.