API Versioning & Idempotency
In a Nutshell
Two API concerns that determine how gracefully your API evolves and how safely clients can call it. Versioning is how you change an API without breaking the clients that already depend on it — because once someone integrates with your API, you can't just change its shape out from under them. Idempotency (in the API context) is how you let clients safely retry requests after a network failure without causing duplicate side effects — a client that times out on POST /charges must be able to retry without double-charging. Both are about the reality that APIs are contracts consumed by clients you don't control, over networks that fail.

How It Actually Works
Why Versioning Is Necessary
You ship an API. Clients integrate. Now you need to change it:
- rename a field, change a type, restructure a response, remove an endpoint
If you change it in place → every existing client BREAKS. 💥
Versioning lets old clients keep using the old contract while new clients
adopt the new one — evolve without breaking anyone.
Breaking vs Non-Breaking Changes
The first rule: know which changes force a new version.
| Non-Breaking (usually safe) | Breaking (needs a new version) |
|---|---|
| Adding a new optional field | Removing or renaming a field |
| Adding a new endpoint | Changing a field's type |
| Adding an optional parameter | Making an optional field required |
| Adding an enum value (carefully) | Changing response structure |
| Changing error codes/semantics |
Design clients to tolerate additions (ignore unknown fields) so additive changes never require a version bump.
Versioning Strategies
| Strategy | Example | Notes |
|---|---|---|
| URI path | /v1/orders, /v2/orders |
Most common, explicit, cache-friendly |
| Query param | /orders?version=2 |
Simple but easy to omit |
| Header | Accept: application/vnd.api.v2+json |
Clean URLs; less visible/discoverable |
| Content negotiation | Media type versioning | RESTful purist choice |
URI path versioning is the pragmatic favorite — explicit, visible, easy to route and cache. Whatever you choose, publish a deprecation policy: how long old versions live, how you communicate sunsets, and a migration path.
Idempotency for Safe Retries
The problem (see Idempotency): a client sends POST /charges, the server processes it, but the response is lost. The client can't tell "failed" from "succeeded-but-ack-lost," so it retries — risking a duplicate charge.
The solution: idempotency keys. The client generates a unique key per logical operation and sends it with every retry; the server dedupes.
POST /charges
Idempotency-Key: 7f3a-unique-per-charge ← same on every retry
Server:
if key seen before → return the STORED original response (no re-charge)
else → process, store (key → response) atomically, return
This is how Stripe makes charges safe to retry. Which HTTP methods are naturally idempotent matters too:
| Method | Idempotent? | Retry Safety |
|---|---|---|
| GET, PUT, DELETE, HEAD | Yes (by spec) | Safe to retry |
| POST, PATCH | No | Needs an idempotency key |
Combining Both: Evolving Safely + Retrying Safely
Versioning and idempotency are the two pillars of a robust, client-friendly API contract: versioning lets the API change over time without breaking clients; idempotency lets clients interact reliably over an unreliable network. Together they make an API something teams can build on with confidence.

Seeing It in Action
Scenario: Versioning an evolving payments API and making charges retry-safe.
Versioning — introducing a breaking change:
v1 response: { "amount": 1995 } (cents, integer)
Business wants: structured money with currency.
v2 response: { "amount": { "value": 19.95, "currency": "USD" } }
← BREAKING: shape and type changed.
Strategy:
- Serve BOTH: /v1/charges (unchanged) and /v2/charges (new shape).
- Old integrations keep working on v1; new ones adopt v2.
- Announce v1 deprecation with a 12-month sunset + migration guide.
- Add a `Deprecation` + `Sunset` response header to v1 responses.
Idempotency — safe retries on charge creation:
POST /v2/charges
Idempotency-Key: order-7-attempt ← stable per logical charge
{ "amount": { "value": 19.95, "currency": "USD" }, "source": "tok_..." }
Server logic (atomic):
try: INSERT idempotency_keys(key, status) VALUES('order-7-attempt','new')
except UniqueViolation: # duplicate retry
return stored_response(key) # original result, no double charge
result = charge_gateway(...)
store_response(key, result) # same transaction
return result
What happens on a flaky network:
attempt 1 → charged, but 200 response LOST → client sees timeout
attempt 2 → same Idempotency-Key → server returns the stored charge result
→ customer charged exactly once, client gets a clean success. ✅
Why both matter together: the versioning strategy means the payments team can improve the money representation (a genuinely breaking change) without a single existing integration breaking — old clients ride /v1 until they migrate on their own schedule. The idempotency key means that even over the unreliable networks real clients use, a retried charge never double-bills a customer. One concern protects clients across time (as the API evolves); the other protects them across failures (as requests are retried). An API that handles both is one that external teams can trust with money — which is the whole point.
Interview Questions
Q: What distinguishes a breaking from a non-breaking API change? Hint: Non-breaking (additive): adding an optional field, a new endpoint, or an optional parameter — existing clients keep working if they ignore unknown fields. Breaking: removing/renaming a field, changing a type, making an optional field required, restructuring responses, or changing error semantics — these force a new version because they invalidate existing client assumptions.
Q: Compare the main API versioning strategies. Hint: URI path (
/v1/orders) — explicit, visible, cache/route-friendly, most common. Query param (?version=2) — simple but easy to omit. Header/content negotiation (Accept: ...v2+json) — clean URLs but less discoverable/visible. URI path is the pragmatic favorite. Whatever you pick, pair it with a clear deprecation/sunset policy and migration path.Q: Why do clients need idempotency keys for POST requests? Hint: POST isn't idempotent, and networks fail — a client can't distinguish "request failed" from "succeeded but the response was lost," so it retries, risking duplicate side effects (double charge). An idempotency key (unique per logical operation, sent on every retry) lets the server detect the duplicate and return the original stored response instead of reprocessing — making retries safe.
Q: Which HTTP methods are idempotent, and how does that affect retry logic? Hint: GET, PUT, DELETE, HEAD are idempotent by spec — safe to retry directly since repeating them yields the same result. POST and (usually) PATCH are not — retrying can create duplicates or apply changes twice, so they need an idempotency key or conditional logic to be retry-safe. Clients/proxies can auto-retry idempotent methods but must be careful with POST.
Q: How do you roll out a breaking change without disrupting existing clients? Hint: Serve both versions simultaneously (e.g.,
/v1unchanged,/v2with the new shape), let existing clients stay on the old version, announce a deprecation timeline with a sunset date and migration guide, addDeprecation/Sunsetheaders to old responses, and monitor old-version usage to know when it's safe to retire. Never change an existing version's contract in place.
References
- Stripe API versioning — a gold-standard versioning approach
- Stripe idempotent requests — the canonical idempotency-key design
- Microsoft API Guidelines: Versioning — practical rules
Dive Deeper
- AWS Builders' Library: Making retries safe with idempotent APIs — deep dive
- Google API Design Guide: Versioning — semantic versioning for APIs
- Zdenek "How to version APIs" (Nordic APIs) — strategies compared