Field Note 67current
The API I'd build today
What a new API needs before the first endpoint: operations kept apart, idempotency keys that replay the response, state tokens, DPoP, quota in requests and in work, regions, an archive. Cheap to decide before you have callers, a migration afterwards. Specified in ZBP-7.
My Personal LLM Policy: Extract, not generate
The thinking is mine, whether it’s years old or from this week. What a model does is get it out of my head and onto the page — writing time I’d otherwise never spend, not substance I didn’t have. I read every line, I can defend any sentence, and the errors are mine: the same bar I hold everything here to, model or no model.
TL;DR
Before the first endpoint of a new API ships, decide the concerns every endpoint will share. As of today that list is:
- Four kinds of operation (queries, mutations, events, and long-running operations) kept apart, because they have different guarantees and collapsing them produces a mutation nobody can safely retry and a wait that means polling.
- An idempotency key that replays the stored response, not one that merely suppresses a second execution.
- A state token on every response, so read-your-writes is a property of the protocol rather than of your database.
- Sender-constrained credentials, and a principal chain - support access, impersonation, and agents acting for users are one mechanism, not three flags.
- Quota in two currencies, requests and work, with cost estimated before execution and measured after.
- Regional endpoints, with anycast used for discovery rather than in the serving path, and failover treated as an entirely new session.
- A request id on every response and an archive of every call, written off the critical path.
- Typed, prefixed, opaque identifiers - the one thing here you genuinely cannot change later.
- A schema that is introspectable, permissioned per field, extensible by customers, and evolved by expand/migrate/contract - plus a test mode, and generated clients, without which none of the above reaches a caller.
These are not the most important properties an API can have. They are the ones where deciding late is categorically worse than deciding early, which is why they belong in one list and on day one. The normative version is ZBP-7Blueprint · v1.0.0ZBP-7 — The cross-cutting contract of an API surfaceYou are designing a new API surface, or retrofitting a cross-cutting concern — idempotency, quota, regions, delegation, versioning — onto one that already has callers you cannot break.Why it's cited here: The normative version of this note. Everything argued here as a position is specified there as numbered requirements, with wire formats, algorithms, test vectors and a conformance checklist.Open ZBP-7 → (requirements, wire formats, algorithms, test vectors, conformance checklist). This is the argument for bothering.
Context
An idempotency key costs almost nothing to design before the API has any callers. Afterwards it is a coordinated migration across every SDK, every customer integration, and every internal service that already talks to you. Same decision, wildly different price.
That holds for every concern in this note, and it is the only reason they belong in one list. It also carries an awkward consequence: you have to settle them before there is any evidence you need them, which is precisely why they get skipped.
The failure mode is never a team deciding against one of these. It’s a team never reaching the decision. Idempotency arrives on the three endpoints that caused an incident. Rate limiting arrives the week a customer’s retry loop saturates a shard. Regions arrive with the first contract that has a residency clause, at which point the hostname is in ten thousand config files. Each retrofit is individually survivable and collectively the reason a five-year-old API has four ways to paginate.
This isn’t a demand to build all of it before launch. Most items are a day-one shape with a trivial implementation behind it: a scope registry can be a YAML file, a request archive one JSON object per line in a bucket, a regional partition a single region. What costs you isn’t the machinery, it’s not having the concept - adding a second region is work, but adding the idea of a region to an API that assumed one is a different order of problem.
Nor is it original. Nearly every line is an existing note applied to one surface, or a standard I’d rather adopt than relitigate (ZFN-30Field Note · currentZFN-30 — Use the standard; don't reinvent the protocolWhen a standard exists for a common or complex problem, use it — don't reinvent the protocol. Standards encode huge adversarial expertise, especially in auth and crypto; a partial implementation beats rolling your own. You're not that special, and your problem isn't either.Why it's cited here: Why almost every mechanism here is an existing header field or token format rather than a private invention.Open ZFN-30 →, ZFN-45Field Note · currentZFN-45 — Read the standards; better yet, help write themLearn to read standards docs — RFCs, W3C recs — fluently; they're the primary source, not a last resort. Even better, get involved: reading them well makes you a sharper builder, and helping write them is the best protocol education there is.Why it's cited here: The habit that makes adopting a standard cheaper than inventing one, which is reading the specification before deciding it does not fit.Open ZFN-45 →).
Recommendation
Treat the cross-cutting shape of the API as the day-one deliverable, and the first endpoint as the thing you add to it.
The reason to take the list as a set rather than a menu is that the items only work together. Idempotency keys without an enumerated error taxonomy don’t make retries safe, because the client still can’t tell what to retry (ZFN-58Field Note · currentZFN-58 — Errors are part of the contractError paths are the half of your API clients depend on most, and usually the half nobody designed. Enumerate error codes in the schema like any other type: stable code, retryable-or-not, whose fault, structured params. Machines branch on codes — anyone parsing prose is broken.Why it's cited here: The other half of retry safety. An idempotency key makes a retry harmless; the error taxonomy is what tells the client whether to retry at all.Open ZFN-58 →). Quotas without a cost signal can’t be enforced against the caller who matters, because the limiter has no number until the expensive work is already done. A state token that only some endpoints return gives read-your-writes usually, which is the intermittent, unreproducible bug it existed to eliminate (ZFN-25Field Note · currentZFN-25 — Track the version a client has seen for read-your-writesFor read-your-writes across backends, track the latest version a client has seen — a token or vector clock. Return it on write; reads then go to a backend at least that fresh. Hold it client-side (a token they present) or server-side (a gateway tracks the session and routes).Why it's cited here: Where the state-token argument is made properly, including why the client is the right place to hold it.Open ZFN-25 →). Partial adoption of most of these is worse than none, because none is at least honest about what it doesn’t give you.
The linchpin is that the client is part of the implementation. Reusing an idempotency key across retries, echoing the state token, honouring a retry budget, staying in-region, treating failover as a new session: every one of these is caller-side behaviour. A specification that four hand-written SDKs implement independently is four dialects, three of them subtly wrong and none of them wrong in the same way. That is why generating the clients from the schema is the requirement that makes the rest of them real (ZFN-14Field Note · currentZFN-14 — Define every API with a schema, and generate the clientsDefine every API with a machine-readable schema (OpenAPI, Protobuf, GraphQL) as the source of truth, and generate clients and server stubs from it — never hand-roll request-building and JSON parsing. Hand-written clients drift and break silently; check schema compatibility in CI.Why it's cited here: The reason any of this reaches clients. A behaviour specified in a document and hand-written into four SDKs is four behaviours.Open ZFN-14 →, ZFN-51Field Note · currentZFN-51 — Design the request envelope before the first endpointAuth, idempotency keys, trace IDs, vector clocks: context about a request, not part of it. Define an envelope alongside the payload in the schema on day one — transport-native, per-hop vs propagated, owned by generated SDKs and middleware. The retrofit is what costs you.Why it's cited here: The layer everything on this list rides on. That note argues for defining the envelope before the first endpoint; this one is the inventory of what ends up in it.Open ZFN-51 →).
Everything else is specified in ZBP-7Blueprint · v1.0.0ZBP-7 — The cross-cutting contract of an API surfaceYou are designing a new API surface, or retrofitting a cross-cutting concern — idempotency, quota, regions, delegation, versioning — onto one that already has callers you cannot break.Why it's cited here: The normative version of this note. Everything argued here as a position is specified there as numbered requirements, with wire formats, algorithms, test vectors and a conformance checklist.Open ZBP-7 →: what an idempotency record stores, how a fingerprint is computed, what a discovery response contains, how admission control reconciles an estimate against a measurement, and the hundred-odd numbered requirements that make those testable. Hand that to whoever is building it.
Consequences
Easier:
- Client retries become correct rather than hopeful. A key that replays a stored response, a taxonomy that says whether to retry, and a budget that stops the storm are one mechanism from the caller’s side.
- Incidents get shorter, because “what did they actually send?” is a query against the archive rather than an appeal to a sampled log.
- The security review has a surface to review. A scope registry, a delegation chain, and one serialisation-time visibility filter are auditable; the same decisions spread across handlers aren’t.
- Growth is mostly additive - regions, scopes, customer fields, and new operations slot into concepts that already exist.
Harder:
- Day one is materially longer. None of this ships an endpoint, and all of it is work you do before the first demo. That is why it gets skipped, and exactly why it is cheapest then.
- You’re running stateful infrastructure you’d rather not. Idempotency records, state tokens, quota counters, and an archive are all storage with lifecycle, cost, and failure modes of their own.
- Per-tenant schema is a real subsystem, and every part of it (validation, indexing, introspection, documentation) is harder than its static form.
- Explicit failover looks worse than transparent failover, right up to the first time transparent failover silently serves a stale write.
New obligations:
- The scope registry and the cost estimates have to stay true. Both rot silently, and both are discovered to be wrong by someone outside the team.
- The archive is a data-protection surface. Retention, redaction, access control, and erasure apply to it exactly as to the primary store (ZFN-57Field Note · currentZFN-57 — Deletion is a feature: design it on day oneA deleted_at column is not deletion. Real deletion is a workflow with an SLA: it must reach every replica, projection, index, cache, log, and backup — and you must prove it ran. Partition by owner, propagate tombstones on the event rails, crypto-shred what you can't rewrite.Why it's cited here: The collision the request archive creates. Once you keep every request body for a year, an erasure request has a second, harder place to reach.Open ZFN-57 →).
- The list has an expiry date. It’s the current answer, not a permanent one, and it should be re-read against the standards that exist when you read it.
References
- ZBP-7Blueprint · v1.0.0ZBP-7 — The cross-cutting contract of an API surfaceYou are designing a new API surface, or retrofitting a cross-cutting concern — idempotency, quota, regions, delegation, versioning — onto one that already has callers you cannot break.Why it's cited here: The normative version of this note. Everything argued here as a position is specified there as numbered requirements, with wire formats, algorithms, test vectors and a conformance checklist.Open ZBP-7 → - the specification this note argues for.
- ZFN-51Field Note · currentZFN-51 — Design the request envelope before the first endpointAuth, idempotency keys, trace IDs, vector clocks: context about a request, not part of it. Define an envelope alongside the payload in the schema on day one — transport-native, per-hop vs propagated, owned by generated SDKs and middleware. The retrofit is what costs you.Why it's cited here: The layer everything on this list rides on. That note argues for defining the envelope before the first endpoint; this one is the inventory of what ends up in it.Open ZFN-51 → and ZFN-14Field Note · currentZFN-14 — Define every API with a schema, and generate the clientsDefine every API with a machine-readable schema (OpenAPI, Protobuf, GraphQL) as the source of truth, and generate clients and server stubs from it — never hand-roll request-building and JSON parsing. Hand-written clients drift and break silently; check schema compatibility in CI.Why it's cited here: The reason any of this reaches clients. A behaviour specified in a document and hand-written into four SDKs is four behaviours.Open ZFN-14 → - the envelope, and the generated clients that turn any of this from a document into a behaviour.
- ZFN-25Field Note · currentZFN-25 — Track the version a client has seen for read-your-writesFor read-your-writes across backends, track the latest version a client has seen — a token or vector clock. Return it on write; reads then go to a backend at least that fresh. Hold it client-side (a token they present) or server-side (a gateway tracks the session and routes).Why it's cited here: Where the state-token argument is made properly, including why the client is the right place to hold it.Open ZFN-25 →, ZFN-58Field Note · currentZFN-58 — Errors are part of the contractError paths are the half of your API clients depend on most, and usually the half nobody designed. Enumerate error codes in the schema like any other type: stable code, retryable-or-not, whose fault, structured params. Machines branch on codes — anyone parsing prose is broken.Why it's cited here: The other half of retry safety. An idempotency key makes a retry harmless; the error taxonomy is what tells the client whether to retry at all.Open ZFN-58 → and ZFN-61Field Note · currentZFN-61 — Propagate the deadlineEvery request has a deadline whether you set one or not — the caller's patience. Make it explicit at the edge, carry it as remaining budget on every hop, check it before expensive steps, and cancel downstream when it dies. Work past the deadline is the fuel of cascading collapse.Why it's cited here: The deadline half of the envelope, and why a cancelled call is not the same as a call that didn't happen.Open ZFN-61 → - state tokens, errors, and deadlines.
- ZFN-18Field Note · currentZFN-18 — Enforce a quota at ingress on every endpoint — even unabused onesPut a quota on every endpoint and enforce it at ingress from day one — per tenant, principal, IP — even for endpoints nobody abuses yet. Unlimited-by-default means the first runaway client or compromised key is an outage. Return 429 + Retry-After; retrofitting limits is painful.Why it's cited here: Why the limiter belongs at the edge rather than in each service, which is what makes a second quota currency practical at all.Open ZFN-18 →, ZFN-13Field Note · currentZFN-13 — Fail fast and push back: retries, load shedding, and flow controlBuild client retries (backoff, jitter, Retry-After) from day one. Under overload, shed fast and push the failure back to the source to retry — don't retry internally and amplify it. Flow-control everywhere, bound every queue, and don't take more work than you can finish in time.Why it's cited here: What a quota system degrades into when its limits are wrong or missing, and why a retry budget is a client-side control.Open ZFN-13 → and ZFN-53Field Note · currentZFN-53 — Make abuse cost money: attack the unit economics, not the identityAbuse at scale is a business with a P&L. Detection is an arms race you eventually lose, because the attacker gets unlimited free queries against your classifier. Instead find the metered input they can't substitute away from, and inflate it — per attempt, dialled by risk.Why it's cited here: The reasoning behind charging for work rather than requests, so the expensive path is expensive for whoever chose it.Open ZFN-53 → - quotas, shedding, and making abuse expensive.
- ZFN-56Field Note · currentZFN-56 — IDs are an interface: prefix the type, randomise the bodyAn ID is read by more than your database: humans in logs, machines at boundaries, adversaries probing. Serve all three — a type prefix so IDs self-describe and misuse fails at parse time, a random body so nothing leaks or enumerates, time-ordered only when the index needs it.Why it's cited here: The full argument for identifiers that carry their type and reveal nothing, including why the format can never be changed.Open ZFN-56 →, ZFN-65Field Note · currentZFN-65 — Journal the write, apply it in micro-batchesIf nothing the caller does next depends on a write, it does not belong in the request path: append it to a durable ordered journal and apply it in micro-batches. The bill is ordering. Sequence comes from the journal, never the clock, and never two paths to one row.Why it's cited here: Why the archive is written as a buffered stream of batches rather than a row per request.Open ZFN-65 → and ZFN-57Field Note · currentZFN-57 — Deletion is a feature: design it on day oneA deleted_at column is not deletion. Real deletion is a workflow with an SLA: it must reach every replica, projection, index, cache, log, and backup — and you must prove it ran. Partition by owner, propagate tombstones on the event rails, crypto-shred what you can't rewrite.Why it's cited here: The collision the request archive creates. Once you keep every request body for a year, an erasure request has a second, harder place to reach.Open ZFN-57 → - identifiers, how the archive is written, and what it obliges you to delete.
Changelog
- 2026-08-30: First published as a Field Note.
- 2026-08-30: Amended - anycast belongs in endpoint discovery, not the serving path (with the lookup piggybacked on authentication); added client version negotiation and upgrade signalling.
- 2026-08-30: Amended - long-running operations need a wait that isn’t a poll loop, streamed progress events, and a state machine declared per operation type.
- 2026-08-30: Amended - added typed and prefixed identifiers, batch and partial-failure semantics, a request id on every response, and test mode as a day-one path; plus unknown-value tolerance, overlapping credential validity, and naming the tenant explicitly on the request.
- 2026-08-30: The normative specification moved to ZBP-7Blueprint · v1.0.0ZBP-7 — The cross-cutting contract of an API surfaceYou are designing a new API surface, or retrofitting a cross-cutting concern — idempotency, quota, regions, delegation, versioning — onto one that already has callers you cannot break.Why it's cited here: The normative version of this note. Everything argued here as a position is specified there as numbered requirements, with wire formats, algorithms, test vectors and a conformance checklist.Open ZBP-7 →, which carries the requirements, wire formats, algorithms, test vectors and conformance checklist. This note keeps the argument for deciding them together and early.