Agent-era architecture · Practical technical guide · 17 October 2026

What a service owner should change when AI agents start calling an existing HTTP API: preview endpoints, required idempotency keys, machine-actionable errors, bounded responses, per-principal budgets and contract tests.

A new client with old expectations

Most services that agents call were designed for two kinds of clients: a user interface written by the same team, and integrations written by engineers who read the documentation once and then hard-coded the happy path. An agent is neither. It reads the API description at run time, decides which call to make from natural language, retries when something looks wrong, and treats every response body as more context. It is also often acting for a person who is not watching each call.

This changes what the API contract needs to say, but it does not change who enforces it. The service still validates, authorizes and decides. The work for a service owner is to make the contract explicit enough that a model can use it correctly, and strict enough that when the model gets it wrong, the server catches it cheaply. The consumer-side view, turning an OpenAPI document into tools, is covered in ADK Java and OpenAPI; this article is about the server side.

What stays deterministic on the server

ConcernStays in the serviceChanges for agent callers
Validation and business rulesYes, unchangedError bodies say which field failed and what would be valid
AuthorizationYes, per user and per scopeAgent principal plus on-behalf-of user, both checked
IdempotencyYesKeys become required on every mutating call
Rate limitsYesBudgets per agent principal and per run, not only per IP
Pagination and response sizeYesHard caps and explicit continuation handles
Dangerous operationsYesA preview step that returns the effect before it happens

No row moves a decision into the model. The column on the right is about making the server's existing decisions legible and making unsafe client behaviour fail early.

Previews: let the caller see the effect first

The most useful addition for agent callers is a preview of a mutating operation. A preview runs the same validation and pricing logic as the real call and returns what would happen: the rows that would change, the amount that would be charged, the notifications that would be sent. It does not commit. Many teams already have this logic hidden behind a "confirm" screen in their own UI; exposing it as an endpoint lets an agent show a human exactly what it is about to do.

Bind the preview to the commit. The preview returns a short-lived token that encodes a hash of the normalized request. The commit must present that token, and the server re-validates and checks the hash. An agent cannot preview a small refund and commit a large one, and a human who approved the preview approved those exact arguments.

Illustrative python, checked for syntax; adapt names and storage to your system.

import hashlib, hmac, json, time

SECRET = b"rotate-me"
TTL = 300

def canonical(body: dict) -> bytes:
    return json.dumps(body, sort_keys=True, separators=(",", ":")).encode()

def issue_preview_token(principal: str, body: dict, now: float) -> str:
    digest = hashlib.sha256(canonical(body)).hexdigest()
    payload = f"{principal}|{digest}|{int(now) + TTL}"
    sig = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()
    return payload + "|" + sig

def verify_preview_token(token: str, principal: str, body: dict, now: float) -> bool:
    who, digest, expires, sig = token.split("|")
    payload = f"{who}|{digest}|{expires}"
    good = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()
    return (hmac.compare_digest(sig, good) and who == principal
            and int(expires) >= now
            and digest == hashlib.sha256(canonical(body)).hexdigest())

body = {"order_id": "o-81", "refund_cents": 4500}
tok = issue_preview_token("agent:support/user:42", body, time.time())
assert verify_preview_token(tok, "agent:support/user:42", body, time.time())
assert not verify_preview_token(tok, "agent:support/user:42",
                                {"order_id": "o-81", "refund_cents": 9900}, time.time())

Idempotency keys stop being optional

Agents retry more than other clients. A tool runtime may time out and call again, a model may repeat a call it already made because the first result scrolled out of its context, and an orchestrator may resume a run after a crash. If mutating endpoints accept an optional Idempotency-Key header, make it mandatory for agent principals and reject calls without it with a clear error.

Store the key with a fingerprint of the request body and the response. A repeat with the same key and the same body returns the stored response. A repeat with the same key and a different body is a client bug and should fail loudly rather than execute. Tell agent integrators to derive keys from intent, such as run id plus step id, so that a resumed run reuses them. This is the same contract payment APIs have used for years; see idempotency architecture for the storage design.

Errors a model can act on

Human-facing errors such as "Something went wrong" are useless to an agent, and stack traces are worse because they spend context and leak internals. Return a small, stable structure: a machine code, the field at fault, whether a retry could succeed, and what a valid request would look like. RFC 9457 problem details is a reasonable envelope; the extension fields are the part that matters.

SituationStatusCodeRetryableHint for the caller
Missing idempotency key400idempotency_key_requirednoSend Idempotency-Key derived from the step
Amount above limit422refund_exceeds_order_totalnoMaximum is the order total, 4500
Stale version409version_conflictyes, after re-readFetch the order and retry with its version
Budget exhausted429run_budget_exhaustedno for this runAsk a human or wait for the next window
Upstream timeout503dependency_unavailableyes, with backoffRetry-After: 2

The retryable flag prevents a common failure: an agent that treats a validation error as transient and retries the same bad request until a budget stops it.

Bounded responses and continuation handles

A list endpoint that returns 5,000 rows is merely slow for a UI. For an agent it is expensive, because every token of the response is read and billed, and harmful, because the instructions that matter get pushed out of the context window. Put hard caps on page size for agent principals, return only the fields the operation is about by default, and include a continuation handle and a total count so the caller knows what it is not seeing. A short summary field, generated by the server, often lets the agent decide without fetching more pages.

Identity and budgets per principal

Authenticate the agent as its own principal and carry the user it acts for, then authorize both: the agent must be allowed to use the operation, and the user must be allowed to have it done. Budgets belong to that pair and to the run: calls per minute, mutating calls per run, and a money or quantity ceiling per day. When a budget trips, return the structured 429 above. These limits are cheap insurance against a loop, and they make cost attributable. Permission boundaries for autonomous agents covers scoping the credentials themselves.

Migration steps

  1. Find the agent traffic you already have: user agents, token audiences, call patterns with no UI session. It usually exists before anyone plans for it.
  2. Publish an accurate machine-readable description of the subset of operations agents may use, with descriptions written for a reader who cannot ask questions.
  3. Require idempotency keys for agent principals on every mutating route and add the body fingerprint check.
  4. Add preview endpoints for the operations with money, deletion or external messages, and bind commits to preview tokens.
  5. Convert error responses to the structured form and add the retryable flag.
  6. Introduce per-principal and per-run budgets, first in report-only mode, then enforced.
  7. Add contract tests that replay recorded agent traffic against new releases.

When this is unnecessary

If the only agent calling your service is a fixed workflow written by your own team, with each call hard-coded and reviewed, you are dealing with an ordinary integration and the usual contract is enough. The changes above pay off when the caller chooses operations dynamically, when third-party agents can reach the API, or when calls have effects that are expensive to reverse. Read-only endpoints with small responses need little beyond the error and budget work.

Failure modes and how to verify

  • Preview and commit drift. If the two paths use different code, the preview lies. Test that for random valid bodies the preview effect equals the committed effect.
  • Key reuse with a different body. Assert that it returns an error and changes nothing.
  • Retry loops on 4xx. Replay a recorded agent session against a staging server that always returns 422 and check the budget stops it.
  • Oversized responses. A contract test that every list route enforces its agent page cap.
  • Confused identity. A test where the agent may use the operation but the user may not, which must be denied.

Sources and scope

Checked against these sources on 17 October 2026. Examples and figures are illustrative, not measurements.

Agent callers need previews bound to commits, mandatory idempotency keys, errors that say whether a retry can help, bounded responses and budgets per principal and run.