How to expose your agent as an API your customers can call
Designing an AI agent API customers call from their own code: request and response shape, sync or async run handles, streaming, idempotency, keys, versioning.
Expose the agent behind a small contract that you version: the request carries the customer's input and identity and nothing model-specific, the response carries a result, a run id, the cost and a schema that does not move, and any run longer than a few seconds returns a handle to poll or stream instead of a held-open connection. Each customer gets their own key with scopes, a rate limit and a budget, and the agent's behaviour is versioned separately from the endpoint so a prompt change never breaks a customer's code.
The moment a customer asks for the API
The request usually arrives after the chat widget works. A customer's platform team wants to call the triage assistant from their own ticketing pipeline, a partner wants your summaries inside their product, and someone has already pointed a script at the endpoint your widget uses. That endpoint was built for a browser you control, and the ways it fails as a public contract are specific.
Your widget's request body carried model and systemPrompt because the front end passed them through, so three customers now send those fields and you cannot change models without a migration. A run that takes forty seconds exceeds the customer's HTTP client timeout at thirty, the client retries, and an agent that files a ticket files two. One customer's script runs at twenty requests per second against your single provider key, and every other customer sees the resulting 429s.
Then a prompt change moves the result from a string to an object, a customer's parser breaks in production, and the ticket they open says "it failed" with no identifier you can search for. Each of these is a contract problem, and each is cheaper to design out than to patch after a customer depends on the accident.
How do I let customers call my AI agent from their own code
The steps below are in dependency order. The request shape decides what you can change later, the response shape decides what customers parse, the run model decides what happens on a slow run, and keys, versioning and support sit on top of those three. The wider checklist for shipping an agent to production is collected under AI agent deployment.
1. Put input and identity in the request, and nothing model-specific
The tenant comes from the key, never from the body: a body field a customer can set is a field a customer can set to another customer's value. The end user is a claim the customer makes under their own tenant, and the input is a JSON object you validate against a published schema before spending anything, so a malformed request is a 400 rather than a wasted run.
POST /v1/agents/support-triage/runs
Authorization: Bearer key_live_acme_...
Idempotency-Key: 2c9d6b1e-4f1a-4c6e-9a7b-1d2e3f4a5b6c
Content-Type: application/json
{
"input": {
"ticket": { "subject": "Invoice shows double charge", "body": "..." },
"locale": "en-GB"
},
"endUser": { "id": "u_8812" },
"conversationId": "conv_01J9X...",
"metadata": { "customerRef": "ticket-48213" },
"mode": "async"
}
Leave out model, temperature, systemPrompt and the tool list. Any of those in a public request becomes a dependency the first time a customer sets it, and it moves control over cost and quality from you to whoever wrote the integration. A customer who wants different behaviour gets a named configuration on their tenant, set through your dashboard or a separate admin call, and the run request stays the same shape for everyone. How that per-tenant configuration stays isolated is covered in the tenant isolation checklist.
2. Return a result, a run id, the cost and a schema that does not move
The response is a run record, whether it arrives synchronously or from a status poll. Customers parse output and nothing else, so output has a schema you publish and validate twice: once at the model layer through structured output, and again at the edge before the response leaves.
{
"runId": "run_01J9XK3M...",
"status": "completed",
"agentVersion": "support-triage@2026-08-14",
"output": {
"category": "billing",
"priority": "p2",
"summary": "Customer was charged twice for the August invoice."
},
"usage": { "inputTokens": 2310, "outputTokens": 188, "costUsd": 0.0071 },
"createdAt": "2026-09-03T09:14:02Z",
"completedAt": "2026-09-03T09:14:19Z"
}
When the model returns something that fails the output schema, the run finishes as failed with the code output_schema_violation rather than as a success carrying a malformed object. A customer can retry a failure; a customer cannot defend against a success that is wrong in shape. Cost goes in the response so the customer can meter their own usage without asking you, and agentVersion goes in so a support conversation can start from what actually answered.
3. Offer sync, async with a run handle, and streaming from one endpoint
The mode field selects the run model, and the default is async, because an agent with tools routinely runs past the timeout a customer's HTTP client or load balancer applies. A caller that times out at thirty seconds retries, and your first run is still executing with its own side effects.
| Mode | Use it when | Response |
|---|---|---|
sync | Single-step work that finishes in a few seconds | 200 with the full run record; past a fixed bound (say 20 s) return 202 with the handle |
async | Anything with tools, approvals or an unknown length | 202 with runId, status: queued and a statusUrl to poll, or a webhook on completion |
streaming | A chat-like UI on the customer's side | Server-sent events with a sequence id per event, terminal event carries the run record |
The async acknowledgement is small on purpose:
{
"runId": "run_01J9XK3M...",
"status": "queued",
"statusUrl": "/v1/runs/run_01J9XK3M..."
}
Polling returns the same run record shape as a sync response, with status moving through queued, running, awaiting_input, and then completed, failed or cancelled. Treat awaiting_input as actionable rather than transient: a run parked on a human approval or a question for the end user will not advance because the customer keeps polling, and the record should say what it is waiting for. For streaming, number every event and accept Last-Event-ID on reconnect, because the connection will drop mid-run and the alternative is a client that saw half an answer and starts over.
4. Make retries safe with an idempotency key
The Idempotency-Key header in the request above is a UUID the customer generates once per logical run. Store the mapping from key to run id for 24 hours, scoped to the customer's API key so two customers can use the same value without collision. A repeated request with the same key returns the original run (a 202 while it runs, a 200 once it finishes) instead of starting a second one, and a repeated key with a different body returns 422 with idempotency_key_reused.
An agent makes this matter more than it does for an ordinary endpoint. A duplicate run is a duplicate set of tool calls, and the model on the second run has no way to know a ticket was already filed by the first. Pass runId through to your tools as a correlation id as well, so a tool that creates something external can dedupe on its own side when the run itself is retried after a crash.
5. Issue per-customer keys with scopes, a rate limit and a budget
One key per customer per environment, with a prefix that says which (key_test_ and key_live_), so a test key pasted into production fails loudly. Scopes list what the key can call, and a key that can only run one agent and read its own runs is the default rather than the exception.
{
"keyId": "key_01J9XQ...",
"customerId": "cus_acme",
"environment": "live",
"scopes": ["agents:support-triage:run", "runs:read"],
"rateLimit": { "requestsPerMinute": 120, "concurrentRuns": 5 },
"budget": { "usdPerMonth": 500, "onExhausted": "reject" }
}
The two limits catch different failures. A request limit and a concurrency cap stop scripts and retry storms within seconds, and they run at admission before anything is queued. A spend budget catches slow, expensive usage that a request limit passes, and it runs against a meter settled per run, which is why the response in step 2 carries cost.
At the rate limit return 429 with Retry-After; at the budget return 429 with budget_exhausted and the reset time, since every client already retries a 429 correctly. Sizing the budget, and what to do at the cap, is worked through in AI usage limits per customer.
Allow two live keys per customer at once so they can rotate without downtime, and never let anything in the request body override the tenant the key resolves to. That last rule is the one an internal endpoint most often breaks, because the widget passed a tenant id and nobody removed it.
6. Version behaviour separately from the endpoint
Two version numbers, changed at different rates. The API version describes the request and response shape, lives in the path or a header, and changes when a field is renamed or removed, which should be rare. The behaviour version describes the prompt, the model, the tool set and the output schema, and changes whenever you improve the agent.
A behaviour change is a draft until it passes the regression cases you keep for that agent, then it is published to a channel. Customers pin to stable by default or to an explicit version such as support-triage@2026-08-14, and every response reports the version that answered. Keep the previous published version callable for a window after promotion, so a customer who sees a regression can pin back while you look. The testing side of this is covered in prompt change regression testing.
Watch for behaviour changes that are API changes in disguise. A prompt edit that changes what output.category can contain breaks a customer's switch statement on that enum. Treat the output schema as part of the API version, and add values behind a new API version or as an explicitly documented open enum.
The same versioned agent definition should sit behind every way a customer reaches it. A customer whose own agent is the caller wants the same capability as an MCP server, and the customers who never write code want it in chat or Slack; keeping one definition behind all of them is the subject of one agent across many channels.
7. Give support a run id and a trace behind it
Every error response includes runId, including a 500, and every error carries one code from a short fixed list: invalid_input, rate_limited, budget_exhausted, output_schema_violation, tool_failed, timeout, cancelled. Customers can read GET /v1/runs/{runId} for status, error code, timing and cost. Internally the same id keys the full trace: each step's input and output, every tool call with its arguments and result, the model, the token counts and the latency, and the behaviour version.
A ticket that says "it failed" with a run id is a search; the same ticket without one is a reconstruction from timestamps and guesswork. Put the id in the response before you put it in the documentation, and put it in the documentation before the first customer goes live.
Where this gets easier
Runtype exposes any agent or flow as an API surface with keys scoped to selected capabilities and a rate limit per key, streams responses as server-sent events with a sequence id on every frame, and turns a request sent with Prefer: respond-async into a durable execution handle you poll for status, with a per-run cost ceiling available through loopConfig.maxCost. Agent behaviour lives in draft and published versions independent of the endpoint, so a prompt change is a publish rather than a deploy, and the same definition sits behind the REST surface, an MCP server, web chat and Slack. Key scoping is documented at scoping API keys to capabilities.
Frequently asked questions
- Should an AI agent API be synchronous or asynchronous?
- Asynchronous by default, with a synchronous option for short, single-step work. An agent with tools routinely runs longer than the 30 to 60 second timeout most HTTP clients and load balancers apply, and a caller that times out retries, which starts a second run with its own side effects. Return a run id from a 202 and let the caller poll or subscribe to a stream.
- How do I version an AI agent API without breaking customer integrations?
- Keep two version numbers. The API version describes the request and response shape and changes rarely; the behaviour version describes the prompt, model and tool set and can change every week. Customers pin to a behaviour channel such as stable, every response reports which behaviour version answered, and a prompt change ships as a new behaviour version after it passes your regression cases.
- What should the API return when a customer's run fails?
- A run id in every error, including a 500, plus a short machine-readable code from a fixed list such as invalid_input, rate_limited, budget_exhausted, output_schema_violation, tool_failed or timeout. Keep the full trace (steps, tool calls, model, tokens, timing) internally, keyed by that run id, so support can answer a ticket from the id alone.