A growing share of the traffic hitting our API isn't typed by anyone. Not scripts either — agents: a model reading the docs, writing an integration, calling endpoints to check its own work, and retrying whenever something comes back ambiguous.
Most of that is fine. What it exposes is your defaults, and a lot of ours turned out to have been chosen casually.
The stakes are unusually literal in our case, because our writes are permanent by law. This is an invoicing API for Spain: every production invoice becomes a fiscal record, hashed and chained to the one before it and submitted to the tax agency. You can't delete one. You can only issue a second record stating the first was wrong, and that one is permanent too.
So we spent a while rethinking the surface for a caller that never gets tired. Most of what came out has nothing to do with tax.
Agents don't make mistakes once
A developer who runs the wrong command feels it immediately, stops, and thinks. An agent that runs the wrong command has already run it eleven more times while it works out what the error message meant. Whatever your worst-case single call is, an agent will find it and multiply it.
So the first thing we did was make the dangerous environment opt-in rather than default. Our CLI runs against sandbox unless you pass --live. And crucially, a live key in the environment doesn't quietly upgrade you:
export BEEL_API_KEY=beel_sk_live_...
beel invoices issue inv_123 # error, not a silent production write
beel invoices issue inv_123 --live # explicit
That error looks unhelpful if you read it as a human. It's exactly right if you read it as an agent, because the agent's next move is to reason about the flag rather than to discover afterwards that it filed forty invoices with the tax agency.
The same logic pushed us to attach an Idempotency-Key to every POST automatically. Retrying is what agents do when something looks ambiguous, and they retry faster than a human can intervene.
Machine-readable errors beat readable ones
Prose errors are a trap. An agent parsing "something went wrong, please check your configuration" will guess, and its guess will be confident.
We ended up with two channels. Data goes to stdout as JSON, so it pipes into jq. Errors go to stderr, also as JSON, carrying a code, a message, an HTTP status and a request ID. On top of that, exit codes are typed by class rather than being 0-or-1:
| Exit code | Meaning |
|---|---|
0 |
Success |
2 |
Usage or config error |
3 |
Auth |
4 |
Not found |
5 |
Validation |
6 |
Rate limit |
7 |
Server error |
The distinction that matters is between "retry this" and "stop, you're wrong". A 6 means back off. A 5 means the payload is wrong and hammering it will never help. Collapsing both into 1 is what produces those loops where an agent tries the same broken request twenty times.
Don't make the model guess your surface
The usual failure mode with tooling is a hand-maintained command list that drifts from the actual API. An agent reads it, believes it, and calls something that no longer exists.
Our commands are derived from the OpenAPI spec at build time, and there's one command that dumps the whole surface as JSON:
beel commands # { command, signature, description, method, path } for everything
beel invoices create --help
One call, whole API, no scraping of help text. And because the spec is bundled with the installed version rather than fetched live, there's an obvious hole: an endpoint newer than your CLI. So there's a generic escape hatch that doesn't pretend otherwise.
beel request POST /v1/customers --data @customer.json
We've come to treat an escape hatch as a requirement rather than a nicety in agent tooling. The alternative is a model that concludes something is impossible because a wrapper hasn't caught up.
Plugins go stale, which makes agents confidently wrong
We ship a Claude Code plugin. The obvious version of that is to bake the API reference into the plugin, and the obvious version is wrong: the day you publish it, it starts decaying, and a stale reference is worse than no reference because it reads authoritative.
What's local now is only the part that doesn't change: authentication, idempotency, the response envelope, the invoice lifecycle. Endpoints, schemas and event names get fetched from the docs at the moment they're needed. We publish /llms.txt and /llms-full.txt for exactly that, plus the raw OpenAPI spec.
The split turns out to be a decent rule of thumb. Invariants can live in the tool. Anything with a version number should be fetched.
There's a related detail we like more than it probably deserves: beel docs search downloads the docs once, caches them, and filters on your machine. The agent gets the matching section, and the query — which often contains the shape of whatever the user is building — never reaches our servers.
Verification is the whole game
The part that changed our defect rate wasn't the docs. It was that an agent can prove an integration works before anyone ships it.
npx @beel_es/cli invoices create --data @invoice.json
npx @beel_es/cli invoices get <invoice_id>
With a test key, a model can create invoices, trigger the resulting events, and confirm a webhook receiver actually handles them, all against a sandbox that's free and unlimited. No throwaway scripts, no "looks right to me" as the last check before production.
That's the argument for a free, unlimited, genuinely complete sandbox that we'd underrated: it isn't a generosity feature, it's the thing that lets an automated caller close its own feedback loop.
The part I'd take to any API
If you're building something an agent might call, the questions worth asking are pretty small in number. Can a single call do irreversible damage, and is it reachable by default? Can a caller distinguish "retry" from "stop" without reading English? Can it discover your surface in one call? And can it verify its own work somewhere that doesn't count?
We had to answer those because our writes are permanent by law. Most APIs get to answer them because a customer will otherwise wake up to a mess. The list is the same either way.
If you want to see how any of it is wired up, the docs are at docs.beel.es, sandbox included. And we'd like to hear how other teams are handling the retry-loop problem in particular — it's the one we're least confident we've solved.
Top comments (0)