How to Build for AI Agents
Traditionally APIs were designed for a browser or a client library. Something on the other end was going to parse the response and turn it into a screen or an object. However, the thing calling your API now is a model in a loop, with a token budget and a terminal, and it does not render your JSON anymore. Instead, it reads it.
I have argued before that you should build for who is not human. That was the why. This is the how, and it lands almost entirely on your API, because the API is the only surface an agent ever touches.
Most of what follows I learned the slow way, building crmkit, a deliberately experimental CRM designed for agents to drive. It is headless, there is no UI at all, and the agent is the only user it has. That turns out to be a good teacher, because there is nowhere to hide a bad decision behind a screen. We run it internally on a few projects. It is not perfect, but I think it is pointing the right way.
Start with the format. Text is the default interface. Not JSON. JSON is a serialisation format for programs that are going to parse it, and an agent is not parsing anything. It is reading. Every brace, quote, repeated key name is a token you charge the agent for on every single response, and none of it carries meaning to the reader. Return text first, and let structured output be the fallback through the accept header. There is nothing wrong with adding other ways in either, like ?format=json, when a real program is on the other end. Just stop making that the default, because it is not going to be the common case in the future.
Then make it grepable. Surface as much useful information in a single line as you can. The agent's first instinct with your output is to pipe it into grep, because that is the environment it lives in. A record spread across fifteen lines of pretty printed JSON cannot be sliced. The same record on one line can be filtered, counted, cut and sorted, without a round trip back to you or another pass through the model. In crmkit a contact comes back like this:
One labeled line per record, and anything that is not a record rides on a comment line the agent can read or ignore. Pagination is the same trick, a # next: <cursor> at the end of the page. One line per thing is not an aesthetic preference. Not at all. It is what makes your output composable with everything else the agent already knows how to use.
Identifiers must be short. Short enough to describe in five tokens, but not so short that you introduce collisions or confusion. A UUID is thirty six characters of noise that the agent carries through every subsequent call, and you pay for it every time. Prefixes help a lot here, like cus_abc123 or token_xxx. The prefix does double duty. It tells the agent what kind of thing it is holding even when the identifier shows up with no context around it, and it stops it from handing you a customer id where a token belongs. Stripe worked this out for humans long before agents existed. It turns out the reasoning holds even better for a reader that pays by the token. One thing I would add from practice, take the identifier back in both forms. crmkit accepts GET /contacts/k7m2q and GET /contacts/contact_k7m2q and treats them as the same thing, because an agent will guess, and being right about the record while wrong about the punctuation is not a mistake worth a round trip.
Surface what is likely to be needed next. If the agent asks for a list of resources, do not just return the list. Return the information around those resources that helps it understand what it is looking at. Without this a single question turns into a fan out of ten follow up calls, each one a round trip, each one more tokens and more latency, and every one of them was predictable. You know what the next question is going to be. Answer it now, in the same response. Every contact line in crmkit carries activities=N and last_activity for exactly this reason. The agent can tell a live record from a dormant one by reading the list it already has, instead of fetching each row to find out.
Add bulk operations. Inserting ten records in one request is a lot easier than performing ten separate requests. For a human client that is a nice optimisation. For an agent it is the difference between one decision and ten, because the model has to think between every call, and thinking is the expensive. This is the same instinct I wrote about in compress, don't expand. Give the agent one call that does the whole job instead of ten that each do a tenth of it.
The half of this I underrated is bulk on the way out. Reads fan out just as badly as writes, and nobody thinks to fix it because on a screen you would have paginated the problem away. crmkit takes a list of handles on the activity endpoint, so an agent holding a page of companies pulls the history for all of them in one call and groups the result locally. It is the N+1 problem, except every extra query also costs a thinking step.
Error messages should be descriptive. An error is not a status code. When a human hits a 400 they go and search for it, read a forum thread, find the manual. An agent cannot do any of that. It has exactly what you handed it and nothing else. So point it at the actual docs and manuals it can go and read, and return hints about what to do differently. Every error in crmkit is two lines, an ERROR code and a HINT that names the next move. Ask for a record that is not there and it says to list them first and gives you the endpoint. Filter on a field that does not exist and it prints the fields that do, so the agent is never guessing at a vocabulary you could have just handed it.
The part I only learned by watching it fail is that the hint has to say whether to try again at all. An agent's default response to a wall is another attempt, and some walls do not move. When crmkit rejects a write because the plan is full it says so in words and tells the agent to stop and talk to a human, because no amount of retrying is going to buy more room. A retryable error and a terminal one that look alike will burn a budget in a loop.
Ship the manual inside the API. Not a docs site, not a portal with a search box, an endpoint. crmkit answers GET /help with its whole operating manual on one page, written for the agent rather than for a developer evaluating us, with a copy at /.well-known/agent.md for anything that comes looking. It opens with a few lines of YAML saying what the service is, what it can do and how auth works, so a machine can decide whether to keep reading before it spends tokens on the prose.
The consequence took me a while to appreciate. The way into crmkit is a single URL. You paste one line into an agent, read https://api.crmkit.ai/start and follow it, and it signs the user in, puts a real contact in and sets a reminder, with nobody touching a UI. That page is not documentation. It is written in the imperative, as instructions to carry out rather than reference to explain. No SDK to publish, no client library to keep in sync, no onboarding funnel to maintain. Distribution collapses into a link, and the manual becomes the product surface.
Assume every write happens twice. An agent retries. It times out and tries again, loses the thread, starts over, or just decides it is not sure the first one landed. So make the writes safe to repeat. Posting a contact to crmkit upserts on the email address, so sending the same person twice updates them instead of growing a second copy, and the response says plainly whether it created or updated. Attaching something that is already attached is a free no-op rather than an error. Idempotency is not an advanced feature when your caller is a probabilistic process with a timeout.
Assume it is not the only one. Several agents work the same records at the same time, and so do the people around them. Every record in crmkit carries a version. Send it back with your update and a stale write is rejected instead of quietly flattening what another agent just did. Leave it out and last write wins, but at least you chose that. It also stamps every row with who wrote it, agent or human, and keeps the field level diff, because "who changed this, and to what" stops being a rhetorical question the moment a fleet is working your data.
Gate what cannot be undone. A delete in crmkit does not delete. The first call comes back asking for confirmation and hands over a token, and the agent has to go and ask a human before repeating the call with that token attached. Promoting someone to admin or dropping a workspace emails a code that has to come back. The check lives in the API, not in the agent's judgement and not in a stern sentence in a prompt. This is the honest version of what I meant by human in the loop, just not like this. You do not ask the model to be careful. You make the irreversible call impossible to complete in one step, so the pause happens whether the agent thought to pause or not.
The thread running through all of it is the same. Your reader has no second reader behind it. There is no developer who will look at the payload, no client library that will smooth over the rough parts, no browser doing the rendering. Whatever you put on the wire is the whole experience, and it gets paid for in tokens, on every call, forever. That reframes API design as a budget exercise more than an aesthetic one.
None of it is exotic. Most of it is the stuff you would have done anyway if the caller had been a junior engineer with a shell, no patience, and a bill running the whole time.
Your API has a new reader. It works in a terminal and it pays by the token. So write for it.