Designing API Errors That AI Agents Can Actually Recover From

“Make your API failures useful to both developers and autonomous software without leaking sensitive implementation details”

Why API Error Design Matters More in the Age of AI Agents

Most developers spend far more time designing successful API responses than failed ones.

A successful request receives clean JSON. An error often receives something like:

{
    "error": "Something went wrong"
}

That may be enough for a human developer who can open logs, inspect source code, and retry manually. It is much less useful for an autonomous software agent that is expected to diagnose the problem and decide what to do next.

Modern AI agents increasingly interact with APIs, developer tools, databases, business services, and internal applications. When one API call fails, the agent needs to determine whether it should correct its request, ask the user for missing information, wait and retry, refresh authentication, or stop completely.

The quality of your error design directly affects whether that recovery is possible.

The Problem With Human-Only Error Messages

Consider this API response:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
    "message": "Invalid request"
}

A human developer knows something was wrong, but even the human does not know exactly what.

An automated client has even less information.

Was a required field missing?

Was an email address malformed?

Was the requested plan unavailable?

Did the client send a string where an integer was expected?

Should the request be retried?

The answer is impossible to determine safely.

The client may start guessing, which is exactly what an API should prevent.

Use HTTP Status Codes for the First Layer of Meaning

Before inventing custom JSON fields, use HTTP correctly.

The status code gives clients a broad category of failure.

Common examples include:

  • 400 Bad Request for malformed or invalid requests
  • 401 Unauthorized when valid authentication is required
  • 403 Forbidden when authentication exists but access is not allowed
  • 404 Not Found when a resource cannot be found
  • 409 Conflict when the request conflicts with current resource state
  • 422 Unprocessable Content for semantically invalid request content
  • 429 Too Many Requests for rate limiting
  • 500 Internal Server Error for unexpected server failures
  • 503 Service Unavailable for temporary service availability problems

An API should not return HTTP 200 simply because the server successfully generated an error object.

Generic HTTP software, reverse proxies, SDKs, monitoring tools, and agents all benefit from correct status semantics.

RFC 9457 Gives APIs a Standard Error Shape

RFC 9457 defines Problem Details for HTTP APIs.

Its purpose is simple: APIs should not need to invent a completely new error format for every service.

The standard JSON media type is:

application/problem+json

A problem response can contain fields including:

  • type - a URI reference identifying the problem category
  • title - a short human-readable summary
  • status - the HTTP status associated with the problem
  • detail - an explanation specific to this occurrence
  • instance - an identifier for this particular occurrence

RFC 9457 also allows applications to define extension members when additional structured information is needed. :contentReference[oaicite:1]{index=1}

A Better Error Response

Suppose an application accepts a registration request.

The client sends:

{
    "email": "john@example.com",
    "age": -5
}

A machine-friendly response could look like:

HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
    "type": "https://api.example.com/problems/validation-error",
    "title": "The request contains invalid fields.",
    "status": 422,
    "detail": "One or more fields could not be accepted.",
    "instance": "/requests/01J8Y8M9Q4",
    "errors": [
        {
            "field": "age",
            "code": "value_too_small",
            "message": "Age must be 0 or greater."
        }
    ]
}

Now an automated client has enough information to act deliberately.

It does not need to guess which field failed.

Stable Error Codes Are More Important Than Clever Error Text

AI agents understand natural language, but your API should not depend on natural-language interpretation for core behavior.

Human text changes.

You may rewrite:

"Age must be 0 or greater."

as:

"Please provide a non-negative age."

A program that tries to detect error meaning from those sentences is fragile.

Instead, provide a stable identifier:

{
    "code": "value_too_small"
}

The client can make decisions from the code while showing the message to a human.

A Good Rule

Humans read title, detail, and field messages.

Machines rely on documented values such as:

  • HTTP status
  • problem type
  • error code
  • field
  • retryable
  • allowed values

RFC 9457 specifically warns consumers not to parse the human-readable detail field for structured information. Extension members are better suited for machine processing. :contentReference[oaicite:2]{index=2}

Design Errors Around Recoverable Actions

When designing an API for autonomous software, ask one useful question:

What can the client reasonably do after receiving this error?

This produces better error categories.

Correct the Request

{
    "type": "https://api.example.com/problems/invalid-date",
    "status": 422,
    "code": "invalid_date_format",
    "field": "start_date",
    "expected_format": "YYYY-MM-DD"
}

The agent can correct the formatting and send the request again.

Choose From Allowed Values

{
    "type": "https://api.example.com/problems/unsupported-plan",
    "status": 422,
    "code": "unsupported_plan",
    "field": "plan",
    "allowed_values": [
        "starter",
        "professional",
        "business"
    ]
}

The agent now has valid alternatives instead of inventing another value.

Ask the User

{
    "type": "https://api.example.com/problems/missing-information",
    "status": 422,
    "code": "required_field_missing",
    "field": "shipping_country"
}

If the agent cannot infer the country safely, it can ask the user.

Stop

{
    "type": "https://api.example.com/problems/access-denied",
    "status": 403,
    "code": "permission_denied",
    "retryable": false
}

The agent should not repeatedly retry an operation it is not authorized to perform.

Make Retry Behavior Explicit

One of the worst behaviors in autonomous software is a retry loop with no understanding of why the previous attempt failed.

Suppose your API is temporarily overloaded.

A useful response might be:

HTTP/1.1 503 Service Unavailable
Retry-After: 30
Content-Type: application/problem+json

{
    "type": "https://api.example.com/problems/service-busy",
    "title": "The service is temporarily busy.",
    "status": 503,
    "code": "service_temporarily_unavailable",
    "retryable": true
}

The client now knows that retrying is reasonable.

Contrast that with a validation problem:

{
    "code": "invalid_email",
    "retryable": false
}

Sending the exact same request again will not fix an invalid email address.

Do Not Make Every Error Retryable

AI agents can easily create unnecessary traffic if retry logic is too aggressive.

A simple strategy can classify failures.

Failure Typical Action
Invalid field Correct request
Missing information Ask user or supply value
Authentication expired Refresh authentication if allowed
Permission denied Stop
Rate limited Wait, then retry
Temporary outage Retry with backoff
Unknown server failure Limited retry, then escalate

The exact policy belongs to your application, but the error response should give the client enough information to implement it.

Use Request IDs Without Exposing Internals

When a production failure occurs, developers often need to connect the public error with server logs.

Do not expose a complete stack trace.

Return a safe request identifier instead.

{
    "type": "https://api.example.com/problems/internal-error",
    "title": "The request could not be completed.",
    "status": 500,
    "detail": "An unexpected server error occurred.",
    "request_id": "req_7F31C2A9"
}

Your server logs can contain:

request_id=req_7F31C2A9
exception=PDOException
database_error=...
stack_trace=...

The client receives only the safe identifier.

Your support or engineering team can use that identifier to locate the detailed internal event.

Never Return Raw Exceptions to API Clients

This PHP code is convenient during development:

catch (Throwable $e) {

    echo json_encode([
        'error' => $e->getMessage(),
        'file' => $e->getFile(),
        'line' => $e->getLine()
    ]);

}

It is a bad production error strategy.

Raw exceptions may reveal:

  • Database structure
  • SQL queries
  • Server paths
  • Framework internals
  • Third-party service details
  • Unexpected secrets

RFC 9457 includes explicit security considerations warning that problem details should not expose implementation information that creates new attack opportunities. :contentReference[oaicite:3]{index=3}

A Small PHP Problem Details Helper

You can centralize public API errors instead of manually constructing inconsistent JSON in every endpoint.

<?php

function problemResponse(
    int $status,
    string $type,
    string $title,
    string $detail,
    array $extensions = []
): never {

    http_response_code($status);

    header(
        'Content-Type: application/problem+json; charset=utf-8'
    );

    $problem = array_merge(
        [
            'type' => $type,
            'title' => $title,
            'status' => $status,
            'detail' => $detail
        ],
        $extensions
    );

    echo json_encode(
        $problem,
        JSON_UNESCAPED_SLASHES
        | JSON_UNESCAPED_UNICODE
    );

    exit;
}

An endpoint can then return a controlled validation problem:

<?php

$email = trim(
    $_POST['email'] ?? ''
);

if (
    !filter_var(
        $email,
        FILTER_VALIDATE_EMAIL
    )
) {

    problemResponse(
        422,
        'https://api.example.com/problems/invalid-email',
        'The email address is invalid.',
        'Provide a valid email address.',
        [
            'code' => 'invalid_email',
            'field' => 'email',
            'retryable' => false
        ]
    );
}

This keeps your public error contract independent from internal exceptions.

Handling Multiple Validation Errors

A form or API request may contain several invalid fields at once.

Returning only the first problem forces the client into a slow sequence:

Send request
↓
Fix first field
↓
Send again
↓
Discover second problem
↓
Fix second field
↓
Send again

For related validation problems, returning structured field errors can improve the experience.

{
    "type": "https://api.example.com/problems/validation-error",
    "title": "The request contains invalid fields.",
    "status": 422,
    "errors": [
        {
            "field": "email",
            "code": "invalid_email"
        },
        {
            "field": "age",
            "code": "value_too_small",
            "minimum": 18
        }
    ]
}

RFC 9457 includes an example of a validation problem that extends the standard format with an array describing multiple field-level issues. :contentReference[oaicite:4]{index=4}

Document Problem Types Like Normal API Endpoints

A stable problem type becomes much more useful when it has documentation.

For example:

https://api.example.com/problems/insufficient-credit

The documentation page could explain:

  • What causes the error
  • Which HTTP status is returned
  • Whether retrying makes sense
  • Which extension fields are present
  • How a client can resolve the problem

RFC 9457 recommends that HTTP or HTTPS problem type URIs provide human-readable documentation when dereferenced. :contentReference[oaicite:5]{index=5}

That gives developers a stable contract without stuffing every explanation into every response.

Do Not Use a New Error Type for Every Tiny Failure

Structured errors can become too complicated.

Imagine an API with:

invalid-email-format
invalid-email-domain
invalid-email-length
email-uppercase-not-allowed
email-temporary-provider
email-account-disabled

Clients now need to understand dozens of nearly identical problem types.

Prefer a smaller set of meaningful problem categories and use extensions for specific details.

For example:

type:
validation-error

code:
invalid_email_domain

The type describes the general problem. The code gives application-specific detail.

Keep Error Contracts Backward Compatible

Once SDKs, agents, and third-party applications depend on your error format, changing it can break integrations.

Avoid changing:

"code": "rate_limited"

into:

"error_code": "too_many_calls"

without versioning or a migration plan.

Human-facing text can change more freely because machines should not depend on it.

This is another reason stable machine-readable fields matter.

Avoid Ambiguous Authentication Errors

Authentication systems deserve careful error design.

You want useful machine behavior without exposing information attackers can exploit.

For example, a login endpoint should be cautious about returning:

{
    "code": "email_exists_but_password_wrong"
}

That may confirm that a particular account exists.

In public authentication flows, a more generic response may be appropriate:

{
    "type": "https://api.example.com/problems/authentication-failed",
    "title": "Authentication failed.",
    "status": 401,
    "code": "authentication_failed"
}

The principle is important: machine readability should not override security boundaries.

Separate User-Correctable Errors From System Failures

AI agents work better when they can distinguish problems they can fix from problems outside their control.

User-Correctable

{
    "status": 422,
    "code": "missing_shipping_address",
    "action": "provide_required_field"
}

System Failure

{
    "status": 503,
    "code": "payment_provider_unavailable",
    "action": "retry_later"
}

The client should not attempt to invent a shipping address, and it should not rewrite payment data to solve an unavailable upstream service.

Clear error semantics reduce unsafe guesses.

Consider Idempotency Before Automatic Retries

Retry behavior becomes dangerous when an operation has side effects.

Consider:

POST /payments

The server successfully creates a payment, but the network connection closes before the client receives the response.

An automated agent may conclude that the call failed and submit it again.

Without an idempotency strategy, the user could potentially be charged twice.

For operations where retries are expected, design an idempotency mechanism or another safe duplicate-detection strategy.

Error handling and retry design should therefore be considered together.

Do Not Let AI Agents Guess Missing Business Rules

Suppose an API returns:

{
    "message": "Quantity unavailable"
}

An agent might try quantities 9, 8, 7, 6, and so on.

If your API already knows the maximum allowed value, tell the client directly:

{
    "type": "https://api.example.com/problems/quantity-unavailable",
    "status": 409,
    "code": "quantity_exceeds_available_stock",
    "requested": 10,
    "available": 4
}

The client can now make an informed decision.

It might ask the user whether purchasing four units is acceptable rather than blindly modifying the order.

Error Information Should Be Useful, Not Excessive

More JSON is not automatically better.

This response is structured but unnecessarily noisy:

{
    "status": 422,
    "error": true,
    "success": false,
    "failed": true,
    "has_error": true,
    "message": "Invalid email",
    "error_message": "Invalid email"
}

Every field should have a purpose.

A cleaner response is easier for humans and machines:

{
    "type": "https://api.example.com/problems/invalid-email",
    "title": "The email address is invalid.",
    "status": 422,
    "code": "invalid_email",
    "field": "email"
}

Log More Than You Return

Public error responses and internal logs serve different audiences.

Your API response should provide enough information to correct the request safely.

Your internal logs may contain operational details required for debugging.

A useful architecture is:

Application Error
       |
       +--------------------+
       |                    |
       v                    v
Public Problem         Internal Log
       |                    |
Safe details           Exception
Error code             Stack trace
Request ID             Database context
                       Service response

This lets developers investigate problems without leaking internals to every API client.

Test Failure Responses, Not Only Success Responses

API tests often concentrate on the happy path.

Error contracts deserve automated tests too.

Useful tests include:

  • Missing required fields
  • Incorrect data types
  • Invalid authentication
  • Insufficient permissions
  • Missing resources
  • Duplicate operations
  • Rate limiting
  • Temporary upstream failures

You can test both the HTTP status and the machine-readable error code.

assertSame(
    422,
    $response->status()
);

assertSame(
    'invalid_email',
    $response->json('code')
);

This prevents accidental API changes from silently breaking clients.

A Practical Error Design Checklist

  • Return the correct HTTP status
  • Use a consistent problem format
  • Provide stable machine-readable error types
  • Keep human text separate from machine logic
  • Include field-level validation information where useful
  • Tell clients when retrying is appropriate
  • Use Retry-After when applicable
  • Provide safe request identifiers for support
  • Never expose raw stack traces
  • Do not expose database queries or credentials
  • Document custom error codes
  • Keep the error contract backward compatible
  • Test common failure scenarios
  • Design idempotency for retryable side-effect operations

Why This Matters Beyond AI

Although autonomous agents make structured errors more important, the same design improves normal software development.

A well-designed problem response helps:

  • Mobile applications
  • Frontend JavaScript
  • CLI tools
  • SDKs
  • Automation scripts
  • Third-party integrations
  • Monitoring systems
  • Human developers

The goal is not to create an API specifically for AI.

The goal is to create a precise API contract that any responsible client can understand.

Final Thoughts

AI agents make a weakness in many APIs easier to see: successful responses are carefully structured, while failures are often vague strings.

If an autonomous client is expected to recover safely from errors, it needs reliable information about what failed and what actions are reasonable next.

Start with correct HTTP semantics. Use RFC 9457 when a standardized problem representation fits your API. Give machine consumers stable identifiers instead of forcing them to interpret changing English sentences.

When recovery is possible, return enough structured information to guide it. When retrying is appropriate, make that clear. When an operation must stop, say so without exposing sensitive internals.

Most importantly, keep your public API errors separate from debugging information.

A useful API error should help the client solve the problem, not teach an attacker how your server is built.

Sources

Common questions

Frequently asked questions

What makes an API error useful for AI agents?

A useful error includes the correct HTTP status, a stable machine-readable error type, a concise explanation, and structured fields that tell the client what can be corrected or retried.

What is RFC 9457?

RFC 9457 is an IETF standard for HTTP API problem details. It defines a common error object with fields such as type, title, status, detail, and instance.

Should an AI agent parse the detail message?

No. The detail field is intended for humans. Machine decisions should rely on stable fields such as type, status, error codes, and documented extensions.

Can I add custom fields to a problem details response?

Yes. RFC 9457 allows extension members, so an API can add fields such as retryable, errors, request_id, or allowed_values when they are clearly documented.

Should every API error return HTTP 200?

No. The actual HTTP status should represent the error correctly. A validation failure, authentication failure, rate limit, or server error should use the appropriate HTTP status.

Can structured errors leak sensitive information?

Yes. Error responses should not expose stack traces, database queries, credentials, internal file paths, or other implementation details that could help an attacker.

How should rate limit errors be returned?

Use the appropriate HTTP status, usually 429, and provide structured information about retry behavior. When appropriate, HTTP Retry-After can tell clients when another attempt may succeed.

Should PHP APIs expose raw exception messages?

Usually no. Internal exceptions should be logged securely while public API responses return controlled, documented error information.

Shanawar AliFounder and developer at S Pro Coder, sharing practical coding and technology guides.