Skip to content
Supra Builds

Your API Wasn't Built for AI Agents โ€” Here's How to Fix It

้ปƒๅฐ้ปƒ

้ปƒๅฐ้ปƒ

· 16 min read

Your API Wasn't Built for AI Agents โ€” Here's How to Fix It

By 2026, over 30% of API traffic will come from AI agents rather than human-driven applications. That number will keep climbing.

Here's the uncomfortable truth: most APIs were designed for human developers who read documentation, interpret ambiguous responses, and manually handle edge cases. AI agents do none of that. They parse schemas, chain requests programmatically, and fail silently when your API does something unexpected.

I learned this firsthand while building a zero-cost email API on Cloudflare Workers. The API worked perfectly for human integrators โ€” clear docs, sensible endpoints, proper auth. But when I started thinking about how an AI agent would consume the same API, I realized how many assumptions I'd baked in that only made sense to humans.

This article is the guide I wish I'd had. We'll cover the five principles of agent-ready API design, walk through a real before-and-after retrofit, tackle authentication and error handling for non-human consumers, and finish with a migration checklist you can start on tomorrow.

Whether you're building new APIs or maintaining existing ones, the agent era is already here. Let's make sure your APIs are ready.


Why AI Agents Break Your Existing APIs

The fundamental disconnect is simple: your API was designed for developers who think. AI agents don't think โ€” they parse.

Here's what that means in practice:

AspectHuman DeveloperAI Agent
DocumentationReads prose, follows tutorialsParses OpenAPI schemas and descriptions
AmbiguityInfers meaning from contextNeeds explicit, precise definitions
WorkflowMakes isolated, manual requestsChains multiple calls automatically
ErrorsReads error messages, checks Stack OverflowNeeds structured codes and remediation steps
DiscoveryBrowses docs, bookmarks endpointsNeeds programmatic schema endpoints

When an AI agent encounters your API, it's essentially doing this:

// What your API returns
{
  "status": "error",
  "message": "Invalid request. Please check your parameters and try again."
}

// What the agent needs
{
  "status": "error",
  "code": "INVALID_PARAMETER",
  "message": "The 'email' field must be a valid email address.",
  "parameter": "email",
  "received": "not-an-email",
  "expected": "string (email format, RFC 5322)",
  "docs": "https://api.example.com/docs/errors#INVALID_PARAMETER",
  "remediation": "Validate the email format before sending. Example: user@domain.com"
}

The first response is perfectly fine for a human who can read the message and figure out what went wrong. The second gives an agent everything it needs to self-correct and retry without human intervention.

This isn't just about error handling. Every layer of your API โ€” from endpoint naming to authentication flows โ€” carries assumptions about human consumers that break down when an agent is on the other end.


The 5 Principles of Agent-Ready API Design

Through building APIs and studying how agents consume them, I've distilled the essentials into five principles. These aren't theoretical โ€” they're the minimum bar for making your API useful to autonomous agents.

1. Self-Describing: Let Your API Explain Itself

The most impactful thing you can do is make your API self-describing. This means every endpoint, parameter, and response includes enough context for an agent to understand what it does and how to use it without external documentation.

OpenAPI 3.0+ is the foundation:

# Good: Rich descriptions that agents can parse
paths:
  /users/{userId}/orders:
    get:
      operationId: getUserOrders
      summary: Retrieve all orders for a specific user
      description: >
        Returns a paginated list of orders placed by the specified user.
        Orders are sorted by creation date (newest first).
        Includes order items, totals, and current fulfillment status.
        Requires authentication with at least 'read:orders' scope.
      parameters:
        - name: userId
          in: path
          required: true
          description: The unique identifier of the user (UUID v4 format)
          schema:
            type: string
            format: uuid
            example: "550e8400-e29b-41d4-a716-446655440000"
        - name: status
          in: query
          description: >
            Filter orders by fulfillment status.
            Use 'pending' for unprocessed orders,
            'shipped' for orders in transit,
            'delivered' for completed orders.
          schema:
            type: string
            enum: [pending, shipped, delivered, cancelled, refunded]
        - name: limit
          in: query
          description: Maximum number of orders to return (1-100, default 20)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20

Notice the difference: every field has a description that explains not just what it is but when and why you'd use it. An agent reading this schema knows exactly what each parameter does, what values are valid, and what to expect back.

2. Predictable: Zero Surprises

Agents rely on patterns. If your API returns created_at in one endpoint and createdAt in another, an agent will either fail or require special handling for each endpoint.

Consistency checklist:

  • Naming: Pick one convention (snake_case or camelCase) and stick with it everywhere

  • Response format: Every endpoint should return the same envelope structure

  • Pagination: Use the same pagination pattern across all list endpoints

  • Timestamps: One format everywhere (ISO 8601: 2026-02-11T05:30:00Z)

  • Null handling: Decide whether missing fields are null, omitted, or empty strings

3. Semantic: Meaning Over Syntax

Name things for what they do, not how they're implemented:

# Bad: Implementation-leaked naming
POST /api/v2/db/insert-record
GET  /api/v2/cache/fetch?key=user_123

# Good: Intent-driven naming
POST /api/v2/users
GET  /api/v2/users/123

When an agent sees POST /users, it immediately understands the intent: create a user. When it sees POST /db/insert-record, it has to guess what kind of record and where it goes.

4. Composable: Building Blocks, Not Monoliths

Design endpoints as atomic operations that chain well. An agent orchestrating a checkout flow should be able to:

  1. GET /cart โ†’ Get current cart

  2. POST /orders โ†’ Create order from cart

  3. POST /orders/{id}/payments โ†’ Process payment

  4. GET /orders/{id} โ†’ Verify order status

Each step is independent, has clear inputs/outputs, and can be retried individually if something fails.

Avoid "god endpoints" that do multiple things:

# Bad: One endpoint does everything
POST /checkout
{
  "action": "process",
  "validate_inventory": true,
  "apply_discount": "SAVE10",
  "payment_method": "card",
  "send_confirmation": true
}

# Good: Composable steps
POST /orders              โ†’ Creates order
POST /orders/{id}/discounts โ†’ Applies discount
POST /orders/{id}/payments  โ†’ Processes payment
POST /orders/{id}/confirm   โ†’ Sends confirmation

5. Discoverable: Help Agents Find You

Even the best-designed API is useless if agents can't find it. Expose your API schema at known endpoints:

  • GET /.well-known/openapi.json โ€” Your full OpenAPI spec

  • GET /api โ€” API root with available resources and links

  • Response headers with Link pointing to related resources

We'll dig deeper into discoverability with MCP and HATEOAS in a later section.


Before & After: Retrofitting a Real API

Let's take a concrete example โ€” a user management API โ€” and walk through the transformation step by step.

Before and After API Design Comparison

Before: A Typical REST API

// Express.js โ€” Traditional API endpoint
app.get('/api/users/:id', async (req, res) => {
  try {
    const user = await db.users.findById(req.params.id);
    if (!user) {
      return res.status(404).json({
        error: 'User not found'
      });
    }
    res.json(user);
  } catch (err) {
    res.status(500).json({
      error: 'Something went wrong'
    });
  }
});

This works for humans. A developer gets a 404, reads "User not found," and knows to check the ID. But an agent?

  • No structured error code to branch on

  • No indication of why the user wasn't found (invalid ID format? deleted? never existed?)

  • No hint about what to do next

  • No links to related resources

  • The success response has no schema guarantee

After: Agent-Ready API

// Express.js โ€” Agent-ready API endpoint
app.get('/api/users/:id', async (req, res) => {
  // Validate input format first
  if (!isValidUUID(req.params.id)) {
    return res.status(400).json({
      error: {
        code: 'INVALID_PARAMETER_FORMAT',
        message: 'User ID must be a valid UUID v4.',
        parameter: 'id',
        received: req.params.id,
        expected: 'UUID v4 (e.g., 550e8400-e29b-41d4-a716-446655440000)',
        docs: 'https://api.example.com/docs/users#get-user'
      }
    });
  }

  try {
    const user = await db.users.findById(req.params.id);

    if (!user) {
      return res.status(404).json({
        error: {
          code: 'RESOURCE_NOT_FOUND',
          message: `No user found with ID '${req.params.id}'.`,
          resource: 'user',
          parameter: 'id',
          suggestions: [
            'Verify the user ID is correct',
            'Use GET /api/users?search={query} to find users'
          ],
          docs: 'https://api.example.com/docs/users#get-user'
        }
      });
    }

    res.json({
      data: {
        id: user.id,
        email: user.email,
        name: user.name,
        role: user.role,
        createdAt: user.createdAt.toISOString(),
        updatedAt: user.updatedAt.toISOString()
      },
      _links: {
        self: { href: `/api/users/${user.id}` },
        orders: { href: `/api/users/${user.id}/orders` },
        profile: { href: `/api/users/${user.id}/profile` }
      }
    });
  } catch (err) {
    res.status(500).json({
      error: {
        code: 'INTERNAL_ERROR',
        message: 'An unexpected error occurred while fetching the user.',
        requestId: req.id,
        remediation: 'Retry the request. If the issue persists, contact support with the requestId.',
        retryable: true,
        retryAfter: 5
      }
    });
  }
});

What changed and why:

ChangeWhy It Matters for Agents
Input validation with specific errorAgent can self-correct the ID format
Structured error codes (INVALID_PARAMETER_FORMAT)Agent can branch logic on error type
suggestions arrayAgent knows alternative approaches
_links in success response (HATEOAS)Agent discovers related resources programmatically
retryable + retryAfterAgent knows whether and when to retry
requestIdAgent can reference specific failures in escalation
Consistent data wrapperAgent always knows where to find the payload

The before version has about 15 lines. The after version has more code, but every additional line serves the agent. And here's the thing โ€” humans benefit from these improvements too. Better error messages and discoverable links make any API easier to work with.


Authentication for Non-Human Consumers

Authentication is where most "agent-ready" articles get hand-wavy. Let's get specific.

The JWT Problem

Traditional JWT flows assume a human is present to log in, handle MFA, and refresh tokens. AI agents operate autonomously โ€” there's no human in the loop to re-authenticate when a token expires at 3 AM.

Worse, if you pass JWTs to an LLM as part of a tool's context, you're exposing credentials in the model's context window. That's a security risk with no upside.

For agent-to-API communication, the OAuth 2.0 Client Credentials grant is the right choice:

// Agent authenticates with client credentials
const tokenResponse = await fetch('https://auth.example.com/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'client_credentials',
    client_id: process.env.API_CLIENT_ID,
    client_secret: process.env.API_CLIENT_SECRET,
    scope: 'read:users read:orders'  // Request only needed scopes
  })
});

const { access_token, expires_in } = await tokenResponse.json();

// Agent uses the token for API calls
const userResponse = await fetch('https://api.example.com/users/123', {
  headers: {
    'Authorization': `Bearer ${access_token}`,
    'X-Agent-Id': 'order-processing-agent-v2',  // Identify the agent
    'X-Request-Id': crypto.randomUUID()          // Trace requests
  }
});

Why this works for agents:

  • No human in the loop required

  • Scoped permissions (principle of least privilege)

  • Token rotation is automated

  • The agent never sees user credentials โ€” only its own service credentials

  • X-Agent-Id header lets your API track and rate-limit by agent

API Key Patterns

For simpler setups, API keys work โ€” but treat them differently than you would for human developers:

  • Separate keys per agent: Don't reuse the same key across agents with different purposes

  • Scoped permissions: Each key should only allow the operations that specific agent needs

  • Auto-rotation: Set expiration policies and provide a key rotation endpoint

  • Rate limits per key: AI agents can generate bursts of requests โ€” set appropriate limits

Rate Limiting for Non-Human Traffic

AI agents behave differently than humans. A human might make 5-10 API calls during a session. An agent orchestrating a complex task might make 50-100 calls in seconds.

Design your rate limiting accordingly:

# Headers your API should return
X-RateLimit-Limit: 1000          # Requests per window
X-RateLimit-Remaining: 847       # Remaining in current window
X-RateLimit-Reset: 1707635400    # Unix timestamp when window resets
Retry-After: 30                  # Seconds to wait (on 429 response)

Consider tiered rate limits: a basic tier for general API keys and a higher tier for verified agent integrations that have been reviewed and approved.


Error Handling That Agents Can Act On

Here's a principle that will transform your API's agent-friendliness: every error response should tell the agent what to do next.

The Error Response Contract

Define a consistent error schema that agents can rely on:

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "You have exceeded the rate limit for this endpoint.",
    "details": {
      "limit": 100,
      "window": "60s",
      "current": 103
    },
    "retryable": true,
    "retryAfter": 45,
    "remediation": "Wait 45 seconds before retrying. Consider reducing request frequency or upgrading to a higher rate limit tier.",
    "docs": "https://api.example.com/docs/rate-limits",
    "requestId": "req_abc123def456"
  }
}

Key fields explained:

FieldPurpose
codeMachine-readable error type for branching logic
messageHuman-readable explanation
detailsContextual data specific to this error type
retryableCan the agent retry this exact request?
retryAfterHow long to wait (in seconds)
remediationStep-by-step fix instructions
docsLink to detailed documentation
requestIdUnique ID for debugging and support escalation

Error Categories

Organize your error codes into categories that agents can use for high-level branching:

AUTH_*       โ†’ Authentication issues    โ†’ Re-authenticate
PERM_*       โ†’ Permission issues        โ†’ Request different scope
PARAM_*      โ†’ Parameter issues         โ†’ Fix input and retry
RATE_*       โ†’ Rate limiting            โ†’ Wait and retry
RESOURCE_*   โ†’ Resource state issues    โ†’ Check resource status
INTERNAL_*   โ†’ Server issues            โ†’ Retry with backoff

An agent receiving AUTH_TOKEN_EXPIRED knows to refresh the token and retry. An agent receiving PARAM_INVALID_FORMAT knows to fix the input. An agent receiving INTERNAL_ERROR knows to back off and retry later.

This categorization turns error handling from guesswork into a deterministic state machine โ€” exactly what autonomous agents need.


Making Your API Discoverable: MCP and Beyond

Your API might be perfectly designed, but if agents can't find it, it might as well not exist.

API Discoverability Architecture

Model Context Protocol (MCP)

MCP is becoming the standard way for AI agents to discover and interact with APIs. Think of it as a universal adapter between AI agents and your services.

Instead of teaching each AI model how to use your specific API, you expose your API through an MCP server that speaks a protocol agents already understand:

// MCP server exposing your API to AI agents
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';

const server = new McpServer({
  name: 'user-management-api',
  version: '1.0.0',
});

// Define a tool that agents can discover and use
server.tool(
  'get_user',
  'Retrieve a user by their unique ID. Returns user profile including name, email, role, and account creation date.',
  {
    userId: {
      type: 'string',
      description: 'The unique UUID v4 identifier of the user to retrieve',
    }
  },
  async ({ userId }) => {
    const response = await fetch(`https://api.example.com/users/${userId}`, {
      headers: { 'Authorization': `Bearer ${API_TOKEN}` }
    });
    const data = await response.json();
    return {
      content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
    };
  }
);

The key insight: MCP bridges the gap between your existing REST API and agent consumption. You don't have to rewrite your API โ€” you wrap it in a layer that agents can discover.

HATEOAS: The Comeback

HATEOAS (Hypermedia as the Engine of Application State) was ahead of its time. Human developers mostly ignored it โ€” who needs machine-navigable links when you can bookmark the docs?

AI agents, that's who.

{
  "data": {
    "id": "user_123",
    "name": "Supra Huang",
    "email": "supra@example.com"
  },
  "_links": {
    "self": {
      "href": "/api/users/user_123",
      "method": "GET"
    },
    "update": {
      "href": "/api/users/user_123",
      "method": "PATCH",
      "description": "Update user profile fields"
    },
    "orders": {
      "href": "/api/users/user_123/orders",
      "method": "GET",
      "description": "List all orders for this user"
    },
    "deactivate": {
      "href": "/api/users/user_123/deactivate",
      "method": "POST",
      "description": "Deactivate the user account (reversible)"
    }
  },
  "_actions": {
    "available": ["update", "deactivate", "orders"],
    "unavailable": [
      {
        "action": "delete",
        "reason": "User has active orders. Resolve orders before deletion.",
        "blockedBy": "/api/users/user_123/orders?status=active"
      }
    ]
  }
}

Notice the _actions block. It tells the agent not just what it can do, but also what it can't do and why. An agent attempting to delete this user would know to resolve active orders first โ€” without making a failed request and parsing an error.

Schema-First Design

Expose your full API schema at well-known endpoints:

  • GET /.well-known/openapi.json โ€” Full OpenAPI specification

  • GET /.well-known/mcp.json โ€” MCP server configuration (if applicable)

  • GET /api โ€” Root endpoint listing all available resources

This is the minimum for discoverability. An agent landing on your API domain can immediately understand what's available and how to use it.


Testing Your API with AI Agents

You wouldn't ship a website without testing it in a browser. Don't ship an agent-ready API without testing it with actual agents.

Prompt-Based Testing

The simplest test: give an AI agent your API docs and ask it to accomplish a task. If it struggles, your API has discoverability or usability issues.

# Simple agent-based API test
def test_api_with_agent():
    """
    Give an LLM your OpenAPI spec and see if it can
    successfully complete a multi-step workflow.
    """
    openapi_spec = load_openapi_spec('./openapi.json')

    test_scenarios = [
        {
            "task": "Find the user with email test@example.com and list their recent orders",
            "expected_calls": ["GET /users?email=test@example.com", "GET /users/{id}/orders"],
            "expected_result": "Returns a list of orders"
        },
        {
            "task": "Create a new user and assign them the 'editor' role",
            "expected_calls": ["POST /users", "PATCH /users/{id}"],
            "expected_result": "User created with editor role"
        }
    ]

    for scenario in test_scenarios:
        result = run_agent_with_tools(
            prompt=scenario["task"],
            tools=openapi_spec_to_tools(openapi_spec)
        )
        assert_calls_match(result.api_calls, scenario["expected_calls"])
        print(f"โœ… Passed: {scenario['task']}")

Schema Validation

Validate that your actual API responses match your OpenAPI spec. Drift between spec and reality is the number one reason agents fail:

# Use openapi-diff to catch breaking changes
npx openapi-diff previous-spec.json current-spec.json

# Use Spectral to lint your OpenAPI spec (https://github.com/stoplightio/spectral)
npx @stoplight/spectral-cli lint openapi.json

Key Metrics to Monitor

Once agents are consuming your API, track these metrics:

  • Agent success rate: What percentage of agent workflows complete without errors?

  • Self-correction rate: How often do agents recover from errors without human help?

  • Average calls per task: Are agents making efficient use of your endpoints?

  • Error category distribution: Which error types are most common? That's where to improve.


Migration Checklist: Start Tomorrow

You don't have to rewrite your API from scratch. Here's a phased approach:

Quick Wins (This Week)

  • [ ] Add operationId and rich description to every OpenAPI endpoint

  • [ ] Standardize error response format with code, message, retryable

  • [ ] Add X-Request-Id to every response for tracing

  • [ ] Expose your OpenAPI spec at /.well-known/openapi.json

  • [ ] Add rate limit headers to all responses

Medium Effort (Next 2 Weeks)

  • [ ] Implement structured error codes with categories (AUTH_*, PARAM_*, etc.)

  • [ ] Add _links (HATEOAS) to resource responses

  • [ ] Set up OAuth 2.0 client credentials flow for agent auth

  • [ ] Create agent-specific API keys with scoped permissions

  • [ ] Add remediation field to error responses

Long-Term (1-3 Months)

  • [ ] Build an MCP server wrapping your API

  • [ ] Implement comprehensive agent-based integration tests

  • [ ] Set up monitoring dashboards for agent traffic patterns

  • [ ] Design composable endpoints for complex workflows

  • [ ] Add _actions blocks showing available/unavailable operations

Start with the quick wins. Just adding rich OpenAPI descriptions and structured error codes will make a measurable difference in how well agents work with your API.


Conclusion

The shift from human-first to agent-first API design isn't coming โ€” it's already here. AI agents are consuming APIs at scale, and the APIs that work well with them will get more integrations, more traffic, and more adoption.

The good news: agent-ready API design isn't a radical departure from good API design. Self-describing endpoints, consistent response formats, structured errors, and proper authentication are improvements that benefit all consumers โ€” human and AI alike.

Start with what matters most: make your API self-describing (rich OpenAPI specs), make errors actionable (structured codes with remediation), and make endpoints discoverable (schema at well-known URLs).

Your API wasn't built for AI agents. But with the changes in this guide, it can be โ€” starting this week.


What's your experience building APIs that AI agents consume? Have you tried wrapping your API with MCP? I'd love to hear your approach โ€” drop a comment below or find me on GitHub.

้ปƒๅฐ้ปƒ

้ปƒๅฐ้ปƒ

Full-stack product engineer and open source contributor based in Taiwan. I specialize in building practical solutions that solve real-world problems with focus on stability and user experience. Passionate about Product Engineering, Solutions Architecture, and Open Source collaboration.

More Posts

Beyond Chatbots: Building Real-World Stateful AI Agents on Cloudflare

Beyond Chatbots: Building Real-World Stateful AI Agents on Cloudflare

Most "AI agents" you see today are just LLM wrappers with a fancy prompt. They process a request, return a response, and forget everything. No memory. No scheduling. No persistence. Real agents are different. They remember what happened yesterday. Th...

้ปƒๅฐ้ปƒ ้ปƒๅฐ้ปƒ · · 14 min
When Microservices Are Wrong: A Solutions Architect's Decision Framework

When Microservices Are Wrong: A Solutions Architect's Decision Framework

I've been that architect. The one who spun up AWS Lambda functions and ECS clusters for every new service, convinced that microservices were the only "proper" way to build modern software. After years of managing distributed complexity โ€” and eventual...

้ปƒๅฐ้ปƒ ้ปƒๅฐ้ปƒ · · 12 min