Making Your REST API Callable by Agents: A Retrofit Guide

Why 76% of Teams Still Don’t Build APIs for Agents

According to the Postman State of the API 2025 survey, 89% of development teams use AI, but only 24% design their APIs for AI agents, creating a massive “AI-API gap.” This gap manifests in silent, catastrophic failures — an independent failure-engineering analysis of 1,099 APIs found that silent failures are the worst failure class, with HubSpot returning HTTP 200 with an empty body when secondary quotas are exhausted, causing agents to log “success” on work that never happened. This guide provides a step-by-step retrofit to close this gap, transforming a legacy API into one agents can consume reliably. We’ll follow the agent-computer interface (ACI) principle from Anthropic, which advises investing as much effort in the agent interface as the human one — the same philosophy behind our AI agent error handling guide and production tool calling architecture patterns.

The “AI-API gap” — what the survey data shows

The survey data reveals a stark disconnect: while AI adoption is nearly ubiquitous, the APIs powering the applications haven’t been adapted for machine consumption. This gap leads to agents struggling with basic interactions, a problem that well-designed APIs can partially absorb.

Silent failures: the worst failure class

As the independent analysis notes, the most dangerous failures are the silent ones. Beyond HubSpot, the analysis highlights Auth0 M2M tokens that expire silently after 60 days, leaving agents unable to authenticate without any proactive signal.

What “agent-ready” means — the ACI principle

Anthropic’s engineering guidance posits that tools are “a contract between deterministic systems and non-deterministic agents.” An agent-ready API adheres to this by providing clear boundaries, meaningful context, and optimized responses, making tool selection and argument construction unambiguous.

The retrofit target: a legacy REST API

Our starting point is a typical legacy REST API: it uses a single, long-lived shared API key for authentication, offset-based pagination, returns only prose error messages, has no idempotency on writes, and enforces fixed RPM rate limits without providing a Retry-After header. We will transform it section by section.

Step 1 — Auth for Machine Clients

Auth for machine clients means providing scoped, expiring credentials with explicit metadata, ensuring agents can programmatically handle authentication without human intervention. Anthropic’s error documentation specifies that every error response should be a JSON object with a machine-readable type and that responses must carry a request-id header for traceability.

Failure mode: silent auth expiry

Human-oriented auth breaks agents because it fails silently. When a key is revoked or an OAuth M2M token expires, the agent receives an error but may not understand the context or how to recover. The independent analysis cites Auth0 M2M tokens expiring silently at 60 days as a prime example.

The fix: API keys with explicit expiry

The solution is to use API keys with an explicit expires_at metadata field included in the authentication response. Anthropic’s API returns a 401 authentication_error for malformed, revoked, or expired keys, and its key-expiration feature provides a model for surfacing this state.

When OAuth2 M2M is worth it

OAuth2 M2M flows offer scoped, time-bound tokens but carry the same silent-expiry trap unless proactively surfaced. They are worth the complexity only when fine-grained permission scoping is a strict requirement.

FastAPI retrofit diff

# BEFORE: Simple key check in middleware
def verify_api_key(request: Request):
    api_key = request.headers.get("X-API-Key")
    if api_key != "SECRET_KEY":
        raise HTTPException(status_code=401, detail="Invalid API key")
    return True

# AFTER: Middleware returning key metadata and structured 401
from fastapi import Depends, Request
from fastapi.responses import JSONResponse
import datetime

async def verify_agent_api_key(request: Request):
    api_key = request.headers.get("X-API-Key")
    key_data = get_key_from_store(api_key)  # Assume this retrieves key info
    if not key_data or key_data["expires_at"] < datetime.datetime.utcnow():
        return JSONResponse(
            status_code=401,
            content={
                "error": {
                    "type": "authentication_error",
                    "message": "API key is missing, expired, or revoked."
                },
                "request_id": request.state.request_id
            }
        )
    request.state.agent_id = key_data["agent_id"]
    return True

Step 2 — Error Contracts Agents Can Parse

Error contracts agents can parse are structured JSON envelopes with a stable error.type enum, a human-readable message, and a request_id, eliminating the need for fragile string-matching. According to Anthropic’s error documentation, this structured contract ensures agents can programmatically distinguish between 11 different error types, from invalid_request_error to overloaded_error.

Failure mode: prose errors force string-matching

When an API returns only a prose message like “Invalid API key provided,” an agent must attempt to parse the string to determine the error category. This is brittle and prone to failure if the message wording changes. It’s the API-layer version of the failure modes we catalogued in our guide to building reliable agent error handling.

The fix: structured error envelope

The fix is to adopt a structured error envelope similar to Anthropic’s: every error response returns a JSON object with a top-level error object containing a type (a stable, machine-readable enum) and a message. Every response, successful or not, includes a request-id header.

SSE caveat: errors after HTTP 200

For streaming endpoints using SSE, an error can arrive after the initial HTTP 200 status. As documented by Anthropic, providers must define mid-stream error events that agents can parse.

FastAPI retrofit diff

# BEFORE: Raising generic exceptions
@app.get("/items/{item_id}")
def read_item(item_id: int):
    if item_id == 0:
        raise HTTPException(status_code=400, detail="Invalid item ID.")
    return {"item_id": item_id}

# AFTER: Structured error envelope with request_id
from fastapi import Request
from fastapi.responses import JSONResponse

@app.exception_handler(Exception)
async def structured_error_handler(request: Request, exc: Exception):
    error_type = "api_error"  # Map exceptions to stable types
    if isinstance(exc, ValueError):
        error_type = "invalid_request_error"
    elif isinstance(exc, PermissionError):
        error_type = "permission_error"

    return JSONResponse(
        status_code=getattr(exc, "status_code", 500),
        content={
            "error": {
                "type": error_type,
                "message": str(exc) or "An unexpected error occurred."
            },
            "request_id": request.state.request_id
        },
        headers={"request-id": request.state.request_id}
    )

Step 3 — Rate Limits Agents Can Survive

Rate limits agents can survive are token-bucket systems with Retry-After headers on 429 responses and X-RateLimit-Remaining and X-RateLimit-Reset headers on every response. Anthropic’s rate-limit documentation details that exceeding limits returns a 429 with a retry-after header and exposes headers like anthropic-ratelimit-*-remaining and -reset (RFC 3339).

Failure mode: blind backoff

Without explicit Retry-After guidance, agents either spin in a tight retry loop, exacerbating the problem, or back off arbitrarily, wasting time. The independent analysis contrasts this with Twilio, which returns 429 with documented windows enabling precise backoff.

The fix: token-bucket with headers

Implement a token-bucket algorithm. Ensure every response includes rate-limit state headers, and every 429 includes a Retry-After header. Anthropic also exposes a programmatic Rate Limits API for agents to query their current limits.

Cache-aware accounting

Anthropic’s documentation notes that cached input tokens (cache_read_input_tokens) do not count toward the input-tokens-per-minute limit for most models. This means an agent with an 80% cache hit rate on a 2M ITPM limit can effectively use ~10M input tokens per minute. When you’re counting tokens across an agent fleet, the same math drives cost — see our AI agent cost optimization patterns.

FastAPI retrofit diff

# BEFORE: No rate limiting
@app.get("/data")
def get_data():
    return {"data": [...]}

# AFTER: Using slowapi with agent-friendly headers
from slowapi import Limiter
from slowapi.util import get_remote_address
from fastapi import Request
from fastapi.responses import JSONResponse

limiter = Limiter(key_func=get_remote_address)

@app.get("/data")
@limiter.limit("100/minute")  # Simplified; real token-bucket is more complex
async def get_data(request: Request):
    # Assume limiter exposes these via response headers automatically
    # For explicit control, use a custom middleware
    return {"data": [...]}

# Custom middleware to add headers and handle 429 with structured response
@app.exception_handler(RateLimitExceeded)
async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
    return JSONResponse(
        status_code=429,
        content={
            "error": {
                "type": "rate_limit_error",
                "message": f"Rate limit exceeded. Retry after {exc.detail} seconds."
            },
            "request_id": request.state.request_id
        },
        headers={
            "Retry-After": str(exc.detail),
            "X-RateLimit-Remaining": "0",
            "X-RateLimit-Reset": exc.reset_at.isoformat()
        }
    )

Step 4 — Idempotency on Writes

Idempotency on writes requires an Idempotency-Key header on every mutating endpoint, with the server caching the successful result keyed to that header so retries are safe. Stripe’s idempotency blog defines an idempotent endpoint as one that “can be called any number of times while guaranteeing that side effects only occur once,” using the Idempotency-Key header on mutating POSTs.

Failure mode: duplicate side effects

When an agent retries a failed request due to a network timeout, it risks creating duplicate charges, records, or notifications. This is a critical failure for financial or state-changing operations.

The fix: Idempotency-Key header

Implement server-side caching of successful responses keyed by the Idempotency-Key. As Stripe documents, retries with the same key return the cached result. Retry logic on the client side should use exponential backoff with jitter to avoid a thundering herd.

Which endpoints need it

Every mutating POST, PUT, PATCH, and DELETE endpoint should require an Idempotency-Key. GET and HEAD requests are inherently idempotent and do not need this.

FastAPI retrofit diff

# BEFORE: No idempotency
@app.post("/orders")
def create_order(order: Order):
    db.create_order(order)
    return {"order_id": 123}

# AFTER: Idempotency dependency
from fastapi import Header, HTTPException
import redis
import json

redis_client = redis.Redis()

async def idempotency_key_check(idempotency_key: str = Header(..., alias="Idempotency-Key")):
    cached = redis_client.get(f"idemp:{idempotency_key}")
    if cached:
        return json.loads(cached)
    return None

@app.post("/orders", dependencies=[Depends(idempotency_key_check)])
async def create_order(order: Order, request: Request, idempotency_key: str = Header(...)):
    # Check cache again (double-check pattern)
    cached_response = redis_client.get(f"idemp:{idempotency_key}")
    if cached_response:
        return json.loads(cached_response)

    order_id = db.create_order(order)
    response = {"order_id": order_id}

    # Cache successful response for 24 hours
    redis_client.setex(f"idemp:{idempotency_key}", 86400, json.dumps(response))
    return response

Step 5 — Structured Outputs on Responses

Structured outputs on responses mean constraining response formats to a strict JSON Schema, eliminating the need for agents to validate or retry incorrectly formatted data. OpenAI’s Structured Outputs guide recommends “always using Structured Outputs instead of JSON mode when possible,” because only Structured Outputs guarantees adherence to the supplied schema.

Failure mode: schema hallucination

When a response is free-form JSON, agents may hallucinate field names or misinterpret the structure, leading to silent data corruption or errors in subsequent steps.

JSON mode vs Structured Outputs

JSON mode (response_format: {type: "json_object"}) guarantees valid JSON but not schema adherence. Structured Outputs (response_format: {type: "json_schema", strict: true}) guarantees the response matches the schema, making agent parsing deterministic.

Token overhead of tool definitions

Anthropic’s tool-use documentation notes that tool definitions have a fixed token overhead, with examples ranging from ~496 to ~588 tokens on Claude Haiku 4.5. This cost is worth the reliability gain for agent-facing endpoints, and it’s the same tradeoff we walk through in our production tool calling architecture guide.

FastAPI retrofit diff

# BEFORE: Returning free-form dict
@app.get("/users/{user_id}")
def get_user(user_id: int):
    return {"id": user_id, "name": "John Doe", "email": "[email protected]"}

# AFTER: Using Pydantic for strict schema enforcement
from pydantic import BaseModel

class UserResponse(BaseModel):
    id: int
    name: str
    email: str

@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
    # FastAPI + Pydantic automatically generate JSON Schema and validate output
    return UserResponse(id=user_id, name="John Doe", email="[email protected]")

Step 6 — Pagination Agents Don’t Break On

Pagination that agents don’t break on uses cursor-based pagination with a stable order, a has_more flag, and a next_cursor token. OpenAI’s list endpoints are cursor-paginated, and their official Python library ships auto-paginating iterators like has_next_page() and get_next_page(), so clients never hand-roll page loops.

Failure mode: offset pagination causes data drift

Under concurrent writes, offset pagination (?page=2&limit=10) can cause items to shift between pages, leading agents to miss records or process duplicates.

The fix: cursor pagination with stable ordering

Implement cursor-based pagination where the client sends an after parameter (the ID or timestamp of the last item seen). The response includes a has_more boolean and a next_cursor for the next page.

Auto-paginating SDK iterators

The gold standard for agent clients is an SDK that handles pagination automatically, abstracting the cursor logic away from the agent.

FastAPI retrofit diff

# BEFORE: Offset pagination
@app.get("/items")
def list_items(page: int = 1, limit: int = 20):
    offset = (page - 1) * limit
    items = db.query_items(offset=offset, limit=limit)
    return {"items": items, "page": page, "limit": limit}

# AFTER: Cursor-based pagination
from typing import List, Optional
from pydantic import BaseModel

class PaginatedItems(BaseModel):
    items: List[Item]
    next_cursor: Optional[str]
    has_more: bool

@app.get("/items", response_model=PaginatedItems)
async def list_items(after: Optional[str] = None, limit: int = 20):
    # `after` is the cursor from the previous response's `next_cursor`
    items, next_cursor = db.query_items_after(after=after, limit=limit + 1)
    has_more = len(items) > limit
    if has_more:
        items = items[:limit]  # Trim the extra item used to check for more
    return PaginatedItems(
        items=items,
        next_cursor=next_cursor if has_more else None,
        has_more=has_more
    )

Step 7 — Discoverability: OpenAPI, llms.txt, and Tool Definitions

Discoverability for agents involves publishing an OpenAPI 3.1 specification, an llms.txt file, and JSON Schema tool definitions for each endpoint. As llmstxt.org states, llms.txt is a de-facto standard used by thousands of sites, including OpenAI, Anthropic, and Gemini, for their own developer documentation.

Failure mode: agents can’t find or understand your API

The Postman 2025 survey found that 55% of developers struggle with inconsistent documentation, and 34% can’t find existing APIs. For an agent, this discovery problem is even more acute.

The fix: OpenAPI + llms.txt + JSON Schema

Publish an auto-generated OpenAPI 3.1 JSON file for codegen and tool discovery. Serve a human-readable /llms.txt file with clean markdown and add Link: rel="alternate" type="text/markdown" headers. Define each endpoint’s input and output with JSON Schema, aligning with the MCP tool entry structure that uses inputSchema (JSON Schema default 2020-12).

MCP contrast

The MCP specification structures tool entries with name, title, description, inputSchema, and ToolAnnotations (e.g., idempotentHint). This pattern can inspire the JSON Schema tool definitions in your OpenAPI spec — and if you decide to expose your API through MCP instead of (or alongside) raw HTTP, our MCP server production deployment patterns and MCP integration patterns cover the server side.

FastAPI retrofit diff

# BEFORE: No discoverability files
# AFTER: Auto-generate OpenAPI and add llms.txt route
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse, JSONResponse

app = FastAPI()

# FastAPI automatically generates OpenAPI JSON at /openapi.json
# Customize it to align with tool-use schemas

@app.get("/llms.txt", response_class=PlainTextResponse)
async def llms_txt():
    content = """
# My Agent-Ready API

## Authentication
Use Bearer tokens. See `/auth` endpoint.

## Endpoints

### GET /items
List items with cursor pagination.
- Parameters: `after` (string, optional), `limit` (integer)
- Returns: `{ items: [...], next_cursor: string|null, has_more: boolean }`

### POST /orders
Create an order. Requires `Idempotency-Key` header.
- Request: `{ "item_id": 1, "quantity": 2 }`
- Returns: `{ "order_id": 123 }`
    """
    return content

# Add Link header for all markdown documentation
@app.middleware("http")
async def add_markdown_link_header(request: Request, call_next):
    response = await call_next(request)
    if request.url.path.startswith("/docs"):
        response.headers["Link"] = '<https://yoursite.com/llms.txt>; rel="alternate"; type="text/markdown"'
    return response

The Agent-Ready Decision Matrix

This table summarizes the key design decisions for an agent-ready API, comparing legacy approaches with the recommended agent-ready stack. Use it as a quick reference for your retrofit.

Design decision Option A (Legacy) Option B (Agent-Ready) Verified facts (source) Verdict
Tool definitions Vendor tool schema only Full OpenAPI 3.1 spec + JSON Schema per endpoint OpenAI & Anthropic tool APIs consume JSON Schema defs (Anthropic tool-use docs); MCP inputSchema defaults to JSON Schema 2020-12 (MCP spec); OpenAPI “allows both humans and computers to discover and understand capabilities” (OpenAPI spec) Both — JSON Schema per endpoint for tool use + OpenAPI 3.1 for discovery/codegen
Structured outputs {type: "json_object"} — valid JSON only {type: "json_schema", strict: true} — schema adherence guaranteed OpenAI: “always use Structured Outputs instead of JSON mode when possible” (OpenAI Structured Outputs guide) Structured Outputs (strict JSON Schema) on every agent-facing response
Streaming Single JSON response SSE with dedicated error-event handling Anthropic documents mid-stream SSE error events (Anthropic errors doc) SSE for long/chat endpoints with documented error events; plain JSON for short mutations
Auth Long-lived API key, no expiry metadata Scoped API key with explicit expires_at Anthropic: 401 for malformed/revoked/expired keys (Anthropic errors doc); Auth0 M2M tokens expire silently at 60 days (supertrained analysis) API keys with explicit expiry + expires_at metadata
Rate limits Fixed RPM, no headers, 429 without Retry-After Token-bucket with remaining/reset headers, Retry-After on 429 Anthropic: cache-aware ITPM (Anthropic rate limits doc); HubSpot 200-empty at quota vs Twilio 429 windows (supertrained analysis) Agent-aware limits — Retry-After, remaining/reset headers, never fake-success
Pagination Offset/limit — page drift under concurrent writes Cursor (after=<id>, has_more, stable ordering) OpenAI list endpoints cursor-paginated with auto-paginating iterators (OpenAI Python SDK) Cursor pagination with stable ordering
Errors Prose message strings only Structured envelope: status code + error.type + message + request ID Anthropic: 11 typed codes, JSON envelope (Anthropic errors doc); silent 200-empty worst failure class (supertrained analysis) Structured error envelope with stable type codes, request IDs, never a 200 on failure

The Bottom Line

Start your retrofit with the highest-impact, lowest-effort changes: implement structured error contracts and idempotency on writes immediately to eliminate silent failures and duplicate side effects. Next, add authentication expiry metadata and agent-aware rate-limit headers to give agents the context they need to recover. Follow this with cursor-based pagination and structured outputs to ensure data consistency and parsing reliability. Finally, improve discoverability by publishing an OpenAPI 3.1 spec and an llms.txt file. Every hour you invest in making your API agent-consumable is an hour agents don’t spend failing silently on your behalf.

How This Guide Was Built This guide is based on official documentation from Anthropic, OpenAI, Stripe, and the MCP specification, supplemented by the Postman State of the API 2025 survey, the independent supertrained failure-engineering analysis of 1,099 APIs, and peer-reviewed benchmarks (AgentBench ICLR 2024, GTA NeurIPS 2024). All source URLs were verified as HTTP 200 on August 16, 2026. We did not run a live agent test suite against a production API — the failure modes and retrofit patterns described are drawn from published research and official engineering guidance, not hands-on testing.

FAQ

Q1: What is an agent-ready API?

An agent-ready API is an HTTP API designed so that AI agents can discover, authenticate, call, parse responses from, and retry calls to it without human intervention. According to Anthropic’s engineering guidance, tool definitions should include example usage, edge cases, and clear boundaries from other tools — principles that apply to any agent-facing endpoint.

Q2: Why do agents fail silently on APIs not built for them?

Independent failure-engineering analysis of 1,099 APIs found that six categories cover over 95% of agent-API failures, including silent failures where HubSpot returns HTTP 200 with an empty body when quotas are exhausted. The supertrained analysis details how such failures cause agents to log success on work that never happened.

Q3: What’s the difference between JSON mode and Structured Outputs?

JSON mode (response_format: {type: "json_object"}) guarantees valid JSON but not schema adherence. Structured Outputs (response_format: {type: "json_schema", strict: true}) guarantees adherence to the supplied JSON Schema. OpenAI recommends always using Structured Outputs over JSON mode.

Q4: How should I handle rate limits for AI agents specifically?

Agent-aware rate limits use token-bucket algorithms with Retry-After headers on 429 responses, X-RateLimit-Remaining and X-RateLimit-Reset headers on every response, and programmatic limits endpoints. Anthropic’s approach accounts for cached tokens, enabling up to 10M effective input tokens per minute with high cache hit rates.

Q5: What is llms.txt and why does it matter for agent discoverability?

llms.txt is a discoverability standard where sites publish a /llms.txt file with clean markdown page variants and Link: rel="alternate" headers. Thousands of sites publish it, including OpenAI and Anthropic, enabling agents to discover documentation without human browsing. See llmstxt.org for details.

Q6: How does this retrofit approach compare to adopting MCP?

MCP defines a specific tool-calling protocol with entries carrying name, title, description, inputSchema, and ToolAnnotations. The Postman 2025 survey found 70% of developers are aware of MCP but only 10% use it regularly. This guide focuses on the HTTP/API layer — the patterns here complement MCP adoption, not replace it; if you’re evaluating the protocol itself, our MCP server benchmarking guide compares real implementations.

← Back to all posts