# Versioning & Designing APIs That Last

> An API is a promise to everyone who built on it: what counts as a breaking change, how to evolve without breaking clients (additive-first, then versioning + deprecation), and the durable-API checklist - consistent shapes, pagination, idempotency, rate limits, and great docs.


---

# Versioning & Designing APIs That Last

The first version of an API is the easy part. You ship it, a few clients integrate, and it works. The
hard part starts the day after - because now there are people out there whose code *depends on yours*,
and they can't see the changes you make. You can't roll their integration back. You often don't even
know who they are. So a one-line "improvement" - renaming a field, tightening a type, dropping
something you thought nobody used - can quietly break someone's production system on a random Tuesday,
and the first you'll hear of it is an angry support ticket.

That's the shift this guide is built around: **an API is a promise to everyone who built on it.** Once
you internalize that, versioning and design stop being bureaucracy and become the obvious way to keep
that promise - what a breaking change actually is, the real trade-offs of every way to evolve an
API without breaking people, and the checklist that makes an API durable enough to live for years.

## How to read this

- **About to change a live API right now and want to know if it's safe?** Jump to
  [Phase 1: The Contract Is Forever](01-the-contract-is-forever.md) and check your change against the
  breaking-vs-safe cheat-card at the top.
- **Want API longevity to finally make sense?** Read in order - each phase builds on the last. Phase 1
  is the mindset, Phase 2 is how to evolve, Phase 3 is how to design so you rarely have to.

## The phases

1. **[The Contract Is Forever](01-the-contract-is-forever.md)** - once clients depend on your API, a
   breaking change breaks *them*, silently, in production. What counts as breaking (removing or
   renaming fields, changing types or meaning) versus a safe additive change - and the mindset shift
   that follows.
2. **[Versioning Strategies](02-versioning-strategies.md)** - how to evolve without breaking people:
   prefer additive changes; when you genuinely must break, version (URL `/v2/` vs. a header), run the
   old and new in parallel, and deprecate on a clear timeline with real communication. The real
   trade-offs of each path.
3. **[Designing for Longevity](03-designing-for-longevity.md)** - the durable-API checklist:
   consistent resource and error shapes, pagination from day one, idempotency keys for safe retries,
   rate limits, sensible defaults, and documentation people can actually use. Plus the trap of leaking
   your internal database structure into your public contract.

> Authentication and authorization for APIs (API keys, OAuth, scopes, token rotation) are a security
> topic in their own right and live in the future **security** category - we point there lightly where
> it matters rather than rushing it here.

**Related:** [REST APIs, Explained](/guides/rest-apis-explained) ·
[Webhooks and Message Queues](/guides/webhooks-and-message-queues)


---

# The Contract Is Forever

You changed one field. Tests green, staging fine, you ship it. Somewhere out there, a payment
integration you've never heard of, written by a team you've never met, stops working at 3am their
time. They didn't change anything. *You* did. But it's their pager going off.

When you write a normal function, only code you can see and re-run can break. An API is the
opposite: the code that depends on it lives on *other people's* machines, written against the shape
your API had the day they integrated - and you can't test it, see it, or fix it. So the one skill
that matters above all others: looking at a proposed change and knowing, cold, whether it will break
someone. That's this phase.

## The breaking-vs-safe cheat-card

> **About to change a response, a request, or a status code? Find it here first.** If it's in the left
> column, you cannot do it in place - you need a new version (Phase 2).

| The change | Breaking? | Why |
|---|---|---|
| Remove a field from a response | 💥 **Breaking** | A client reading it now gets nothing (§ "Removing") |
| Rename a field (`user_name` → `username`) | 💥 **Breaking** | It's a remove + an add; the old name vanishes (§ "Renaming") |
| Change a field's type (`"42"` → `42`) | 💥 **Breaking** | Client's parser/validator chokes on the new type (§ "Changing types") |
| Change what a field *means* (same name, new semantics) | 💥 **Breaking** | The worst kind - nothing errors, behavior just goes wrong (§ "Changing meaning") |
| Add a new optional field to a response | ✅ Safe | Old clients ignore what they don't read (§ "The one safe move") |
| Add a new endpoint | ✅ Safe | Nobody depends on it yet |
| Add a new **optional** request parameter | ✅ Safe | Omitting it keeps the old behavior |
| Make a previously-required request field optional | ✅ Safe | Every existing request still validates |
| Make an optional request field **required** | 💥 **Breaking** | Existing requests that omit it now fail |
| Add a new value to an enum the client must handle | ⚠️ **Often breaking** | Clients with strict `switch`/validation reject the unknown value (§ "The enum trap") |

The rest of this phase is the *why* under each row - once it clicks, you can judge changes that
aren't on any list.

## What "the contract" actually is

Your API's contract isn't a document - it's the *observable behavior clients have come to rely on*.
Every field name, type, status code, error shape, default: if a client can see it and build on it,
it's part of the promise, whether you wrote it down or not.

Clients don't integrate against your docs; they integrate against what your API *actually returned*
the day they tested. If your docs say a field is "a string" and you've always returned a number,
clients coded for the number. This is also why undocumented fields are dangerous - people find them,
depend on them, and now you can't change those either.

```text
   What you think the contract is        What it really is
   ─────────────────────────────        ─────────────────────────────
        ┌───────────────┐                ┌───────────────┐
        │   your docs    │                │  every byte a  │
        │  (what you      │                │  client has    │
        │   meant)        │      vs.      │  ever seen and │
        └───────────────┘                │  depended on   │
                                          └───────────────┘
                                          ← this is what breaks them
```

📝 **Terminology.** A change is **backward compatible** if a client written against the *old* version
keeps working unchanged against the *new* one. "Breaking" is the opposite. The whole game of this
guide is staying backward compatible as long as possible.

## Removing a field - the obvious break

A client somewhere reads `response.total_price`. You decide it's redundant and delete it. Their code
now reads `undefined` (or throws, or renders `$NaN` on a checkout page), with no warning until it
happens live.

```console
$ # Old response your client integrated against:
$ curl https://api.example.com/orders/1138
{
  "id": 1138,
  "status": "shipped",
  "total_price": 4200,
  "total_price_display": "$42.00"
}

$ # You "cleaned up" and removed total_price. New response:
$ curl https://api.example.com/orders/1138
{
  "id": 1138,
  "status": "shipped",
  "total_price_display": "$42.00"
}
```
Nothing errors on your side - the response is perfectly valid JSON. But every client that read
`total_price` now gets nothing where a number used to be. The break is silent on your end, loud on
theirs.

⚠️ **Gotcha.** "Nobody uses that field" is a guess unless you have request-level telemetry proving it
 - and even then, a client that calls the endpoint rarely (a monthly billing job) may not show up in a
week of logs. Treat removal as breaking by default.

## Renaming a field - a remove plus an add

A rename feels gentler than removal - you're not *losing* data, just calling it a better name. But to
a client, it's a removal of the old name and an addition of a new one they never asked for.

```console
$ # Before:
{ "user_name": "ada", "id": 7 }

$ # After "just renaming for consistency":
{ "username": "ada", "id": 7 }
```
The client reading `user_name` now reads `undefined`; `username` is invisible to them because their
code never asked for it. You broke a client *and* left the data sitting right there under a
different key - which makes the bug extra confusing to debug from their side.

## Changing a field's type - the parser break

Same field name, different type: an ID goes from string to real integer, or a money amount from
integer cents to a decimal string. Every client that parsed or validated the old type breaks.

```console
$ # Before - id is a string:
{ "id": "1138", "amount": 4200 }

$ # After - id is now a number, amount is now a decimal string:
{ "id": 1138, "amount": "42.00" }
```
A statically-typed client (Go, Rust, Java) that declared `id: String` now fails to deserialize the
*whole* response - one field's type flipped and the entire parse blows up. A client doing `amount *
quantity` now does string-times-number and gets garbage.

💡 **Key point.** Type changes are sneaky because the *shape* (the set of keys) looks identical in a
diff. Reviewers scanning for added/removed keys miss them. A value's type is part of the contract too.

## Changing a field's meaning - the worst kind

The field keeps its name *and* type, but you change what it represents: `price` used to be dollars,
now it's cents. A `status` of `"active"` used to mean "subscribed," now means "account exists."

Every other breaking change at least *fails loudly* somewhere - a missing field, a parse error. A
meaning change passes every type check and schema validation. The data flows through perfectly. It's
just wrong.

```console
$ # Before - price is in whole dollars:
{ "id": 1138, "price": 42 }

$ # After - you switched the whole system to cents, price is now 4200:
{ "id": 1138, "price": 4200 }
```
The response is structurally identical - a number called `price`, exactly as before. No error, no
alert. A reporting dashboard now shows every order at 100× its real value; a fraud rule that flags
orders over `1000` now flags everything. The break is invisible until someone notices the numbers are
insane, and it's been wrong for days by then.

## The one safe move: additive changes

Here's the asymmetry the entire next phase is built on. **Adding is (almost always) safe; removing,
renaming, and redefining are not.** A well-behaved client reads the fields it knows and ignores the
rest, so a *new, optional* field is invisible to old clients and available to new ones.

```console
$ # Old client integrated against this:
{ "id": 1138, "status": "shipped" }

$ # You add a new field. Old client still works; new clients can use it:
{ "id": 1138, "status": "shipped", "estimated_delivery": "2026-06-25" }
```
The old client keeps reading `id` and `status` exactly as before and never notices
`estimated_delivery` exists. Pure upside for clients who want it, zero cost for clients who don't - 
*the lever you'll pull instead of breaking changes whenever you possibly can.*

⚠️ **The enum trap.** "Additive" has one exception. Adding a new *value* to an existing field - a new
`status` like `"refunded"`, a new `type` like `"gift_card"` - is safe only if clients shrug at values
they don't recognize. Many aren't: a strict `switch` with no default, or a schema listing allowed
values, will *reject* the unknown one. Design clients to tolerate unknown values, and document that
new values may appear - see Phase 3's "sensible defaults."

## The mindset shift

- **You can add. You can't take away or redefine.** Once a field, type, or meaning is public, it's
  effectively frozen for the life of that version.
- **"Silent" is the default failure mode.** Most breaking changes don't error on your side - they
  succeed on your side and fail on theirs, which is exactly why they slip through.
- **Default to "breaking" when unsure.** If you can't *prove* a change is backward compatible, treat
  it as if it isn't, and reach for the tools in Phase 2.

The people who depend on you can't see your changes coming and can't fix the breakage themselves.
Hold the promise, and they trust you with more. Break it silently, and they start pinning to old
versions, wrapping your API in defensive code, or leaving.

## Recap

1. Your **contract** is every observable behavior clients rely on - fields, types, status codes,
   meanings - documented or not.
2. **Removing, renaming, and type-changing** a field are all breaking: the client built against the
   old shape, and the old shape is gone.
3. **Changing a field's meaning** is the most dangerous break - it passes every check and just
   produces wrong results.
4. **Adding an optional field or endpoint** is the one reliably safe move; old clients ignore what
   they don't read.
5. **Adding an enum value is a gray-zone change** - safe for tolerant clients, breaking for strict ones.
6. The mindset: **you can add but not take away**, and most breaks are **silent on your side**, so
   default to "breaking" when in doubt.


---

# Versioning Strategies

Phase 1 left an uncomfortable truth: you can add to an API forever, but the day you genuinely *must*
remove or redefine something, there's no in-place way to do it without breaking people. This phase is
that day - the techniques that let an API change shape over years while clients built against its
older shapes keep working.

The order here is also the order of preference: **stretch as far as you can with additive changes,
and only reach for a new version when you've truly run out of additive room.** A version bump is the
most expensive tool in the box - two code paths for you, a migration for clients. Used well, it's a
clean break. Used reflexively, it's a treadmill.

## First, exhaust the additive option

Before any versioning, ask: *can I get what I want by adding instead of changing?* Surprisingly
often, yes - and it sidesteps the entire cost of a version.

- **Want to rename `user_name` to `username`?** Don't. Add `username` alongside it, populate both,
  point new clients at the new one, and let the old field live on (deprecate it later - nobody breaks
  today).
- **Want to change a field's type?** Add a *new* field with the new type (`amount_decimal` next to
  `amount`) rather than mutating the existing one.
- **Want to change behavior?** Gate it behind a new *optional* request parameter that defaults to the
  old behavior. Old clients omit it and get exactly what they got yesterday.

```console
$ # Old clients omit the new param → old behavior, unchanged:
$ curl "https://api.example.com/orders?include=items"

$ # New clients opt in → new behavior:
$ curl "https://api.example.com/orders?include=items&group_by=warehouse"
```
Genuinely new behavior, no version, no migration, no parallel maintenance - the new capability is
opt-in, and the default is "behave like before."

💡 **Key point.** Most "we need v2" instincts dissolve once you ask "what's the additive version of
this?" Versioning is for when the *shape itself* must change in a way no addition can express - a
wholesale restructuring, a removal you can no longer defer, a meaning shift you can't dual-write around.

## When you must break: versioning

Sometimes additive genuinely can't get you there: the resource needs a fundamentally different shape,
an old field has become actively harmful, or the model underneath changed so much that bolting on
fields would make the API incoherent. That's when you version - via **URL versioning** or **header
versioning**, and they make a real trade-off.

```text
                      URL versioning                 Header versioning
                      /v2/orders                      Accept: application/vnd.example.v2+json
   ───────────────  ─────────────────────────────   ─────────────────────────────────────
   Visibility        Obvious - it's in the URL        Hidden - lives in a header
   Try in a browser  Yes, just paste the URL          No, you must set a header
   Routing/caching   Easy: route & cache by path      Harder: must vary by header
   Granularity       Whole API (or whole path) jumps  Can version finely, per request
   "RESTful purist"  Disliked (URL = the resource,    Preferred (one resource, many
   view             not its version)                  representations)
   Who uses it       Stripe-style date headers aside, very common; widely understood
```
*Neither is "correct."* URL versioning optimizes for **obviousness and operational simplicity**;
header versioning optimizes for **purity and fine-grained control**. Most teams land on URL
versioning - but pick for your audience, not the argument.

### URL versioning - `/v2/`

The version is a path segment: `/v1/orders`, `/v2/orders`. A new major version is a new set of paths.

```console
$ curl https://api.example.com/v1/orders/1138
{ "id": 1138, "price": 42 }          # v1: price in dollars (the old, frozen contract)

$ curl https://api.example.com/v2/orders/1138
{ "id": 1138, "price_cents": 4200 }  # v2: the redesigned shape
```
`/v1/` keeps serving the exact old contract forever; `/v2/` is free to be a clean, redesigned shape.
The version is impossible to miss - it's in every request, log line, bug report. The cost: "the
orders resource" now lives at two URLs (REST purists dislike this), and a client moving to v2 has to
change every URL it calls.

### Header versioning - `Accept:` (or a custom header)

The URL stays `/orders` forever; the client states its version in a request header - commonly a
versioned media type in `Accept`, or a custom header.

```console
$ curl https://api.example.com/orders/1138 \
    -H "Accept: application/vnd.example.v1+json"
{ "id": 1138, "price": 42 }

$ curl https://api.example.com/orders/1138 \
    -H "Accept: application/vnd.example.v2+json"
{ "id": 1138, "price_cents": 4200 }
```
Same URL, two representations, selected by header - one canonical address, and a client can move to
v2 by changing a header rather than rewriting every path. The costs: you can't reproduce a v2
response by pasting a URL into a browser, your caches and routers must be configured to **vary by
that header** (a classic source of "why am I getting the wrong version" bugs), and the version is
easy to forget because it's not in the URL you read every day.

⚠️ **Gotcha - don't version per tiny change.** Whichever scheme you pick, a version should mark a
*deliberate, breaking redesign* - not every little change. Cut a new version per field tweak and
clients face an endless migration treadmill while you maintain a graveyard of code paths. Keep making
additive changes *within* a version; reserve the bump for breaks you couldn't avoid.

## Run the old and new in parallel

A version bump is only humane if the old version keeps working while clients migrate - the whole
point of v2 is that v1 *doesn't disappear the moment v2 ships*.

```text
   time ──────────────────────────────────────────────────────►

   v1  ████████████████████████░░░░░░░░░░░  (live, then deprecated, then off)
                       v2  ███████████████████████████████████  (live)
                       ▲                  ▲                    ▲
                   v2 ships          deprecation         v1 sunset
                                     announced            (turned off)
                       └─── migration window ────────────┘
```
For a real stretch of time, both versions serve traffic. Clients migrate on *their* schedule inside
the window, not yours; v1 only goes dark after a clearly communicated sunset. Skip the overlap - flip
v1 off the day v2 lands - and "versioned" just becomes a breaking change with extra steps.

That overlap is the real cost of versioning: two code paths, two sets of tests, two things that can
break, for as long as the window lasts. It's exactly why you exhaust additive changes first.

## Deprecate on a clear timeline, with real communication

"Deprecated" doesn't mean "deleted." It means **"still works, but is going away on a date we're
telling you now."** A deprecation clients learn about via a `404` is not a deprecation - it's an
outage you scheduled and didn't mention.

📝 **Terminology.** **Deprecate** = officially mark as going-away, while keeping it working. **Sunset**
= the date it actually stops working. The gap between them is the migration window.

A respectful deprecation does three things:

1. **Announce it where clients will see it** - changelog, email to registered developers, dashboard
   banner. Not buried in a doc nobody re-reads.
2. **Signal it in the API itself**, so clients who never read your email find out in their logs. The
   HTTP `Deprecation` and `Sunset` response headers exist for exactly this.
3. **Give a real, generous timeline** - proportional to how widely the thing is used. Days for an
   obscure beta endpoint; many months for a core resource thousands depend on.

```console
$ curl -i https://api.example.com/v1/orders/1138
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Oct 2026 23:59:59 GMT
Link: <https://api.example.com/docs/v2-migration>; rel="deprecation"
{ "id": 1138, "price": 42 }
```
v1 still returns a perfectly good `200` - nobody's broken today. But every response now carries
machine-readable proof that this version is on the clock: `Sunset` gives the exact cutoff date, `Link`
points at the migration guide. (`Deprecation` and `Sunset` are defined in IETF RFC 9745 and RFC 8594.)

⚠️ **Gotcha - the silent majority.** Some clients never read your changelog, email, or headers. Before
you flip the switch on sunset day, look at your traffic - who's still calling v1? Reach out to the
heavy holdouts directly if you can. Turning off a version that's still serving real traffic, on
schedule but without a final check, is how a planned deprecation becomes someone's incident.

## Putting the strategy together

1. **Can I do it additively?** (New optional field, new endpoint, new opt-in parameter.) → Do that.
   No version, no migration. This is the answer most of the time.
2. **Must the shape itself break?** → Cut a new version (URL `/v2/` for obviousness, header for
   purity), and **run it in parallel** with the old one.
3. **Ready to retire the old version?** → **Deprecate first** - announce it, signal it in response
   headers, give a generous timeline, check who's still on it - *then* sunset.

Every step keeps the Phase 1 promise: clients depend on you and can't fix breakage themselves, so you
change in ways they can absorb on their own schedule. Phase 3 turns the lens forward - designing the
API so you land in step 1 far more often than step 2.

## Recap

1. **Exhaust additive changes first** - new fields, new endpoints, opt-in parameters - before reaching
   for a version. It's the cheapest evolution and avoids a migration entirely.
2. When the shape *must* break, **version**: **URL `/v2/`** trades RESTful purity for obviousness and
   easy routing/caching; **header versioning** trades convenience for a clean single-resource URL and
   finer control.
3. **Run old and new in parallel** through a real migration window - versioning without overlap is
   just a breaking change in disguise.
4. **Deprecate before you sunset**: announce it, signal it with `Deprecation`/`Sunset` response
   headers, and give a timeline proportional to usage.
5. Before flipping a version off, **check who's still calling it** - the silent majority never reads
   your announcements.

Watch it animated: [API versioning](/explainers/APIVersioning.dc.html)


---

# Designing for Longevity

The cheapest breaking change is the one you never have to make. Phases 1 and 2 were about surviving
change; this phase is about needing less of it. Almost every painful version bump traces back to a
day-one decision - a shape that felt fine for the first three endpoints and became a straitjacket by
the thirtieth, a list endpoint that returned everything because the table was small, a retry that
charged a customer twice.

None of the choices below are exotic. They're the handful of decisions that, made early, let an API
grow for years without forcing clients to keep rewriting their integration - a pre-flight checklist to
run *before* you publish, while changing your mind is still free.

## 1. Consistent shapes - pick a pattern and never deviate

Every resource in your API should be shaped the same way, and so should every error. If an `order`
has `id`, `created_at`, and nests its items under `items`, a `customer` should follow the same
pattern. Consistency means a client who learns one endpoint has learned them all.

Inconsistency is a permanent tax: every endpoint that does things its own way is a special case
clients must learn, you must document, and neither of you can change later without breaking
something. Pick the conventions once - field naming (`snake_case` vs. `camelCase`), timestamps (ISO
8601 UTC strings is the safe default), how you nest related data - and hold the line.

```console
$ # Same envelope everywhere - learn it once, know it for every resource:
$ curl https://api.example.com/v1/orders/1138
{
  "data": {
    "id": 1138,
    "type": "order",
    "created_at": "2026-06-19T14:30:00Z",
    "status": "shipped"
  }
}
```
A client that handles this `data` envelope for orders handles it for customers, invoices, and
everything you add later - for free. It also leaves room to add top-level siblings (like `meta` for
pagination, below) without disturbing `data`.

### Error shapes are part of the contract too

The most-overlooked consistency failure is errors. Clients write error handling *once*, against
whatever your errors looked like the day they integrated. If every endpoint fails differently, you've
forced every client into a tangle of special cases.

```console
$ curl -i https://api.example.com/v1/orders/99999
HTTP/1.1 404 Not Found
{
  "error": {
    "code": "order_not_found",
    "message": "No order exists with id 99999.",
    "request_id": "req_8a2b..."
  }
}
```
The HTTP status (`404`) gives the broad category a client can branch on; the stable `code`
(`order_not_found`) is machine-readable and won't change even if you reword the message; `message` is
for humans reading logs; `request_id` lets a client quote one string in a support ticket. Use this
same shape for *every* error. (RFC 9457 defines a standard "problem details" JSON shape if you'd
rather adopt a convention than invent one.)

💡 **Key point.** Treat your error format as frozen as firmly as your success responses - changing it
later breaks every client's `catch` block.

## 2. Pagination from day one

A list endpoint that returns the whole collection works beautifully at 12 rows and is a catastrophe
at 12 million. The trap: adding pagination *later* is itself a breaking change, since clients coded
against "this returns the full array" will silently process only a fraction of the data once you
start paging. So you paginate **before** you have the data to justify it.

```console
$ curl "https://api.example.com/v1/orders?limit=2"
{
  "data": [ { "id": 1138 }, { "id": 1139 } ],
  "meta": {
    "next_cursor": "eyJpZCI6MTEzOX0",
    "has_more": true
  }
}

$ # Follow the cursor for the next page:
$ curl "https://api.example.com/v1/orders?limit=2&cursor=eyJpZCI6MTEzOX0"
```
The list never promises "everything" - it returns a page plus a `next_cursor`, and `has_more` tells
the client when to stop. Because this was the contract from day one, the shape never has to change as
the collection grows. (This is **cursor-based** pagination - the cursor encodes "where you left off."
It's sturdier than `?page=2&size=20` **offset** pagination, which can skip or duplicate rows when
items are inserted or deleted between requests, and gets slow on deep pages. Offset is simpler and
fine for small, stable datasets; cursor is the safer default for anything that grows.)

⚠️ **Gotcha.** Even if you launch with offset pagination for simplicity, *launch with pagination*. The
breaking change is "unpaginated → paginated," and you only avoid it by paginating from the start.

## 3. Idempotency keys - make retries safe

Networks fail in the cruelest way: the request arrives, your server processes it, and the *response*
gets lost on the way back. The client never heard "success," so it retries - and now the charge, the
order, the email happens twice. For anything that creates or moves money, "just retry" is how you
double-charge a customer.

An **idempotency key** is a unique value the *client* generates and sends with a request. Your server
remembers the result it produced for that key; if the same key comes in again, it returns the
*stored* result instead of redoing the work.

📝 **Terminology.** An operation is **idempotent** if doing it twice has the same effect as doing it
once. `GET` and `DELETE` are naturally idempotent; `POST` (create) is the dangerous one, which is why
idempotency keys target it.

```console
$ # First attempt - client generates a key, server does the work:
$ curl -X POST https://api.example.com/v1/charges \
    -H "Idempotency-Key: a1b2c3-d4e5-charge-once" \
    -d '{ "amount_cents": 4200, "currency": "usd" }'
HTTP/1.1 201 Created
{ "id": "ch_77", "amount_cents": 4200, "status": "succeeded" }

$ # Response got lost; client retries with the SAME key:
$ curl -X POST https://api.example.com/v1/charges \
    -H "Idempotency-Key: a1b2c3-d4e5-charge-once" \
    -d '{ "amount_cents": 4200, "currency": "usd" }'
HTTP/1.1 200 OK
{ "id": "ch_77", "amount_cents": 4200, "status": "succeeded" }
```
The retry carries the same key, so the server recognizes it already created charge `ch_77` and
returns that *same* charge instead of a second one. The customer is charged once, the client gets a
clean success on retry, and a lost-response network blip stops being a financial incident. Offering
this on your write endpoints is one of the highest-trust things an API can do.

## 4. Rate limits - protect the API and tell clients the rules

Without limits, one buggy client in a retry loop - or one abusive one - can degrade the API for
everyone. But limits clients can't *see* are just random failures from their side, so the design is
half "enforce" and half "communicate."

```console
$ curl -i https://api.example.com/v1/orders
HTTP/1.1 200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 6
RateLimit-Reset: 30
...

$ # A few requests later, over the limit:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
{ "error": { "code": "rate_limited", "message": "Rate limit exceeded. Retry in 30 seconds." } }
```
The `RateLimit-*` headers tell a well-behaved client how much budget is left and when it resets, so it
can slow itself down *before* hitting the wall. When it does exceed the limit, the `429` carries a
`Retry-After` telling it exactly how long to wait - a predictable rule to program against, not a
mysterious intermittent failure. (`429` is RFC 6585; `RateLimit-*` headers are an emerging IETF
standard - check current support before relying on the exact names.)

## 5. Sensible defaults - make the easy path the safe path

A durable API makes the *common* call simple and the *full* call possible, and never punishes a
client for not knowing about an option added after they integrated.

- **List endpoints default to a sane page size** (not "everything"), so a naive `GET /orders` can't
  accidentally pull a million rows.
- **New optional parameters default to the old behavior** - the default *is* the backward-
  compatibility guarantee from Phase 2.
- **Be liberal in what you accept, strict and predictable in what you return.** Tolerate fields you
  don't recognize in requests, and design clients to tolerate fields and enum values they don't
  recognize in *responses* - that tolerance is what makes your additive changes (Phase 1) safe in
  practice.

💡 **Key point.** Every default is a promise about what happens when a client says nothing. Choose
defaults so the client who knows the least still gets safe, correct behavior.

## 6. Great docs - the part of the API people actually touch

Clients integrate against your *docs* far more than your source. Docs that are wrong, stale, or
missing examples generate the same support load as a buggy API - documentation largely *is* the API
to the person integrating.

- **A real example request and response for every endpoint** - copy-pasteable, realistic values, not
  `string` and `0`.
- **The error catalog** - every `code` you return and what it means.
- **A changelog** - the running record of what changed and when; also where deprecation
  announcements (Phase 2) live.
- **A way to try it** - see [Reading API Docs (and Using Postman)](/guides/reading-api-docs-postman)
  for getting hands-on and learning to read docs effectively.

## ⚠️ The big trap: leaking your internal database into your public contract

The fastest way to ship an API is to serialize your database rows straight to JSON - your `orders`
table becomes your `GET /orders` response, column-for-column. It feels efficient. It is a trap:
your *internal schema becomes your public contract*. Now you can't:

- **Rename a column** without breaking clients (Phase 1: renaming is breaking).
- **Split or merge a table**, normalize, denormalize, or switch databases - every refactor your data
  layer needs is now a breaking API change.
- **Hide a column you never meant to expose** - an internal flag, a soft-delete marker, a cost field - 
  once it's out, removing it breaks someone.

Your database schema changes for *database* reasons (performance, normalization, a new feature's
needs). Your API contract should change only for *API* reasons. Wiring them together means every
internal refactor leaks out as a breaking change to people who have no idea your database even exists.

```mermaid
flowchart LR
  subgraph Leaky["❌ Leaky - refactor the table and you break clients"]
    R1[orders row] -->|serialize 1:1| Resp[API response]
  end
  subgraph Decoupled["✅ Decoupled - refactor freely behind the map; public shape stays stable"]
    R2["orders row (internal)"] -->|map / transform, you choose| Pub["resource (public shape)"]
  end
```

**The fix.** Put a deliberate translation layer between your storage and your API - a serializer,
mapper, view model, DTO, whatever your stack calls it. It does one job: turn internal data into the
*public shape you chose on purpose*. With that layer in place, you can rename columns, switch
databases, and restructure storage all day, and the public contract never flinches. A little more code
today, a wall of avoided breaking changes for years.

📝 **Terminology.** A **DTO** (Data Transfer Object) or **serializer** is that translation layer: a
deliberate definition of "what this resource looks like to the outside world," kept separate from
"how it's stored inside."

## A note on auth

We've stayed off authentication and authorization deliberately - API keys, OAuth flows, scopes, token
rotation are a security discipline of their own and live in the future **security** category. Design
your durable shapes now; treat auth as its own first-class topic when you get there.

## The durable-API checklist

Run this before you publish, while changing your mind is still free:

1. **Consistent shapes** - one resource envelope, one error format, one naming convention, frozen.
2. **Pagination from day one** - every list endpoint, before you have the data to justify it.
3. **Idempotency keys** - on every write that creates or charges, so retries are safe.
4. **Rate limits** - enforced *and* communicated via headers + `429` + `Retry-After`.
5. **Sensible defaults** - the easy path is the safe path; new options default to old behavior.
6. **Great docs** - real examples, an error catalog, a changelog, a way to try it.
7. **Don't leak your database** - a deliberate mapping layer between storage and contract.

Every item is the same move from Phase 1, applied early: decide what you can promise, make it
something you can keep, and keep it. Do that on day one, and the breaking changes you spent two
phases learning to survive mostly never have to happen.

## Recap

1. **Consistency is a longevity feature** - uniform resource and error shapes mean a client learns
   your API once and you can grow it without surprises.
2. **Paginate from day one** - adding pagination later is a silent breaking change.
3. **Idempotency keys** turn dangerous retries into safe ones on create/charge endpoints.
4. **Rate limits** keep a shared API healthy, but only if you *communicate* them with headers and `429`.
5. **Sensible defaults** make the naive call safe and let new options stay backward compatible.
6. **Docs are the API** to the person integrating - examples, error catalog, changelog, a way to try it.
7. **Never serialize your database rows straight out** - a mapping layer keeps internal refactors from
   leaking as breaking changes.
8. **Auth is its own topic** - design durable shapes now; treat security as a first-class subject later.

Watch it animated: [idempotency keys](/explainers/IdempotencyKeys.dc.html)
