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
| Concern | Stays in the service | Changes for agent callers |
|---|---|---|
| Validation and business rules | Yes, unchanged | Error bodies say which field failed and what would be valid |
| Authorization | Yes, per user and per scope | Agent principal plus on-behalf-of user, both checked |
| Idempotency | Yes | Keys become required on every mutating call |
| Rate limits | Yes | Budgets per agent principal and per run, not only per IP |
| Pagination and response size | Yes | Hard caps and explicit continuation handles |
| Dangerous operations | Yes | A 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.
| Situation | Status | Code | Retryable | Hint for the caller |
|---|---|---|---|---|
| Missing idempotency key | 400 | idempotency_key_required | no | Send Idempotency-Key derived from the step |
| Amount above limit | 422 | refund_exceeds_order_total | no | Maximum is the order total, 4500 |
| Stale version | 409 | version_conflict | yes, after re-read | Fetch the order and retry with its version |
| Budget exhausted | 429 | run_budget_exhausted | no for this run | Ask a human or wait for the next window |
| Upstream timeout | 503 | dependency_unavailable | yes, with backoff | Retry-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
- 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.
- Publish an accurate machine-readable description of the subset of operations agents may use, with descriptions written for a reader who cannot ask questions.
- Require idempotency keys for agent principals on every mutating route and add the body fingerprint check.
- Add preview endpoints for the operations with money, deletion or external messages, and bind commits to preview tokens.
- Convert error responses to the structured form and add the retryable flag.
- Introduce per-principal and per-run budgets, first in report-only mode, then enforced.
- 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.
Practice and related reading
Further reading:
Sources and scope
Checked against these sources on 17 October 2026. Examples and figures are illustrative, not measurements.