A Field Guide

APIs

The connective tissue of modern software — how they actually work, how to use them well, and how to build them well.

🔌 Part 1–3: Fundamentals
📡 Part 4–5: Protocol
🔑 Part 6: Authentication
🏗️ Part 7–8: Design
⚙️ Part 9–10: Operations
🌐 Part 11–12: Beyond REST
🤖 Part 15: APIs & AI

Who This Is For

Developers and technical builders who use APIs regularly but want a deeper model — not just how to call endpoints, but why they're designed the way they are, what the tradeoffs are, and how to avoid the mistakes that hurt later.

This is a 45-minute read covering consuming APIs, building APIs, and the newer territory of AI APIs and tool protocols like MCP.

Part One
The Mental Model
What an API actually is — the contract model that makes everything click.

What an API Actually Is

API stands for Application Programming Interface. That's technically accurate and entirely unhelpful for building intuition. Here's a better framing:

An API is a contract. One piece of software says: "If you send me a request in this specific format, I will send you back a response in this specific format." The contract defines what requests are valid, what responses will look like, and what errors mean. The internals — how the database is structured, what language the server is written in, where it's deployed — are hidden behind the contract.

This contract model is what makes APIs powerful. GitHub's API doesn't care whether you're calling it from Python or JavaScript or a bash script. Stripe's API doesn't care whether your checkout is built in React or server-rendered PHP. The contract is the stable interface; the implementations on both sides can change independently.

The Client-Server Model

Every API interaction involves at least two parties: a client making a request and a server returning a response. The client initiates, the server responds. The client doesn't need to know anything about the server's implementation. The server doesn't need to know anything about the client's implementation. They only need to agree on the contract.

Requests and Responses

Every HTTP API interaction involves:

Request: a method (GET, POST, PUT, DELETE), a URL identifying the resource, headers with metadata (authentication, content type), and optionally a body with data.

Response: a status code indicating what happened, headers with metadata, and optionally a body with data or error information.

# Request
GET /api/users/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Accept: application/json
# Response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 42,
  "name": "Alice Chen",
  "email": "[email protected]",
  "created_at": "2024-01-15T10:30:00Z"
}

Everything you do with a REST API is a variation of this pattern.

HTTP Methods — The Verbs

HTTP methods describe the intended action. REST APIs map them to operations on resources.

GET — retrieve data. Should never modify anything. Safe to call multiple times (idempotent). Results can be cached.

POST — create something new or submit data. Not idempotent — calling it twice typically creates two records.

PUT — replace a resource entirely. Idempotent — calling it multiple times with the same data has the same effect as calling it once.

PATCH — partially update a resource. Only send the fields you want to change.

DELETE — remove a resource. Idempotent — deleting something that's already deleted is fine.

GET    /users           → list users
GET    /users/42        → get user 42
POST   /users           → create a new user
PUT    /users/42        → replace user 42 entirely
PATCH  /users/42        → update some fields on user 42
DELETE /users/42        → delete user 42

Idempotency Matters in Practice

Idempotent operations are safe to retry. If you send a PUT and the network drops before you get a response, you can retry without risk. If you retry a non-idempotent POST, you might create duplicate records.

💡 Idempotency Keys
For operations where this matters (payments especially), APIs often support an idempotency key — a client-generated unique ID you send with the request. If the server sees the same key again, it returns the original response instead of processing again. Stripe uses this pattern extensively.

URL Design — The Nouns

REST URLs identify resources. The verb is in the HTTP method. The URL is the thing you're acting on.

// Wrong
GET /getUser/42
POST /createUser
DELETE /deleteUser/42

// Right
GET /users/42
POST /users
DELETE /users/42

Collections and Hierarchy

Plural for collections, singular for specific items:

GET /products           → collection of products
GET /products/123       → specific product
GET /products/123/reviews → reviews belonging to product 123
POST /products/123/reviews → create a review for product 123

Hierarchy represents relationships. Nested paths show containment. Don't nest more than two or three levels — deep nesting becomes unreadable and often signals a design problem.

Use query parameters for filtering, sorting, and searching:

GET /products?category=electronics&min_price=100&sort=price_asc
GET /users?search=alice&page=2&per_page=25

Versioning in the URL:

GET /v1/users/42
GET /v2/users/42
💡 URL vs. Header Versioning
The alternative is header-based versioning (API-Version: 2), but URL-based versioning is more practical — it's visible, copyable, and obvious in browser history and logs.
Part Four
Status Codes & Headers
The standardized vocabulary for outcomes and metadata.

Status Codes — What Happened

HTTP status codes are a standardized vocabulary for outcomes. Using them correctly makes your API predictable and enables monitoring tools, proxies, and clients to work properly.

2xx — Success

CodeMeaning
200 OKGeneral success for GET, PUT, PATCH. Request worked, here's the data.
201 CreatedA new resource was created (successful POST). Should include a Location header.
204 No ContentSuccess, but no body. Typical for DELETE.
202 AcceptedRequest received, will be processed asynchronously.

3xx — Redirection

CodeMeaning
301Moved Permanently — clients should update their bookmarks.
304Not Modified — used with caching. Resource hasn't changed.

4xx — Client Error

CodeMeaning
400Bad Request — malformed, missing required fields, wrong data types.
401Unauthorized — authentication required or credentials invalid. (Really "unauthenticated.")
403Forbidden — authenticated but not authorized. Knows who you are, but you can't do this.
404Not Found — resource doesn't exist.
405Method Not Allowed — wrong HTTP method on this endpoint.
409Conflict — request conflicts with current state (e.g., uniqueness violation).
422Unprocessable Entity — syntactically valid, semantically invalid.
429Too Many Requests — rate limit exceeded. Should include Retry-After header.

5xx — Server Error

CodeMeaning
500Internal Server Error — something unexpected went wrong.
502Bad Gateway — invalid response from upstream service.
503Service Unavailable — temporarily unable to handle requests.
504Gateway Timeout — upstream service didn't respond in time.
⚠️ Common Mistakes
Don't return 200 with an error body ({"success": false, "error": "something failed"}). This breaks monitoring, logging, and client error handling. Don't return 500 for validation errors — a bad request is a 4xx. Don't return 200 for everything because it's simpler.

Headers — The Metadata Layer

Headers carry metadata about the request and response. A few are essential to understand.

Request Headers

# Authentication
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...   # JWT or OAuth token
Authorization: Basic dXNlcjpwYXNz               # base64 user:password
X-API-Key: sk-prod-1234567890abcdef              # API key (custom header)

# Content negotiation
Content-Type: application/json                   # format of request body
Accept: application/json                         # desired response format

Response Headers

# Rate limit headers — always check these
X-RateLimit-Limit: 1000         # requests allowed per window
X-RateLimit-Remaining: 847      # requests left in current window
X-RateLimit-Reset: 1640995200   # Unix timestamp when window resets
Retry-After: 60                 # seconds to wait (in 429 responses)

CORS Headers

Cross-Origin Resource Sharing — browsers require these before allowing JavaScript on one domain to call an API on another:

Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
💡 CORS Errors
CORS errors in the browser are almost always a server configuration problem, not a client problem. The server must explicitly allow cross-origin requests.
Part Six
Authentication & Authorization
Authentication is "who are you?" Authorization is "what are you allowed to do?"

API Keys — The Simple Approach

An API key is a long random string that identifies and authenticates a caller. Simple, widely used, easy to implement.

curl -H "X-API-Key: sk-prod-abc123xyz" https://api.example.com/data

Good for: server-to-server communication, simple integrations, identifying which application is making requests, rate limiting per client.

Limited because: they're all-or-nothing (one key = full access), they don't represent a specific user, and if leaked they're valid until rotated.

⚠️ Always Headers, Never URLs
Always pass API keys in headers, never in URLs. URLs appear in server logs, browser history, and error messages. A key in a URL is a key waiting to be leaked. For a comprehensive guide to secrets management, .env files, and what to do when keys are exposed, see the Security field guide.

Bearer Tokens and JWT

A bearer token is any token that grants access when presented — "bearer" meaning whoever holds it. The most common format is JWT (JSON Web Token, pronounced "jot").

A JWT is three base64-encoded sections separated by dots: header (algorithm), payload (claims), and signature. The payload typically includes who the token is for, when it expires, and what permissions it grants. The signature proves it hasn't been tampered with.

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI0MiJ9.xZ3hpTbP...

The useful property: the server can validate the token by checking the signature without making a database call. The token is self-describing — fast and stateless.

The limitation: JWTs can't be truly "revoked" before expiration since there's no central registry. Short expiration times (15–60 minutes) mitigate this, combined with refresh tokens.

⚠️ JWTs Are Not Encrypted
JWT payloads are base64-encoded, not encrypted — anyone with the token can decode and read the payload. The signature proves it hasn't been modified, but doesn't hide the contents. Don't put sensitive data in JWT payloads.

OAuth 2.0 — Delegated Access

OAuth 2.0 is the standard for "User A wants to authorize App B to access their data on Service C, without giving App B their password." The "Login with Google/GitHub" flow is OAuth.

Key concepts:

  • Resource owner — the user whose data is being accessed
  • Client — the application requesting access (your app)
  • Authorization server — issues tokens (Google's, GitHub's auth servers)
  • Resource server — where the protected data lives
  • Scopes — specific permissions being requested (read:emails, repo:write)

The User-Facing Flow

  1. Your app redirects the user to the authorization server
  2. User approves the requested scopes
  3. Authorization server redirects back with an authorization code
  4. Your app exchanges the code for an access token (and refresh token)
  5. Your app uses the access token to make API calls

Server-to-server: Client Credentials flow — your server sends its client ID and secret directly to get an access token, no user involved.

When to Use What

ScenarioMethod
Simple server-to-server, you control both sidesAPI key
User-facing app accessing external services on behalf of usersOAuth 2.0 with bearer tokens
User auth within your own systemJWT tokens from your auth server
Need short-lived credentials, granular permissions, or enterprise identityJWT/OAuth from the start
Part Seven
Request & Response Design
Making the right action obvious and the wrong action difficult.

Consistent Structure

Pick conventions and apply them everywhere.

// Response envelope — consistent shape across all endpoints
{
  "data": { ... },
  "meta": {
    "page": 1,
    "per_page": 25,
    "total": 143
  }
}

// Error envelope — same shape regardless of error type
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "email is required",
    "details": [
      { "field": "email", "issue": "missing" }
    ],
    "request_id": "req_abc123"
  }
}
💡 Request IDs
The request_id in errors is underrated. It lets clients report "I got this error" and you can find the exact request in your logs instantly.

Use snake_case or camelCase for field names — pick one and be consistent. Use ISO 8601 for timestamps: "2024-01-15T10:30:00Z". Not Unix timestamps, not custom formats.

Pagination

Any endpoint that returns a collection needs pagination. Without it, performance degrades as data grows.

Offset Pagination (Simpler)

GET /products?page=3&per_page=25

// Response
{ "data": [...], "meta": { "page": 3, "per_page": 25, "total": 143 } }

Clients can jump to any page. Downside: if items are inserted or deleted while paginating, pages shift.

Cursor Pagination (More Correct)

GET /products?after=cursor_abc123&limit=25

// Response
{ "data": [...], "next_cursor": "cursor_def456", "has_more": true }

Immune to insertion/deletion problems because the cursor tracks position in the ordered data, not a page number. Better for feeds, event logs, anything that changes frequently.

💡 Starting Point
For most APIs, start with offset pagination. Move to cursor pagination when real-time data or performance at scale becomes a concern.

Filtering and Sorting

GET /orders?status=pending&created_after=2024-01-01&user_id=42
GET /products?category=electronics&min_price=100&max_price=500
GET /events?sort=created_at&order=desc

Keep parameters consistent — created_after/created_before everywhere, not since/until in some places and after_date/before_date in others.

Error Handling

Errors are a first-class part of the API contract. Good error design is the difference between a developer who integrates in an afternoon and one who spends a week debugging.

Building: What Makes a Good Error

Machine-readable code + human-readable message + context:

{
  "error": {
    "code": "INSUFFICIENT_FUNDS",
    "message": "The payment could not be processed due to insufficient funds.",
    "details": {
      "required": 5000,
      "available": 3200,
      "currency": "USD"
    },
    "request_id": "req_7f3a2b9d",
    "docs_url": "https://docs.example.com/errors/INSUFFICIENT_FUNDS"
  }
}
⚠️ Bad Errors
{ "error": "Something went wrong" } — useless.
{ "error": "Error code: 4028" } — opaque, undocumented.
Never expose stack traces in production API responses.

Consuming: How to Handle Errors Well

Don't just check for 200 and treat everything else as "an error." Distinguish by type:

response = requests.post(url, json=data, headers=headers)

if response.status_code in (200, 201):
    return response.json()
elif response.status_code == 400:
    # Bad request — our code has a bug, fix it
    raise ValueError(f"Bad request: {response.json()['error']['message']}")
elif response.status_code == 401:
    # Auth failed — refresh token or re-authenticate
    self.refresh_auth()
    return self.retry_request(...)
elif response.status_code == 429:
    # Rate limited — back off and retry
    retry_after = int(response.headers.get('Retry-After', 60))
    time.sleep(retry_after)
    return self.retry_request(...)
elif response.status_code >= 500:
    # Server error — retry with exponential backoff
    raise APIServerError(f"Server error {response.status_code}")

Retry Logic and Exponential Backoff

For transient errors (network issues, 5xx, 429), retry with exponential backoff.

import time, random

def api_call_with_retry(url, headers, max_retries=3):
    for attempt in range(max_retries):
        try:
            response = requests.get(url, headers=headers, timeout=30)
            if response.status_code == 429:
                wait = int(response.headers.get('Retry-After', 2 ** attempt))
                time.sleep(wait)
                continue
            if response.status_code >= 500 and attempt < max_retries - 1:
                wait = (2 ** attempt) + random.uniform(0, 1)  # jitter
                time.sleep(wait)
                continue
            return response
        except requests.exceptions.Timeout:
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)
            else:
                raise
    raise Exception("Max retries exceeded")
💡 Jitter
The random addition to the wait time prevents thundering herd — if many clients all retry at exactly the same time, they all hit the server simultaneously. Random jitter spreads them out.
⚠️ Don't Retry 4xx
Don't retry 4xx errors (except 429). A 400 won't become a 200 by retrying. A 404 is still 404. Fix your request.
Part Nine
Rate Limiting & Versioning
Every API has limits. Plan for them before they surprise you in production.

Rate Limiting and Quotas

Every public API has rate limits. Code that ignores them works in development, then breaks in production in ways that are hard to debug.

Types of Rate Limits

  • Requests per time window — most common. Per second (burst protection), per minute/hour/day (usage quotas)
  • Concurrent requests — maximum simultaneous in-flight requests
  • Data volume — bytes per time period. Common for upload/download APIs
  • Cost-based — each operation has a "cost" (tokens for LLMs, compute units). Limited by total cost, not request count

When Remaining is low, slow down proactively. Don't wait for a 429. When you do get a 429, respect the Retry-After header exactly. Don't retry in half the time. Don't retry in a tight loop.

💡 AI APIs
For AI APIs specifically: cost-based rate limits are often more relevant than request counts. A single call that processes 100,000 tokens costs as much as 100 calls with 1,000 tokens each. Build billing awareness into your integration.

Versioning and Breaking Changes

Version your API before you need to. It's much easier to establish versioning upfront than to add it when you need a breaking change.

What Is a Breaking Change?

Breaking (causes existing clients to stop working):

  • Removing or renaming a field from a response
  • Changing a field's type (string to integer)
  • Changing a status code or removing an endpoint
  • Changing the meaning of a parameter

Non-breaking (safe to deploy without a version bump):

  • Adding new fields to responses
  • Adding new endpoints or optional request parameters
  • Adding new enum values (if clients handle unknown values)
📝 The Robustness Principle
Be liberal in what you accept, conservative in what you emit. Accept unknown fields in requests. Emit consistent shapes with predictable fields.

Versioning Strategies

URL versioning (most common): /v1/users, /v2/users. Simple, explicit, visible in logs.

Header versioning: API-Version: 2024-10-01. Cleaner URLs, harder to test in a browser.

Date-based versioning (Stripe's approach): API version is a date. Each client's calls are processed against the semantics that existed on that date. New accounts use the latest version; existing integrations stay pinned.

Managing the Transition

  1. Run old and new versions in parallel
  2. Document what changed and provide a migration guide
  3. Set a deprecation timeline with enough lead time
  4. Use a Sunset header to notify clients via the response
  5. Hold to the sunset date — APIs that say "deprecated" and live forever train developers to ignore deprecation warnings
Part Eleven
Beyond REST
Webhooks, GraphQL, gRPC, real-time protocols, and MCP.

Webhooks — When the Server Calls You

All the patterns above are pull-based: your client calls the server. Webhooks invert this: the server calls you when something happens.

Instead of polling an API every minute to check if an order was paid, you register a URL. When an order is paid, they POST to your URL with the event details.

1. Register:   POST /webhooks { "url": "https://yoursite.com/hooks/stripe",
                                   "events": ["payment.completed"] }
2. Event happens (payment completes)
3. Stripe POSTs: { "event": "payment.completed", "data": { "order_id": "ord_123" } }
4. You return 200 to acknowledge receipt
5. If non-200,  Stripe retries with exponential backoff

Verifying Webhook Signatures

Anyone on the internet can POST to your webhook URL. You must verify that incoming webhooks are actually from the provider. The standard: the provider signs the payload with a secret key and includes the signature in a header.

⚠️ Always Verify
Always verify webhook signatures. Skipping this means anyone can trigger actions in your system by sending fake payloads.

Handling Delivery

Webhooks are delivered at-least-once, meaning the same event might be delivered more than once. Your handler must be idempotent — use the event's ID as an idempotency key. If you've already processed it, skip it.

💡 Respond Quickly
Most providers have a short timeout (5–30 seconds). Respond immediately and queue the actual work for background processing.

GraphQL — Client-Specified Queries

GraphQL addresses REST's biggest pain points: over-fetching (getting more data than you need) and under-fetching (needing multiple requests).

In GraphQL, there's one endpoint. Clients send a query describing exactly what they want:

query {
  user(id: "42") {
    name
    email
    orders(status: "pending") {
      id
      total
      items {
        product { name }
        quantity
      }
    }
  }
}

What would require 3 REST calls (get user, get orders, get order items) can be one GraphQL query.

When GraphQL makes sense: complex data requirements with many relationships, multiple client types with different data needs, rapidly evolving frontends.

When REST makes more sense: public APIs where simplicity matters, simple CRUD, when you need HTTP caching, small teams.

📝 GraphQL Tradeoffs
Harder to cache (single POST endpoint), requires query complexity limits, introspection can expose schema (disable in production), N+1 query problems require data loaders.

gRPC & Real-Time Protocols

gRPC — High-Performance Service-to-Service

gRPC uses Protocol Buffers (binary format) over HTTP/2. Significantly faster than REST+JSON for high-volume internal traffic. The schema is defined first in .proto files, and client libraries are generated in whatever languages you need.

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
}

message User {
  string id = 1;
  string name = 2;
  string email = 3;
}

Use for: internal microservices, high-frequency calls, streaming scenarios. Don't use for: public APIs (not browser-native), when simplicity matters more than performance.

WebSockets and Server-Sent Events

WebSockets — full-duplex persistent connection. Both sides can send messages at any time. Good for chat, collaborative editing, live games.

Server-Sent Events (SSE) — one-way server-to-client. Simpler, built on HTTP, automatically reconnects. Good for live feeds, notifications, and streaming AI responses:

# Streaming SSE response from AI API
with httpx.stream("POST", url, json=data, headers=headers) as response:
    for line in response.iter_lines():
        if line.startswith("data: "):
            chunk = json.loads(line[6:])
            print(chunk["text"], end="", flush=True)

MCP — The Protocol for AI Tools

Model Context Protocol (MCP) is an open standard for connecting AI models to tools and data sources. Where REST APIs connect software systems, MCP connects AI models to capabilities.

An MCP server exposes tools (functions the AI can call), resources (data the AI can read), and prompts (templates). An MCP client (like Claude Code) connects to these servers and the AI model can invoke them.

Client (Claude Code) → MCP Server (GitHub)     → GitHub API
Client (Claude Code) → MCP Server (Filesystem)  → Local files
Client (Claude Code) → MCP Server (Slack)       → Slack API
💡 Same Principles
MCP is REST's philosophical successor in the AI tooling world. The same design principles apply — clear interfaces, predictable behavior, appropriate scoping of permissions. Scope what your MCP servers can do; don't grant broad permissions for narrow tasks.
Part Thirteen
The Practical Craft
Consuming APIs well, building APIs well, and working with AI APIs.

Consuming APIs Well

Building an integration that works in a demo is easy. Building one that works reliably in production for months is harder.

Build a Client, Not Ad-Hoc Requests

Create a client class rather than making raw HTTP calls scattered throughout your code:

class StripeClient:
    def __init__(self, api_key, base_url="https://api.stripe.com/v1"):
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        })

    def create_payment_intent(self, amount, currency, metadata=None):
        response = self.session.post(
            f"{self.base_url}/payment_intents",
            json={"amount": amount, "currency": currency,
                  "metadata": metadata or {}},
            timeout=30
        )
        response.raise_for_status()
        return response.json()

Benefits: authentication configured once, retry logic centralized, error handling consistent, testing easier (mock the client, not HTTP), logging/metrics in one place.

Always Set Timeouts

# Always specify connect timeout and read timeout separately
response = requests.get(url, timeout=(5, 30))  # 5s connect, 30s read

Without a timeout, a slow server will hold your connection open indefinitely, eventually hanging your application.

Log Usefully

# Good: log operation, duration, status, request ID
logger.info("api.call", extra={
    "method": "POST",
    "url": "/v1/payment_intents",
    "status": response.status_code,
    "duration_ms": int(elapsed * 1000),
    "request_id": response.headers.get("X-Request-Id")
})
⚠️ Never Log Secrets
Never log Authorization headers, request bodies that might contain passwords or tokens, or response bodies that might contain PII.

Test Against Real APIs

Use the API's sandbox/test environment, not mocks, for integration testing. Mocks test your assumptions. The real sandbox tests that your assumptions are correct.

Building APIs — Design Principles

The choices you make early determine how easy it is to evolve later.

Design for the developer using it. The primary user is a developer, often working at 11pm against a deadline. Consistent naming, predictable errors, clear documentation with working examples, and a quick start in under five minutes.

Don't design from your database. Your internal data model and your API design are different things. Design resources from the client's perspective — what does a user of the API want to do?

Plan for breaking changes from day one. Version your API from the start, document what constitutes a breaking change, have a deprecation process, test backward compatibility with automated checks.

Document Thoroughly

Documentation is part of the product. The minimum:

  • Every endpoint: what it does, all parameters, all response fields, all status codes
  • Authentication: exactly how to authenticate, where to get credentials
  • Error codes: what each means and how to handle it
  • Rate limits: what they are and what to do when you hit them
  • Changelog: what changed and when, breaking vs. non-breaking
💡 OpenAPI
Use OpenAPI (formerly Swagger) to define your API — it's the standard spec format, enables auto-generated docs, client SDK generation, and automated testing.

APIs and AI

AI has changed API development in several specific ways, both as a consumer and as a builder.

What's Different About AI APIs

Latency is high and variable. An LLM API that generates a response might take 2–30 seconds. This changes UX design (streaming becomes important), timeout settings, and retry strategy. For a deeper look at token economics, AI latency patterns, and optimization techniques, see the Performance field guide.

Costs are response-dependent. LLM APIs charge by tokens — input plus output. Monitor token usage, set max_tokens to cap output, log token counts, build cost awareness into billing logic.

Outputs are non-deterministic. The same input can produce different outputs. Testing requires checking that outputs are within acceptable bounds, not exact equality.

Streaming changes the pattern. For user-facing applications, always use streaming — waiting for the full response creates a worse experience than progressive display.

# Streaming Anthropic API call
import anthropic

client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

with client.messages.stream(
    model="claude-opus-4-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Explain how APIs work"}]
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)

AI-Assisted API Development

Specific uses where AI tools add real value:

  • Writing client code — "Here's the API docs. Write a Python function that calls it, handles errors, and retries on 5xx."
  • Understanding error responses — paste an unexpected error and ask what it means
  • Generating test cases — happy path, validation errors, not found, rate limiting
  • Writing OpenAPI specs — describe your API in natural language, get a spec draft
  • Reviewing your design — ask for feedback on naming, missing error cases, security concerns
⚠️ Review Security-Sensitive Code
For authentication flows, security-sensitive code, and payment integrations, review AI-generated code more carefully than usual. These are exactly the areas where subtle bugs have serious consequences.

Building APIs for AI Agents

If you're building APIs that AI agents will consume:

  • Structured errors matter even more — agents need machine-readable codes to take the right action
  • Idempotency keys are essential — agents may retry when uncertain if they succeeded
  • Consider what actions are reversible — an agent that calls DELETE without confirmation can't undo it
  • Consider building an MCP server alongside or instead of a REST API for AI-native integrations

Quick Reference

HTTP Methods

MethodActionIdempotent?Safe?
GETRetrieveYesYes
POSTCreate / SubmitNoNo
PUTReplaceYesNo
PATCHPartial updateMostlyNo
DELETERemoveYesNo

Status Codes

CodeMeaningUse when
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST
204No ContentSuccessful DELETE, no body
400Bad RequestValidation failure, malformed request
401UnauthorizedMissing or invalid credentials
403ForbiddenAuthenticated but not allowed
404Not FoundResource doesn't exist
409ConflictUniqueness violation, state conflict
422UnprocessableValid syntax, invalid semantics
429Too Many RequestsRate limit exceeded
500Server ErrorSomething unexpected went wrong
503Service UnavailableTemporarily down

Auth Methods

MethodUse forExpiryRevocable
API KeyServer-to-server, simple integrationsNo (rotate manually)Yes (delete key)
JWT BearerUser sessions, stateless authYes (short-lived)Hard (need blacklist)
OAuth 2.0User-delegated access, third-partyYesYes (at auth server)

Design Checklist (Building)

☐

Nouns in URLs, verbs in HTTP methods

☐

Consistent field naming throughout

☐

ISO 8601 for all timestamps

☐

Structured error responses with machine-readable codes

☐

Pagination on all collection endpoints

☐

Version from day one

☐

Request ID in every response

☐

Rate limit headers exposed

☐

Webhook signature verification

☐

OpenAPI spec maintained alongside code

Integration Checklist (Consuming)

☐

Timeouts on every call

☐

Retry with exponential backoff and jitter for 5xx and 429

☐

Never retry 4xx (except 429)

☐

Read and respect rate limit headers

☐

Verify webhook signatures before processing

☐

Idempotency keys for non-idempotent operations

☐

Log request IDs with every error

☐

Use sandbox environment for integration testing

☐

Client class, not scattered ad-hoc calls