Agent-Friendly API Design#

Most APIs are designed for human developers who read documentation, interpret ambiguous error messages, and adapt their approach based on experience. Agents do not have these skills. They parse structured responses, follow explicit instructions, and fail on ambiguity. An API that is pleasant for humans to use may be impossible for an agent to use reliably.

This reference covers practical patterns for designing APIs – or modifying existing ones – so that agents can consume them effectively.

Structured Error Responses#

The single most impactful change for agent consumption is structured error responses. An agent that receives "Internal Server Error" has no basis for deciding what to do next. An agent that receives a structured error with a type, a message, and a suggested action can reason about recovery.

The Problem#

// Bad: Agent receives this and has no actionable information
{ "error": "Something went wrong" }

// Bad: Human-readable but not machine-parseable
{ "error": "The deployment failed because the image tag 'v2.3' was not found in the registry. Please check the tag exists and try again." }

// Bad: Too little structure
{ "code": 500, "message": "Internal error" }

The Pattern: RFC 7807 Problem Details#

Use RFC 7807 (Problem Details for HTTP APIs) as the base format. It provides a standard structure that agents can rely on.

{
  "type": "https://api.example.com/errors/image-not-found",
  "title": "Container image not found",
  "status": 404,
  "detail": "Image tag 'v2.3' does not exist in registry 'us-docker.pkg.dev/myproject/api'.",
  "instance": "/api/v1/deployments/deploy-abc123",
  "retryable": false,
  "suggested_action": "List available tags using GET /api/v1/images/api/tags",
  "context": {
    "registry": "us-docker.pkg.dev/myproject/api",
    "requested_tag": "v2.3",
    "available_tags": ["v2.2", "v2.1", "v2.0", "latest"]
  }
}

What makes this agent-friendly:

  • type is a stable, machine-readable error identifier. The agent can switch on this value without parsing English.
  • retryable tells the agent whether to retry. No guessing.
  • suggested_action gives the agent a concrete next step. This is the single most useful field for agent consumption.
  • context provides structured data the agent can use to self-correct. The available tags let the agent pick a valid one without making another API call.

Error Types to Define#

At minimum, define error types for these categories:

validation_error     — Input failed validation (400)
authentication_error — Invalid or missing credentials (401)
authorization_error  — Valid credentials, insufficient permissions (403)
not_found            — Resource does not exist (404)
conflict             — Operation conflicts with current state (409)
rate_limited         — Too many requests (429)
internal_error       — Server-side failure (500)
service_unavailable  — Temporary outage (503)

Each type should have a consistent response shape. Agents will build error-handling logic around these types.

Pagination That Agents Handle Well#

Pagination is where most agent integrations break. Agents struggle with offset-based pagination, forget to paginate at all, or construct malformed page requests. Design pagination to be foolproof.

Cursor-Based, Not Offset-Based#

Offset pagination (?page=3&per_page=20) requires the agent to track page numbers, calculate offsets, and handle edge cases when results change between requests. Cursor pagination gives the agent a single opaque token to pass.

// Response
{
  "data": [
    { "id": "pod-1", "name": "web-abc123", "status": "running" },
    { "id": "pod-2", "name": "web-def456", "status": "running" }
  ],
  "pagination": {
    "next_cursor": "eyJpZCI6InBvZC0yIn0=",
    "has_more": true,
    "total_count": 47
  }
}

// Next request
GET /api/v1/pods?cursor=eyJpZCI6InBvZC0yIn0=

The agent does not need to understand what the cursor contains. It checks has_more, and if true, passes next_cursor to the next request. No arithmetic, no off-by-one errors.

Include Total Count and Has-More Flag#

Agents need to know two things: is there more data, and how much total data exists (so they can decide whether to fetch it all or summarize).

{
  "pagination": {
    "has_more": true,
    "total_count": 247,
    "returned_count": 50,
    "next_cursor": "abc123"
  }
}

If the total count is 247, the agent can decide to fetch all pages (if it needs comprehensive results) or stop after the first page (if it just needs a sample). Without total_count, the agent has no basis for this decision.

Provide a Full Next-Page URL#

Do not make the agent construct the URL. Provide it directly.

{
  "pagination": {
    "has_more": true,
    "next_url": "https://api.example.com/api/v1/pods?cursor=abc123&limit=50"
  }
}

This eliminates URL construction errors. The agent simply follows the link.

Rate Limit Communication#

Agents do not naturally respect rate limits. They call APIs as fast as they can until they get blocked. Help them by communicating limits clearly.

Response Headers#

Include rate limit headers on every response, not just on 429 responses.

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1708632000
Retry-After: 30

Structured 429 Responses#

When the agent hits the rate limit, tell it exactly how long to wait.

{
  "type": "https://api.example.com/errors/rate-limited",
  "title": "Rate limit exceeded",
  "status": 429,
  "detail": "You have exceeded 100 requests per minute.",
  "retryable": true,
  "retry_after_seconds": 30,
  "rate_limit": {
    "limit": 100,
    "window": "1 minute",
    "remaining": 0,
    "resets_at": "2026-02-22T14:30:00Z"
  }
}

The retry_after_seconds field is critical. An agent that receives a numeric wait time can sleep and retry. An agent that receives "Please try again later" will either retry immediately (making the problem worse) or give up.

Idempotency Keys#

Agents retry failed requests. If the first request succeeded but the response was lost (network timeout), the retry creates a duplicate. Idempotency keys solve this.

How It Works#

The agent sends a unique key with each mutating request. The server checks whether it has already processed that key. If so, it returns the original response without re-executing the operation.

POST /api/v1/deployments
Idempotency-Key: deploy-7f3a-2026-02-22-143000
Content-Type: application/json

{
  "service": "api",
  "image": "api:v2.3",
  "environment": "staging"
}

First call: Server processes the deployment, stores the result keyed by the idempotency key, and returns 201.

Retry (same key): Server finds the stored result and returns it without deploying again. Still returns 201.

Different operation (different key): Server treats it as a new request.

Design Considerations#

Accept idempotency keys on all POST, PUT, and PATCH endpoints. GET and DELETE are naturally idempotent and do not need them.

Return the same status code and response body for idempotent replays. If the first call returned 201, the replay must also return 201 – not 200 or 409.

Set an expiry on stored idempotency keys (24-48 hours). The agent should not rely on keys from last week still being valid.

// If the agent sends a key that was already used with different parameters
{
  "type": "https://api.example.com/errors/idempotency-mismatch",
  "title": "Idempotency key reused with different parameters",
  "status": 422,
  "detail": "Idempotency key 'deploy-7f3a' was already used with different request parameters.",
  "retryable": false,
  "suggested_action": "Generate a new idempotency key for the new request."
}

Self-Describing Responses#

Agents work better with responses that explain themselves. Include metadata that tells the agent what it is looking at and what it can do next.

Type Indicators#

{
  "_type": "Deployment",
  "_links": {
    "self": "/api/v1/deployments/deploy-abc123",
    "logs": "/api/v1/deployments/deploy-abc123/logs",
    "rollback": "/api/v1/deployments/deploy-abc123/rollback",
    "service": "/api/v1/services/api"
  },
  "id": "deploy-abc123",
  "status": "running",
  "created_at": "2026-02-22T14:30:00Z"
}

The _type field tells the agent what schema to expect. The _links tell it where to go next. An agent looking at a failed deployment can follow the logs link without knowing the URL structure.

Enum Values in Responses#

When a field has a fixed set of values, document them in the response or provide a way to list them.

{
  "status": "running",
  "_enum_values": {
    "status": ["pending", "running", "succeeded", "failed", "cancelled"]
  }
}

This lets the agent reason about state transitions without consulting documentation. If the status is "running", the agent knows the possible next states and can check for the right one.

Machine-Readable Documentation#

Agents can consume OpenAPI specifications to understand an API without human-written docs. But the spec must be detailed enough.

Effective OpenAPI Annotations#

paths:
  /api/v1/deployments:
    post:
      operationId: createDeployment
      summary: Deploy a service to the specified environment.
      description: |
        Creates a new deployment. The deployment runs asynchronously.
        Poll the deployment status at the returned URL until it reaches
        a terminal state (succeeded, failed, or cancelled).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [service, image, environment]
              properties:
                service:
                  type: string
                  description: "Service name as registered in the service catalog."
                  example: "api"
                image:
                  type: string
                  description: "Full image reference including tag. Tag must exist in the registry."
                  example: "us-docker.pkg.dev/myproject/api:v2.3"
                environment:
                  type: string
                  enum: [production, staging, development]
                  description: "Target environment. Production deployments require approval."
      responses:
        "201":
          description: Deployment created successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Deployment"
        "404":
          description: Service or image not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
              example:
                type: "https://api.example.com/errors/image-not-found"
                title: "Container image not found"
                status: 404
                retryable: false
                suggested_action: "List available tags using GET /api/v1/images/{service}/tags"

Key elements that make OpenAPI agent-friendly:

  • operationId gives every endpoint a unique, stable identifier the agent can reference.
  • description on the operation explains what happens after the call (polling behavior, async execution).
  • example values on every field. The agent uses these as templates for constructing requests.
  • Error response schemas with examples. The agent learns the error format before encountering a real error.
  • enum values on constrained fields. The agent cannot submit an invalid environment name.

Discovery Endpoint#

Expose the OpenAPI spec at a well-known path so agents can discover it automatically.

GET /.well-known/openapi.json
GET /api/v1/openapi.json

Good vs Bad Patterns: Summary#

Aspect Bad for Agents Good for Agents
Errors {"error": "failed"} RFC 7807 with type, retryable flag, suggested action
Pagination ?page=3&offset=40 Cursor-based with has_more, next_url, total_count
Rate limits Silent 429 with no retry info retry_after_seconds with remaining quota headers
Mutations No idempotency protection Idempotency key support on all mutating endpoints
Responses Flat JSON with no context Type indicators, links to related resources, enum values
Documentation Prose-only docs OpenAPI spec with examples and error schemas
Status values "status": "2" "status": "running" with possible values documented
Timestamps Unix timestamps ISO 8601 with timezone (2026-02-22T14:30:00Z)
IDs Sequential integers Prefixed strings (deploy-abc123) that indicate resource type

The common thread: reduce ambiguity. Every field that forces the agent to guess is a potential failure point. Every response that tells the agent what to do next is a success path. Design for the consumer that cannot read between the lines.