Why Your API Breaks When Things Get Real
Most APIs work in staging and crumble in production when networks stutter, clients retry, and error responses become unparseable blobs. The gap between demo-ready and production-grade isn't architectural philosophy — it's two patterns that should be table stakes but rarely are: idempotency keys and structured error responses.
These aren't exotic. Stripe has used idempotency keys since at least 2014. GitHub's API has returned structured errors for over a decade. Yet in 2026, when AI agents make automated, retry-heavy API calls at scale, these patterns have gone from nice-to-have to critical.
Idempotency Keys: The Simple Fix for Duplicate Disasters
Here's the scenario: a client sends a POST /charges request. The server processes the charge, but the response never arrives — network timeout, client crash, whatever. The client has no idea whether the charge went through. So it retries. Without safeguards, you've just charged the customer twice.
Idempotency keys solve this elegantly. The client sends a unique identifier — typically a UUID v4 — in an Idempotency-Key header with every state-changing request. The server caches the response against that key. If the same key arrives again, the server returns the cached result without re-executing the operation.
# Client request with idempotency key
POST /v1/charges HTTP/1.1
Host: api.example.com
Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Content-Type: application/json
{
"amount": 2000,
"currency": "usd",
"source": "tok_visa"
}On the server side, the implementation is straightforward. Before executing any side effect, check whether the idempotency key exists in your cache. If it does, return the stored response. If not, execute the operation, cache the result, and return it.
async function chargeHandler(req) {
const key = req.headers['idempotency-key'];
const cached = await redis.get(`idempotency:${key}`);
if (cached) {
return JSON.parse(cached); // Safe retry — return cached result
}
const result = await processCharge(req.body);
await redis.set(`idempotency:${key}`, JSON.stringify(result), 'EX', 86400);
return result;
}Stripe stores idempotency key results for a minimum of 24 hours. That window is arbitrary but practical — long enough to handle retries and incidents, short enough to avoid unbounded storage growth. The IETF is currently standardizing the header in draft-ietf-httpapi-idempotency-key-header.
When to Require Idempotency Keys
Apply idempotency keys to any endpoint that produces side effects: POST requests creating resources (orders, charges, registrations), PUT requests updating resources (profile changes, configuration updates), and DELETE requests (though DELETE is semantically idempotent, caching the response prevents race conditions). Don't bother with GET requests — they're read-only by definition. The key insight is that idempotency isn't about HTTP methods; it's about side effects. If your endpoint changes state, it needs idempotency.
RFC 9457 Problem Details: Errors That Machines Can Actually Read
Every API consumer has hit this: the error shape changes between endpoints, between teams, between versions. Sometimes it's {"error": "Not found"}. Sometimes it's {"message": "Error", "code": 404}. Sometimes it's HTML. Clients can't write reliable error-handling logic against a moving target.
RFC 9457, published in July 2023 and obsoleting RFC 7807, defines a standard JSON error format. The media type is application/problem+json, and it includes five fields:
- type — a URI identifying the error class, serving as a stable documentation anchor
- title — a short human-readable summary
- status — the HTTP status code
- detail — a human-readable explanation for this specific occurrence
- instance — a URI for this specific error instance
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/insufficient-funds",
"title": "Insufficient Funds",
"status": 422,
"detail": "Account balance $12.50 is below the required $50.00 minimum.",
"instance": "/transactions/trans_a1b2c3d4"
}The type URI is the clever part. It gives every error class a stable address that clients can use for conditional logic and that documentation can link to directly. It's versioning for your error taxonomy.
Extension Members for Domain Context
RFC 9457 explicitly allows additional fields beyond the five standard ones. This is where you add domain-specific context that clients actually need:
{
"type": "https://api.example.com/problems/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "The request body contains invalid fields.",
"instance": "/users/create/req_xyz",
"invalid_fields": [
{ "field": "email", "reason": "not a valid email address" },
{ "field": "age", "reason": "must be between 18 and 120" }
]
}This is the difference between an error that tells a developer "something's wrong" and one that tells them exactly what to fix. For AI agents parsing responses, the structured format means they can reliably extract and act on error information without brittle string matching.
Why These Patterns Matter Even More in 2026
The AI agent era has quietly changed API design requirements. Agents don't just consume APIs — they retry them aggressively when things go wrong. They parse responses programmatically. They chain multiple API calls into workflows. A flaky API that humans can tolerate becomes a workflow-breaking bug when agents depend on it.
Idempotency keys give agents safe retry semantics. They can retry on any failure — timeout, 5xx, network error — without fear of duplicate side effects. Structured error responses give agents machine-readable feedback about what went wrong, enabling them to make intelligent decisions: retry, fallback, or abort.
Incremental Adoption
You don't need to retrofit your entire API overnight. Start with your most critical endpoints — the ones handling payments, data mutations, or external integrations. Add idempotency keys to those first. Switch their error responses to RFC 9457 format. Then expand outward.
Each pattern can be implemented one endpoint at a time. Both are backward-compatible. Existing clients that ignore the Idempotency-Key header continue working. Clients that parse application/problem+json get structured errors; everyone else still gets readable JSON.
The companies whose APIs developers love — Stripe, GitHub, Twilio — didn't get there by accident. They got there by implementing boring, unglamorous patterns that prevent real failures. Idempotency keys and structured errors are exactly that boring. That's the point.
Comments