Connecting agents to your APIs: identity, idempotency and the failures nobody plans for
An agent team is only as good as its connection to the systems that hold the work. These are the integration rules we apply when agents start calling a company's CRM, ERP, helpdesk and internal APIs: who they act as, what happens on a retry, and how failures are made legible.
The integration is most of the work
Demos run against toy data. Production agents run against a CRM with fifteen years of exceptions in it, an ERP that rejects a posting for reasons no one can explain, and an internal API whose documentation stopped matching reality in 2022. The model is rarely the hard part. The integration is.
Everything below assumes agents reach systems through typed tools, usually exposed as MCP servers, rather than by being handed raw HTTP access. That boundary is what makes the rest of this enforceable.
Identity: who is the agent acting as?
Answer this before writing a line of integration code. Three options, in descending order of how much we like them:
- On behalf of a person. The agent uses a token scoped to the requesting user, so existing permissions apply unchanged and the audit trail in the target system names a real human. Best when it is available.
- A dedicated service identity per agent role. The posting agent has its own account with exactly the rights to post, and nothing else. Logs in the ERP show which agent role acted.
- One shared service account for everything. Common, and the source of most integration incidents. Every agent inherits every permission, and the audit trail says only "integration user".
Whichever you pick, the credential lives in the tool server, never in a prompt, never in the model's context, and never in a log line.
Scopes follow the authority table
The permission model you drew during agent team design should map one to one onto API scopes. If the matching agent may read purchase orders and change nothing, its credential has read scope on purchase orders and no write scope anywhere. Do not rely on the tool implementation to be the only thing standing between an agent and a destructive call: the server should be unable to perform the call, not merely unwilling.
The practical form of this is a read server and a write server, separately credentialed. Read-only agents connect to the first and cannot reach the second.
Idempotency: the retry problem
Agents retry. Networks time out, models produce a malformed argument and try again, an orchestrator re-plans after a partial failure. Without idempotency this means duplicate invoices, duplicate tickets, duplicate emails.
Every write tool needs a deterministic idempotency key derived from the business fact, not from the attempt. For an invoice posting, something like supplier_id + invoice_number; for a reply, the ticket id plus a content hash. The tool passes it to the API where the API supports it, and where it does not, the tool keeps its own record of keys already committed and returns the previous result instead of acting twice.
post_invoice(
idempotency_key = "SUP-4412:INV-2026-0918",
...
)
# second call with the same key returns the first result,
# marked as a replay, and changes nothing
Make the replay visible in the return value. An agent that cannot tell "I posted it" from "it was already posted" will narrate the wrong thing to a human later.
Typed errors, not prose
An API that answers a bad request with a 400 and an HTML error page gives the agent nothing to act on, so it guesses. Tools should translate every failure into a small, closed set of typed errors that say what to do next:
not_found: the record does not exist. Stop, report.validation_failed: with the offending field. Fix and retry once.permission_denied: never retry, escalate.conflict: someone else changed the record. Re-read, then decide.rate_limited: with a wait hint. Back off.upstream_unavailable: retry with backoff, then escalate.
Each error should carry a human-readable message for the escalation path and a machine-readable code for the agent. The difference between an agent that recovers and an agent that loops is almost always the quality of this taxonomy.
Rate limits and backpressure
An agent can generate a year of a human's API traffic in an afternoon. Assume you will hit limits and design for it: a concurrency cap per tool server, exponential backoff with jitter, a queue with a visible depth, and a circuit breaker that stops a run rather than hammering a degraded system. Tell the orchestrator when work is deferred, so it reports "waiting on the ERP" instead of silently stalling.
Reads are not free either
Large result sets are a context problem, not just a cost problem. Tools should paginate, accept filters that push work to the API, and return summaries with a handle for detail rather than dumping ten thousand rows into an agent's context. A good read tool answers "which invoices are unmatched this week" with twenty rows and a count, not with the table.
Events coming the other way
Most integrations eventually need inbound events: a ticket created, an invoice received, a record changed. Treat webhooks as a first-class part of the integration, with the same discipline as outbound calls: verify signatures, deduplicate by event id, accept fast and process asynchronously, and make replay safe. An agent team that reacts to events needs the same idempotency guarantees as one that writes.
Test against something real
Mocked integrations pass and production fails, because the interesting behaviour lives in the exceptions. Get a sandbox tenant if the vendor offers one, a replica if they do not, and build the evaluation set from real historical cases including the ones that errored. Every typed error above should appear in at least one test case, with an assertion about what the agent does next.
Integration checklist
- Identity decided and scoped per agent role; credentials only in the tool server.
- Read and write separated, with separate credentials.
- Idempotency key on every write, replays visible in the response.
- Closed set of typed errors with a defined agent reaction to each.
- Backoff, concurrency cap and circuit breaker on every external call.
- Pagination and filtering on every read; no unbounded result into context.
- Inbound events deduplicated and signature-verified.
- Evaluation set built from real cases, including failures.
- Every call logged with actor, arguments, result and duration.
None of this is specific to AI. It is ordinary integration engineering, applied to a caller that is faster, more literal and less embarrassed to retry than any human user you have integrated for before.
Frequently asked questions
Should agents call our APIs directly instead of through MCP servers?
We do not recommend it. A tool server is where permissions, idempotency, error translation, rate limiting and logging live. Handing an agent raw HTTP access means every one of those concerns has to be enforced by prompt instructions, which is not enforcement.
What if the internal API has no idempotency support?
Then the tool server provides it: keep a record of committed idempotency keys with the result of the original call, and return that result on a repeat instead of calling the API again. It is a small table and it prevents the most expensive class of agent incident.
How do we handle APIs that are slow or frequently down?
Treat unavailability as a normal path, not an exception. Queue the work, report the delay to the orchestrator so the run shows as waiting rather than failed, apply backoff with a circuit breaker, and define an escalation after a threshold so a person learns about it before the customer does.
We look at the process, design the agent team it needs, and decide together whether it is worth building. No packages, no price list.
Get in touch → Book a call