🌱http.api.design

paste to any AI agent

assumes

- you're designing or revising a JSON-over-HTTP API in REST's neighborhood (verb-based, OpenAPI-describable). gRPC, WebSockets, GraphQL, and MQTT have different design constraints; this seed isn't load-bearing for those. - familiarity with HTTP fundamentals (verbs, status codes, idempotency, content negotiation) is assumed. the seed talks about how to use them well, not about what they are. - you have a specific use case in mind. the seed isn't a general API encyclopedia — it'll help you make informed choices for the API you're building, not survey the whole space. HTTP API design context for agents. The stable semantics clients depend on, the systematic mistakes agents make, and what's changing as AI enters the loop.

view raw
# Usage: curl -sSL https://seed.show/http.api.design | bash -s <install-path>
# <install-path> is the directory where the file should land.

set -euo pipefail
# seed.show — assumption contract for http.api.design
# (printed to stderr before unpack so the agent sees it in tool output)
cat >&2 <<'ASSUMPTIONS_EOF'
- you're designing or revising a JSON-over-HTTP API in REST's neighborhood (verb-based, OpenAPI-describable). gRPC, WebSockets, GraphQL, and MQTT have different design constraints; this seed isn't load-bearing for those.
- familiarity with HTTP fundamentals (verbs, status codes, idempotency, content negotiation) is assumed. the seed talks about how to use them well, not about what they are.
- you have a specific use case in mind. the seed isn't a general API encyclopedia — it'll help you make informed choices for the API you're building, not survey the whole space.

HTTP API design context for agents. The stable semantics clients depend on, the systematic mistakes agents make, and what's changing as AI enters the loop.

ASSUMPTIONS_EOF

[ -z "${1:-}" ] && {
  echo "install requires a path: curl -sSL https://seed.show/http.api.design | bash -s <install-path>" >&2
  exit 1
}
TARGET="$1"
mkdir -p "$TARGET"
DEST="$TARGET/seed-fold.hEoea2.folded.md"

cat > "$DEST" <<'PORTDOWN_A1A73FC2'
<!--
This is a `.folded.md` archive — a directory packed into one markdown
file. The markers below are load-bearing; don't edit them directly.

To unpack (requires bash — if you have no shell, tell the user):
  1. curl -sSLf https://fold.dom.vin/skill | bash -s <INSTALL_DIR>
  2. <INSTALL_DIR>/fold/scripts/unfold <this-file>
     (or: unfold <this-file>  if fold/scripts is on your PATH)
-->

---
fold: true
marker: d1af56
at: 2026-05-07T16:16:11Z
root: seed-pack.xzpkwz
---

<!--fold:d1af56@file path="README.md" mode="644"-->
# http.api.design


## Assumptions

- you're designing or revising a JSON-over-HTTP API in REST's neighborhood (verb-based, OpenAPI-describable). gRPC, WebSockets, GraphQL, and MQTT have different design constraints; this seed isn't load-bearing for those.
- familiarity with HTTP fundamentals (verbs, status codes, idempotency, content negotiation) is assumed. the seed talks about how to use them well, not about what they are.
- you have a specific use case in mind. the seed isn't a general API encyclopedia — it'll help you make informed choices for the API you're building, not survey the whole space.

HTTP API design context for agents. The stable semantics clients depend on, the systematic mistakes agents make, and what's changing as AI enters the loop.

## What level to work at

HTTP method semantics, status code contracts, and REST constraints are stable — defined in RFC 9110 (2022) and Fielding's dissertation. OpenAPI versions change; fetch sources.md for current normative text. This bundle covers what agents consistently mis-apply. patterns.md covers the common design decisions (resource naming, versioning, pagination, error bodies, bulk ops, long-running ops).

## What NOT to do: the systematic mistakes

Agents designing APIs make the same mistakes in the same places. Know these before generating a single endpoint.

**Wrong status codes — the four most common errors:**

| Situation | Wrong | Correct | Why |
|---|---|---|---|
| Resource created | `200 OK` | `201 Created` + `Location` header | Clients need the location of what was created |
| Input fails validation | `400 Bad Request` | `422 Unprocessable Entity` | 400 = malformed syntax; 422 = syntactically valid, semantically wrong |
| Unauthenticated request | `403 Forbidden` | `401 Unauthorized` + `WWW-Authenticate` | 401 tells the client to authenticate; 403 says credentials are valid but access denied |
| Resource doesn't exist for this user | `404 Not Found` | `403 Forbidden` | Returning 404 leaks existence; 403 hides it |

**Non-idempotent PUT:**
`PUT /resources/123` with a partial body that merges with existing state is wrong — that's `PATCH`. PUT must replace the full resource. Calling PUT ten times must produce the same state as calling it once.

**Missing `Location` header on 201:**
`201 Created` without `Location` forces clients to make a second request to find what was created. Always return `Location: /resources/<new-id>`.

**Generic error bodies:**
```json
// Wrong — client cannot branch on error type without parsing human text
{ "error": "Insufficient funds in account" }

// Correct — RFC 7807 Problem Details
{
  "type": "https://example.com/errors/insufficient-funds",
  "title": "Insufficient funds",
  "status": 402,
  "detail": "Account balance $10.00 is below the required $25.00.",
  "instance": "/accounts/abc123/transactions/xyz"
}
```
`type` is a URI clients branch on programmatically. `detail` is human-readable and occurrence-specific. `title` is the stable human-readable label for the error class.

**PATCH semantics:**
PATCH sends only the fields to change; it is NOT idempotent by spec (though a well-designed PATCH often is in practice). PUT replaces the entire resource. Sending a partial body to PUT is a design bug — use PATCH.

## HTTP as a constraint system

REST is not a style guide. It is a set of architectural constraints that enable client-server interoperability at scale. Violating a constraint doesn't produce a style violation — it produces broken clients.

The six REST constraints that matter for API design:

**1. Uniform interface — the one that matters most.** Resources are identified by URIs. Representation (JSON) is separate from the resource itself. Messages are self-descriptive: method + URI + headers tell a client everything it needs without out-of-band knowledge. HATEOAS (hypermedia as the engine of application state) is the constraint teams routinely skip — clients should be able to navigate the API from a single entry point via links. Skip HATEOAS pragmatically, but understand it before skipping it.

**2. Stateless.** Each request from client to server must contain all information needed to process it. No server-side session state. This is why JWTs travel in every request header rather than being stored server-side.

**3. Cacheable.** Responses must declare whether they are cacheable. `GET` and `HEAD` are safe and cacheable by default. `POST`, `PUT`, `PATCH`, `DELETE` are not. A `GET` endpoint with side effects will be cached by intermediaries — which is why `GET /emails/send` is a protocol violation, not just a style violation.

**4. Client-server.** UI and data storage are separated. The server does not need to know how the client renders; the client does not need to know how the server stores. This separation is what makes APIs reusable across clients.

**5. Layered system.** A client cannot tell whether it is connected to the origin server or an intermediary (CDN, load balancer, gateway). API design must account for this: caching headers, idempotency, and auth must work correctly through any number of layers.

**6. Code on demand (optional).** Servers can send executable code to clients. Rarely used; safely ignorable.

## The stable facts: HTTP method semantics

| Method | Safe | Idempotent | Typical use |
|---|---|---|---|
| `GET` | Yes | Yes | Read a resource or collection |
| `HEAD` | Yes | Yes | Same as GET, headers only |
| `OPTIONS` | Yes | Yes | CORS preflight, capability discovery |
| `DELETE` | No | Yes | Remove a resource |
| `PUT` | No | Yes | Replace a resource (full representation) |
| `PATCH` | No | No* | Partial update (*idempotent if designed carefully) |
| `POST` | No | No | Create a resource, trigger an action |

**Safe** means no side effects — the request cannot modify state. Servers, CDNs, and browsers all rely on this to prefetch, cache, and retry.

**Idempotent** means the same request repeated N times produces the same result as once. Network failures should result in retries for idempotent methods. `DELETE /resources/123` called twice: first call removes the resource (204), second call gets 404 or 204 depending on design — both are correct as long as the state is the same.

## The status code taxonomy

`1xx` — Informational. Rare in REST APIs.
`2xx` — Success. `200` (got it), `201` (created), `202` (accepted, processing async), `204` (done, no body).
`3xx` — Redirection. `301` (permanent), `302` (temporary), `304` (not modified, use cache).
`4xx` — Client error. Client sent a bad request; retrying without changing it will fail.
`5xx` — Server error. Retry may succeed; the fault is on the server.

The rule: `4xx` errors are the client's fault. Never return `5xx` for invalid client input.

## Eight more rules that break clients if ignored

**1. Idempotency keys for non-idempotent mutations.**
Any POST that creates a charge, sends a message, or triggers an irreversible side effect needs an idempotency key. Accept it as `Idempotency-Key: <client-uuid>`. Return the same response for duplicate keys within a time window. Stripe's implementation is the reference.

**2. ETag + If-Match for optimistic concurrency.**
Return `ETag: "<version-hash>"` on GET. Require `If-Match: "<hash>"` on PATCH/PUT. Return `412 Precondition Failed` if the resource changed since the client's fetch. Prevents last-write-wins data loss in concurrent edits.

**3. Rate limiting requires headers.**
`429 Too Many Requests` must include `Retry-After: <seconds>`. Add `RateLimit-Remaining` and `RateLimit-Reset` (draft IETF RateLimit header standard) so clients back off before hitting the limit.

**4. Pagination returns a cursor, not just items.**
```json
{ "data": [...], "next_cursor": "opaque_string", "has_more": true }
```
Offset pagination (`?page=3`) breaks when items are inserted or deleted between requests. Cursor pagination is stable. Always include `has_more` so clients don't need to infer from an empty array.

**5. `Content-Type` must match the body.**
Sending JSON with `Content-Type: text/plain` breaks any client that checks. For file uploads: `multipart/form-data`. Never `application/json` with a base64-encoded file body.

**6. CORS headers are required for browser clients.**
The preflight `OPTIONS` request must return `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, and `Access-Control-Allow-Headers`. A missing CORS header looks like a network failure to browsers — the actual error is in the preflight, which developer tools hide unless you know to look.

**7. `401` requires `WWW-Authenticate`.**
RFC 9110 requires `WWW-Authenticate` on every 401 response identifying the auth scheme. Most APIs omit it; clients handle this, but it's a spec violation that can break strict clients and API gateways.

**8. `GET` endpoints must have no side effects.**
`GET /emails/send` will be cached by CDNs, prefetched by browsers, bookmarked and reloaded by users. Side-effectful operations belong on `POST`.

## What AI is changing

**AI-generated OpenAPI specs introduce new failure modes.**
LLMs generating OpenAPI often produce: schemas that don't validate the actual payloads, status codes that don't match the endpoint behavior, missing `required` fields, and `$ref` cycles. Treat AI-generated specs as a starting draft; validate with a linter (Spectral, vacuum) before publishing.

**AI in API design review.**
The most effective use: give an agent an existing API spec and ask it to check for RFC 9110 compliance, RFC 7807 error body conformance, idempotency holes, and missing pagination. Agents catch the systematic mistakes better than they design from scratch, because the failure modes are enumerable and the rules are stable.

**MCP as an alternative protocol for agent-to-agent communication.**
Model Context Protocol is an emerging standard for agent-to-API communication that sidesteps HTTP REST entirely in some contexts. Instead of designing a REST API and having an agent call it via curl, MCP exposes tools that agents call directly with structured arguments. The tradeoff: MCP tools are easier for agents to invoke correctly (no URL construction, no status code interpretation, no pagination logic); REST APIs are easier for humans to inspect, debug, and use from non-agent clients. If your primary consumer is an agent and your secondary consumer is a human, consider whether MCP reduces the surface area of mistakes. If you need one API for both humans and agents, design REST correctly and add an MCP adapter layer.

**What this means for HTTP API design choices.**
The trend toward agent consumers changes the cost structure of API design mistakes. An agent that gets a non-RFC-7807 error body will either silently discard the error or hallucinate a retry strategy. An agent that misreads a 200 with `{"success": false}` may not recognize the failure at all. The strictness that was "good practice" for human clients becomes "required for correctness" for agent clients. Design to spec.
<!--fold:d1af56@file path="patterns.md" mode="644"-->
# patterns

Common API design decisions, each with the preferred pattern and the failure modes of the alternatives.

## Resource naming

**Use nouns, not verbs.** The HTTP method is the verb.
```
/users          POST   → create a user
/users/123      GET    → read user 123
/users/123      PUT    → replace user 123
/users/123      PATCH  → partially update user 123
/users/123      DELETE → delete user 123
```

**Wrong**: `/createUser`, `/getUser/123`, `/deleteUser?id=123`

**Collections are plural.** `/users`, `/orders`, `/products`. Singular `/user` is a convention violation that breaks predictability.

**Nested resources for owned relationships, query params for filters.**
```
/users/123/orders          → orders belonging to user 123 (ownership)
/orders?user_id=123        → orders filtered by user (query)
```
Nest at most one level. `/users/123/orders/456/items/789` is a path that breaks when `items` also live outside orders.

**Actions that don't map to CRUD:** use a sub-resource verb.
```
POST /accounts/123/activate
POST /invoices/456/send
POST /jobs/789/cancel
```
Not `PUT /accounts/123` with `{ "status": "active" }` — that conflates the trigger with the state.

## Versioning strategies

Three approaches; choose one before launch and commit to it.

**URL path versioning** — `/v1/users`, `/v2/users`.
Easiest to understand, easiest to route, breaks bookmarks on version change. Use for public APIs where clients may not control headers.

**Header versioning** — `API-Version: 2024-01-15` (date-based, Stripe's approach) or `Accept: application/vnd.api+json; version=2`.
Cleaner URLs, harder to test in a browser, requires clients to set headers. Use for APIs with sophisticated clients.

**Query parameter versioning** — `/users?version=2`.
Easy to test, pollutes every endpoint's parameter space. Acceptable for internal APIs; avoid for public ones.

**Never version with no strategy.** The cost of breaking change without versioning is a coordinated migration across all clients. A bad versioning strategy beats no strategy.

**Sunset headers for deprecation.**
When deprecating a version, add to every response:
```
Sunset: Sat, 01 Jan 2026 00:00:00 GMT
Deprecation: Mon, 01 Jan 2024 00:00:00 GMT
Link: <https://api.example.com/docs/migration>; rel="successor-version"
```

## Pagination patterns

**Cursor-based (preferred for most APIs):**
```json
{
  "data": [...],
  "next_cursor": "eyJpZCI6MTIzfQ",
  "has_more": true
}
```
Client passes `?cursor=<next_cursor>` on the next request. Cursor is opaque (base64-encoded pointer to the last item). Stable across insertions and deletions. Cannot skip pages; must page forward sequentially.

**Offset-based (acceptable for small, stable datasets):**
```json
{
  "data": [...],
  "total": 1000,
  "page": 3,
  "per_page": 20
}
```
`?page=3&per_page=20`. Breaks when items are inserted mid-page. `total` is expensive to compute on large datasets. Use only when clients need to jump to arbitrary pages.

**Keyset-based (high-performance alternative to offset):**
`?after_id=123&limit=20`. Uses the primary key of the last item seen. Fast (uses index), stable, but cannot skip pages. Preferred for time-ordered feeds.

**Always set a maximum page size.** Never allow `?limit=99999`. Default to 20–50; cap at 100–500 depending on object size.

## Error body format (RFC 9457 Problem Details)

The standard. Use it everywhere. Return `Content-Type: application/problem+json`.

```json
{
  "type": "https://api.example.com/errors/validation-failed",
  "title": "Validation failed",
  "status": 422,
  "detail": "The request body contains 2 validation errors.",
  "instance": "/orders/create#request-id-abc123",
  "errors": [
    {
      "field": "email",
      "message": "Must be a valid email address",
      "received": "not-an-email"
    },
    {
      "field": "amount",
      "message": "Must be greater than 0",
      "received": -5
    }
  ]
}
```

`type` — a URI uniquely identifying the error class. Can be a docs page. Clients branch on this.
`title` — stable human-readable name for the error class. Do not vary per occurrence.
`status` — the HTTP status code, mirrored in the body for clients that lose the outer envelope.
`detail` — occurrence-specific human-readable explanation. May vary.
`instance` — a URI identifying this specific occurrence (for correlation with logs).

Extensions (`errors` above) are legal. Add fields; never remove `type`, `title`, `status`.

## Filtering and sorting

**Filtering via query parameters:**
```
GET /orders?status=pending&created_after=2024-01-01
GET /products?category=electronics&min_price=100&max_price=500
```
Use `snake_case` parameter names. Use ISO 8601 for dates. For multi-value filters: `?tag=a&tag=b` (repeated key) or `?tags=a,b` (comma-separated). Pick one and document it.

**Range operators:** `?amount_gte=100&amount_lte=500`. The `_gte`/`_lte` suffix convention is readable without being terse.

**Sorting:**
```
GET /orders?sort=created_at&order=desc
GET /products?sort=-price,+name   # prefix notation: minus=desc, plus=asc
```
Default sort must be documented. Omitting sort from the default breaks clients that assume stable ordering.

**Full-text search:** separate from filtering. Use `?q=search+terms`. Never conflate `?q=` with structured filters.

**Sparse fieldsets:** `?fields=id,name,email`. Reduces payload size for large objects. Only include if your objects are actually large enough to warrant it.

## Bulk operations

**Batch create:** `POST /users/batch` with `{ "items": [...] }`. Return `207 Multi-Status` with per-item results:
```json
{
  "results": [
    { "index": 0, "status": 201, "id": "u_123" },
    { "index": 1, "status": 422, "error": { "type": "...", "title": "Validation failed" } }
  ]
}
```
207 is the correct code for mixed success/failure responses. Never return 200 when some items failed.

**Batch update:** `PATCH /users/batch` with `{ "items": [{ "id": "u_123", "name": "New Name" }] }`.

**Bulk delete:** `DELETE /users?ids=123,456,789` or `DELETE /users/batch` with body `{ "ids": [...] }`. Note: DELETE with a body is technically allowed by HTTP but some clients and proxies drop it. Query params are safer.

**Cap batch size.** Document the maximum. Return `400` with an explanation if the cap is exceeded — not a silent truncation.

## Long-running operations

For operations that take more than ~2 seconds: return `202 Accepted` immediately and poll for status.

**Pattern:**
```
POST /exports                         → 202 Accepted
                                         Location: /exports/job_abc123

GET /exports/job_abc123               → 200 OK
                                         { "status": "pending", "created_at": "..." }

GET /exports/job_abc123               → 200 OK
                                         { "status": "running", "progress": 0.45 }

GET /exports/job_abc123               → 200 OK
                                         { "status": "complete",
                                           "result_url": "https://...",
                                           "expires_at": "..." }
```

`status` values: `pending`, `running`, `complete`, `failed`. Never invent new status strings — clients branch on them.

On failure, return the full RFC 9457 error in the job body, not just `{ "status": "failed" }`.

**Webhook alternative:** for clients that cannot poll, send a webhook POST to a registered callback URL when the job completes. Webhooks require signed payloads (HMAC-SHA256 signature in `X-Signature` header) and idempotency handling on the receiver side.

**Prefer synchronous APIs unless you have evidence they're too slow.** The async pattern adds complexity on both sides. Measure first.
<!--fold:d1af56@file path="sources.md" mode="644"-->
# sources

Fetch these at task time. Ordered by importance.

1. HTTP semantics (RFC 9110, 2022) — the normative spec for status codes, methods, content negotiation, authentication, caching:
   https://httpwg.org/specs/rfc9110.html

2. RFC 9457 — Problem Details for HTTP APIs (supersedes RFC 7807):
   https://www.rfc-editor.org/rfc/rfc9457

3. OpenAPI 3.1 specification — describing APIs, schema definitions, response objects, security schemes:
   https://spec.openapis.org/oas/v3.1.0

4. REST architectural constraints (Fielding dissertation, chapter 5):
   https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm

5. HTTP status code registry (IANA) — authoritative list with references:
   https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml

6. RFC 6585 — Additional HTTP Status Codes (429 Too Many Requests, 511 Network Authentication Required):
   https://www.rfc-editor.org/rfc/rfc6585

7. IETF RateLimit Header Fields draft — standard headers for rate limiting:
   https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/

8. Model Context Protocol specification — for agent-to-agent communication patterns:
   https://modelcontextprotocol.io/specification
<!--fold:d1af56@end-->
PORTDOWN_A1A73FC2

# ── post ──
MARKER=$(awk '/^---$/ { f++; if (f==2) exit; next } f==1 && /^marker:[[:space:]]/ { sub(/^marker:[[:space:]]+/, ""); print; exit }' "$DEST")
[ -z "$MARKER" ] && { echo "seed: archive has no marker — corrupt" >&2; exit 1; }
awk -v m="$MARKER" -v outdir="$TARGET" '
  BEGIN {
    # Match <!--fold:<m>@file path="X"--> with an optional mode attr after
    # the path (fold emits  mode="644"  on executables).
    file_re = "^<!--fold:" m "@file path=\"([^\"]+)\"( mode=\"[0-9]+\")?-->$"
    end_re  = "^<!--fold:" m "@end-->$"
  }
  $0 ~ end_re { if (current) close(current); exit }
  $0 ~ file_re {
    if (current) close(current)
    line = $0
    sub(/^<!--fold:[^@]+@file path="/, "", line); sub(/".*$/, "", line)
    current = outdir "/" line
    dir = current; sub(/\/[^\/]*$/, "", dir)
    if (dir != current) system("mkdir -p \"" dir "\"")
    printf "" > current
    next
  }
  current { print >> current }
' "$DEST"
SEED_EXTRACTED=$(find "$TARGET" -type f -not -path "$DEST" 2>/dev/null | wc -l)
if [ "$SEED_EXTRACTED" = "0" ]; then
  echo "seed: archive contained no files — refusing to delete the source" >&2
  echo "  archive preserved at: $DEST" >&2
  exit 1
fi
rm -f "$DEST"

echo "" >&2
echo "✓ seed unpacked → $TARGET ($SEED_EXTRACTED files)" >&2
find "$TARGET" -type f | sort | while IFS= read -r _sf; do
  echo "  ${_sf#${TARGET}/}" >&2
done
echo "" >&2
if [ -f "$TARGET/SKILL.md" ]; then
  echo "This seed contains a skill (SKILL.md). Install it in your agent's skills directory." >&2
  echo "" >&2
fi
echo "Install the seed skill if not already installed:" >&2
echo "  https://seed.show/skill" >&2
echo "" >&2
echo "Publisher prompt:" >&2
sed 's/^/  /' >&2 <<'__SEED_PROMPT_END_AC1F2B__'
You have the HTTP API design context. Read README.md for the mental model: HTTP as a constraint system, what agents get wrong with status codes and idempotency, and what AI is changing. Read patterns.md for the common design decisions: resource naming, versioning, pagination, error bodies (RFC 9457), bulk ops, long-running operations. Fetch sources.md for normative specs when you need exact RFC text. Then ask what API to design. To share your own context bundles: seed.show/skill
__SEED_PROMPT_END_AC1F2B__
exit 0

instructions

You have the HTTP API design context. Read README.md for the mental model: HTTP as a constraint system, what agents get wrong with status codes and idempotency, and what AI is changing. Read patterns.md for the common design decisions: resource naming, versioning, pagination, error bodies (RFC 9457), bulk ops, long-running operations. Fetch sources.md for normative specs when you need exact RFC text. Then ask what API to design. To share your own context bundles: seed.show/skill

idhttp.api.design size23.6 KB created2026-05-06 expirespermanent