Agents API
Define agents and run them for your own users through /v1.The Agents API runs a Dexto agent for your own customers, from your backend. You define the agent once: its instructions, model and tools. Then you start a session for each of your users and send their messages. Dexto runs the agent loop, streams the reply, and pauses for your approval when a tool needs it.
An agent you create here is separate from your own Dexto agents. Its sessions never appear in your
chats, it can’t see your files, memory or connections, and its runs get only the tools you list.
Call the API from a server with your API key and https://api.dexto.ai; never put the key in a
browser.
Use a developer API key, or the key Dexto gives a deployed Function or Site Worker as
DEXTO_API_KEY. A deployed key acts as its deployment’s Cloud project, not as the person who
deployed it, so a deployed backend can create and run agents without a key pasted into its settings.
A CLI sign-in key gets 403.
Create an agent
Section titled “Create an agent”/v1/agents curl -X POST https://api.dexto.ai/v1/agents \ -H "Authorization: Bearer $DEXTO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme support", "instructions": "You help Acme customers with orders. Be brief and friendly.", "tools": [{ "type": "web_search" }] }'The response is the agent, with an id and version: 1. Leave model out to use Dexto’s default
model, or set it to an id from GET /v1/models that supports tool calls.
GET /v1/agents lists your agents, newest first, and GET /v1/agents/{agent_id} reads one.
Give it tools
Section titled “Give it tools”An agent’s tools are the only tools its runs get. There are eight kinds:
| Tool | What it does |
|---|---|
web_search | Searches the web. |
web_fetch | Reads the text of up to 10 web pages by URL. |
files | Finds, reads, searches, writes, edits and shares your project’s files. |
function | Calls named operations of a Function you own. |
sandbox | Starts a sandbox, works with its files and runs commands in it. |
media | Generates images, audio or video. |
automations | Schedules follow-ups for itself, such as “check again tomorrow at 9”. |
mcp | Calls tools of a remote MCP server, with your key attached by Dexto. |
Each search, fetch, Function call, MCP tool call and media generation is charged to the agent’s project like the
Search and fetch API, Function invocations and the Media API, and
files it writes are stored like any file. When the agent’s credits or spend limit
refuse a call, the tool returns an error with insufficient_credits or spend_limit_reached
instead of running, and the run goes on.
A files tool works with your API key’s Cloud project’s files. prefix limits it to
paths that start with it (end it with /; leave it out for every file), and read_only gives it
the reading tools only:
{ "type": "files", "prefix": "knowledge/", "read_only": true }The agent sees the prefix as its root, /: with the prefix knowledge/, the file
knowledge/refunds.md is /refunds.md to it. Every path the tools return starts with /, and a
path starting with / always means that path from the root, so a returned path always names the
same file again. Without the leading /, a path that starts with knowledge/ means the same file
too, as your instructions may name it. The agent can’t name a path above /. A files tool gives the model these tools, with the names
and arguments of the file tools Dexto agents use on a computer:
| Model tool | Arguments | What it does |
|---|---|---|
read_file | file_path, offset, limit | Reads a text file, up to 2,000 lines or 256 KiB per call, from line offset. A binary file, or text over 1 MiB, comes back as a 15-minute URL. |
read_media_file | file_path | Shows the model an image or PDF, up to 16 MiB; a larger one comes back as a URL. |
glob_files | pattern, after | Finds files by a glob such as **/*.md, 200 per page. |
search_files | pattern, include, fixedString, after | Searches text files line by line with a regular expression (ripgrep syntax) or a fixed string; up to 200 files or 16 MiB per call. |
write_file | file_path, content | Writes UTF-8 text up to 1 MiB, replacing any file at that path. |
edit_file | file_path, old_string, new_string, replace_all | Replaces text that must match exactly once, unless replace_all; nothing is written if the file changed since it was read. |
delete_file | file_path | Deletes one file. |
share_file | file_path, expires_in | Creates a download link for a person, for 1 second to 24 hours (one hour by default). |
A read-only files tool has only read_file, read_media_file, glob_files and search_files.
require_approval asks before each write, edit, delete and share; reads never ask. A refused call
returns an error that says what to do next, and the run goes on. Files the agent writes record it as
their author, and their storage is charged to the project. Reading and searching are free.
A Function tool names the Function’s project_id, a ready deployment_id and the operations
the agent may call:
{ "type": "function", "name": "orders", "project_id": "7f1b5a4e-…", "deployment_id": "0c9d2e11-…", "operations": ["lookup_order", "refund_order"], "require_approval": true}Each operation becomes a tool the model sees as <name>_<operation>, for example
orders_refund_order. Its input schema comes from your Function’s OpenAPI document when you save
the agent. Calls run with your authority against that exact deployment. To change the deployment,
save the agent again with the new deployment_id.
A sandbox tool works in your API key’s Cloud project, in one of two ways. It can name a sandbox of the project that every run shares:
{ "type": "sandbox", "sandbox": { "type": "project", "sandbox_id": "5d0c7a90-…" }}Or it can give each session a sandbox of its own, of size large, the only size for now:
{ "type": "sandbox", "sandbox": { "type": "session", "size": "large" } }A session sandbox is created the first time the session uses the tool. It is deleted, with its
files, when the session ends: when you archive the agent, or after the session has had no run for
24 hours. The next run then starts with a fresh one. Session sandboxes appear in
GET /v1/sandboxes as agent-session-<session id>. The agent starts and pauses the sandbox itself,
and a file or command call starts a stopped sandbox first. Its running time is paid from credits
like any sandbox, and an idle sandbox is paused, so idle time is not charged.
When the agent also has a files tool, its sandbox tool can copy files: copy_from_files copies
files (by path, or every file in a folder) into a /workspace directory, and copy_to_files
saves a sandbox file as a project file. Both name files as the files tools do, relative to the
files tool’s prefix, and a read-only files tool can’t save. Copies are free.
A sandbox tool can also use your project’s secrets without seeing them. Name them in
secrets:
{ "type": "sandbox", "sandbox": { "type": "session", "size": "large" }, "secrets": ["STRIPE_KEY"]}Each command then has $STRIPE_KEY in its environment, holding a placeholder. Requests the command
makes over HTTPS to the secret’s hosts go through Dexto, which puts the real value on them as the
secret’s placement says, and removes the value from the response. Requests to any other host get
nothing, so a command can’t send the key elsewhere. Use the variable as you would the real key:
curl -H "Authorization: Bearer $STRIPE_KEY" https://api.stripe.com/v1/charges. A sandbox tool
uses at most one secret per host. The access lasts only while the command runs, and commands
with secrets in one sandbox take turns. In a sandbox that several agents share, a command of
another agent running at that moment could reach the same hosts with the key attached; give an
agent with secrets its own sandbox, or a session sandbox, to avoid that.
A media tool lets the agent generate the kinds of media you name in kinds: image, audio or
video. Leave kinds out to allow all three.
{ "type": "media", "kinds": ["image"], "require_approval": true }The agent lists the models of those kinds, gets a price estimate for its input, then generates from
the estimate, with the same models and estimates as the Media API.
A model of another kind is refused. A generation starts only when credits and spend limits cover
its estimate, and is charged its actual cost. An image is usually ready within the call. A longer
generation, such as a video, comes back with its status and an id the agent checks later. A result
has the shape of a Media API generation: each output has a url, a download link the agent can
give your user, valid for up to an hour. Outputs are deleted 24 hours after they are made. When the
agent also has a files tool, generate takes save_to (a file path, or a prefix ending in /,
inside the files tool’s prefix) to keep the outputs as project files, and each saved output names
the exact path it was saved at in file_path; a read-only files tool can’t save. The agent’s
generations belong to its project: your project’s keys list and read them, with fresh links,
through GET /v1/media/generations, while keys of other projects can’t. With require_approval,
the run waits for your OK before each generation; listing models and estimating don’t.
An automations tool lets the agent run itself again later: once at a time, or on a cron schedule no more often than every 5 minutes, with a message it writes now for its future self.
{ "type": "automations", "max_active": 5 }The model gets one automations tool with the actions create, list, pause, resume and
delete. A follow-up continues the current session, or starts a new session for the same end user
and metadata each time. Each is an automation of the agent’s project that runs
only this agent, created by the agent: you list and manage them through /v1/automations, and the
agent reaches only the ones it created, never yours. It keeps at most max_active active at once
(10 when you leave it out, at most 100); past that, create tells the model to pause or delete one
first. Each firing is a run of the agent, paid like any other. With require_approval, the run
waits for your OK before each create and resume.
An MCP tool calls tools of a remote MCP server (Streamable HTTP, over HTTPS on a public host) from Dexto’s servers:
{ "type": "mcp", "name": "linear", "server_url": "https://mcp.linear.app/mcp", "auth": { "type": "secret", "secret": "LINEAR_KEY" }, "tools": ["list_issues", "create_issue"], "require_approval": true}auth is null for a server that needs no credential, or names one of your project’s
secrets whose hosts include the server’s host. When you save the agent, Dexto
connects to the server with the secret, lists its tools, refuses a name the server doesn’t publish
and keeps each listed tool’s input schema with the version. Each becomes a model tool
<name>_<tool>, for example linear_create_issue; to pick up a changed schema, save the agent
again. Calls carry the secret only to the server’s own address, follow no redirects, time out after
30 seconds and return at most 256 KiB; the secret’s value is removed from results. Each call the
server answers is charged a flat price, like a Function call. A server that can’t be reached, a
refused key or a deleted secret comes back as a tool error, and the run goes on.
Set require_approval: true on any tool that should wait for your OK before it runs.
Update an agent
Section titled “Update an agent”/v1/agents/{agent_id} PUT replaces the whole definition and creates the next version. Sessions that already exist keep
the version they started with. Send expected_version to reject the update if someone else
changed the agent first; the API then returns 409 with the current version.
DELETE /v1/agents/{agent_id} archives the agent. It accepts no new sessions or messages, and its
history stays readable.
Start a session for one of your users
Section titled “Start a session for one of your users”/v1/agents/{agent_id}/sessions curl -X POST "https://api.dexto.ai/v1/agents/$AGENT_ID/sessions" \ -H "Authorization: Bearer $DEXTO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user": "customer_42" }'user is your own identifier for the person. Dexto stores it as a label, so you can list a user’s
sessions with GET /v1/agents/{agent_id}/sessions?user=customer_42; it doesn’t sign anyone in. A
session uses the agent’s current version, or the one you pass as version.
Lists return a page at a time, newest first. When has_more is true, pass the page’s
next_after as after to read the next one; it is null on the last page.
Tell your Functions who they’re serving
Section titled “Tell your Functions who they’re serving”Pass metadata when you start a session, for example your tenant id:
{ "user": "customer_42", "metadata": { "tenant_id": "acme", "plan": "pro" } }Each time the agent calls one of your Function tools, Dexto sends the session’s user and
metadata, plus the agent, session and run ids, as trusted context. Your Function reads it as
context.caller. The model can’t set or change it, so scope data by context.caller instead of
any customer id the model put in the tool input. Metadata is up to 4 KiB of JSON and fixed for the
session.
Dexto keeps your organizations apart; keeping your own customers apart is up to your backend.
Check that a session belongs to the signed-in user before relaying it, and let your Functions
enforce access with context.caller.
Send a message and stream the reply
Section titled “Send a message and stream the reply”/v1/agents/{agent_id}/sessions/{session_id}/messages curl -X POST "https://api.dexto.ai/v1/agents/$AGENT_ID/sessions/$SESSION_ID/messages" \ -H "Authorization: Bearer $DEXTO_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $REQUEST_ID" \ -d '{ "content": "Where is my order 1042?" }'The API returns 202 with the run. A session runs one message at a time; sending another while a
run is active returns 409. Retry a lost request with the same Idempotency-Key to get the same
run back.
Open the session’s event stream to follow the run:
/v1/agents/{agent_id}/sessions/{session_id}/events curl -N "https://api.dexto.ai/v1/agents/$AGENT_ID/sessions/$SESSION_ID/events" \ -H "Authorization: Bearer $DEXTO_API_KEY"Each server-sent event is named by its type:
| Event | Meaning |
|---|---|
run.started | The agent started working on a message, with its trigger. |
message.delta | More of the reply’s text. |
message.completed | The full text of one reply. |
tool.started | The agent called a tool, with its arguments. |
tool.completed | The tool finished, with its status, error, output and cost. |
approval.requested | A tool is waiting for your approval. |
approval.resolved | The approval was decided. |
run.completed | The run finished, with its stop_reason and usage. |
run.failed | The run stopped, with an error code and message, and its usage. |
run.cancelled | The run was cancelled, with its usage. |
Every event has an id. If the connection drops, reconnect with the last id in the Last-Event-ID
header to continue where you left off. GET …/messages returns the conversation so far.
Tool calls
Section titled “Tool calls”tool.started carries the tool’s input as arguments, and tool.completed reports how the call
went:
{ "type": "tool.completed", "run_id": "3f0c…", "tool_call_id": "call_8d2…", "name": "orders_lookup_order", "status": "success", "error": null, "output": { "order_id": "1042", "status": "shipped" }, "output_truncated": false, "usage": { "credits": 0.00031 }}status is success, error, rejected (you rejected its approval) or cancelled (the run was
cancelled during the call). When the call did not succeed, error has the tool’s code (such as
not_found, insufficient_credits or the generic tool_error), a message and whether a retry
may succeed (retryable). A result whose error is null counts as a success.
output is the tool’s result and arguments its input. Both have recognised secrets, such as API
keys, replaced with [REDACTED]. When either is larger than 8 KiB of JSON, it becomes a string
holding the start of that JSON, and output_truncated or arguments_truncated is true. Both are
also null and marked truncated when a replayed event lost them to the stream’s replay limits.
usage.credits is what the call was charged by the time the event was sent: its searches,
fetches, Function calls and media generations. It is null when nothing was charged by then, for
example a free call, or a media generation that settles later. Sandbox compute is charged to the
sandbox and file storage to the project, never to a call.
How runs end
Section titled “How runs end”The run’s last event, and the run itself, carry stop_reason beside status:
stop_reason | Status | Why the run stopped |
|---|---|---|
completed | completed | The agent finished its reply. |
cancelled | cancelled | You cancelled the run. |
spend_limit_reached | failed | A spend limit on the agent, project or key was reached. |
insufficient_credits | failed | The organization ran out of credits. |
max_steps | failed | Reserved for a step limit; runs have none yet. |
error | failed | Any other failure. |
Each also carries usage: the run’s model input_tokens and output_tokens and the credits
charged so far for its model calls, searches, fetches, Function calls and media. Charges settle as
work finishes, so a later read can be a little higher than the event. Read a run at any time:
/v1/agents/{agent_id}/sessions/{session_id}/runs/{run_id} The run returned by POST …/messages, POST …/cancel and a session’s active_run has the same
fields; stop_reason is null until the run stops.
A run that an automation started, and its run.started event, carry
trigger: { "type": "automation", "automation_id": "…", "firing_id": "…" }. Every other run has
trigger: null, as does a run whose automation was deleted. active_run leaves it out.
Read what a run did
Section titled “Read what a run did”The event stream is live. To read a finished run, or one still going, page through its steps:
/v1/agents/{agent_id}/sessions/{session_id}/runs/{run_id}/steps curl "https://api.dexto.ai/v1/agents/$AGENT_ID/sessions/$SESSION_ID/runs/$RUN_ID/steps?limit=50" \ -H "Authorization: Bearer $DEXTO_API_KEY"Steps come oldest first, with has_more and next_after. Pass next_after as after for the next
page; limit is 1 to 100 and defaults to 50. A step is one of two kinds:
- A
model_turnis one model response: itsmodel, its visibletext, thetool_call_idsit asked for and itsusagein tokens. A response that only called tools has emptytext. Itsstatusisrunning,completed,failedorcancelled. Reasoning, prompts and the provider’s own error text are never included. - A
tool_callfollows the model turn that made it. It has thename,arguments,output,errorandusagethattool.startedandtool.completedcarry, with the same redaction and the same 8 KiB limit, and the samestatusvalues plusrunningandwaiting_for_approvalwhile the call is unfinished.
{ "object": "run_step", "id": "call_8d2…", "run_id": "3f0c…", "type": "tool_call", "name": "orders_refund_order", "status": "success", "arguments": { "order_id": "1042" }, "arguments_truncated": false, "output": { "refunded": true }, "output_truncated": false, "error": null, "approval": { "id": "7c1e…", "status": "approved", "requested_at": "2026-10-15T09:00:00.000Z", "decided_at": "2026-10-15T09:00:42.000Z", "expires_at": null }, "usage": null, "started_at": "2026-10-15T09:00:00.000Z", "completed_at": "2026-10-15T09:00:43.000Z"}Approvals are not steps of their own. The call that waited for one holds it as approval, with
its status (pending, approved, rejected or expired) and when it was requested and
decided. null means the call needed none.
Steps stay readable as long as the session exists. A run can have many steps: to follow every
page in code, iterate dexto.agents.sessions.runs.steps(agentId, sessionId, runId).
List a session’s runs, newest first, with the same paging. Each is the run GET …/runs/{run_id}
returns:
/v1/agents/{agent_id}/sessions/{session_id}/runs Approve tool calls
Section titled “Approve tool calls”When a tool with require_approval is called, the run pauses and the stream sends
approval.requested with the approval_id, tool_name and input. Decide it:
/v1/agents/{agent_id}/sessions/{session_id}/approvals/{approval_id} curl -X POST "https://api.dexto.ai/v1/agents/$AGENT_ID/sessions/$SESSION_ID/approvals/$APPROVAL_ID" \ -H "Authorization: Bearer $DEXTO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "decision": "approve" }'"reject" stops the tool, and the agent is told it was rejected. POST …/cancel stops the
session’s active run.
Delete your users’ data
Section titled “Delete your users’ data”Sessions are kept until you delete them. To honour a deletion request from one of your users, delete one session at once, or everything of a user or an agent in the background.
/v1/agents/{agent_id}/sessions/{session_id} Deleting a session cancels its active run, ends its session sandbox and deletes its messages, runs,
approvals and stored tool output. It returns the session’s id with deleted: true; afterwards the
session is not found.
/v1/data_deletions curl -X POST "https://api.dexto.ai/v1/data_deletions" \ -H "Authorization: Bearer $DEXTO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user": "customer_42" }'- With
user, Dexto deletes every session labelled with that user in your key’s Cloud project, or only one agent’s when you also passagent_id. It also deletes the automations whose sessions carry that label, since they would write more data about the person. - With only
agent_id, the agent stops at once: new sessions and messages get409and its automations pause. Dexto then deletes its sessions, automations, firings and versions, and the agent is not found. Spend views still show its past spend under its name.
The API answers 202 with a data_deletion. Read it with GET /v1/data_deletions/{data_deletion_id}
until status is completed (or failed); deleted counts the sessions and automations it
removed. GET /v1/data_deletions lists them, newest first. The user label is never returned. A
Cloud project can have at most 10 deletions queued or running.
While a user’s deletion is queued or running, creating a session for that user gets 409, so a
completed deletion leaves none behind. If a deletion keeps failing, it ends failed with an
error: what it deleted stays deleted, and an agent it was deleting stays archived. Request the
same deletion again to finish it.
Usage and billing records are never deleted: credits charged for a deleted session still count in the agent’s and project’s spend. They keep the agent, project and key, lose the session and run, and never hold message content or the user label. The organization’s audit log records each deletion with the key that asked for it, its counts and a salted hash of the label. Database backups keep deleted rows until they age out of the backup window.
Session retention
Section titled “Session retention”An Owner or Admin can set a Cloud project’s session retention in the Cloud app: Dexto then deletes
each session that number of days (1 to 3650) after its last message or run, as DELETE would. A
session with a run in progress is never deleted. With a retention set, sessions report expires_at;
without one, expires_at is null and sessions are kept until you delete them. API keys can’t
change retention.
Usage and limits
Section titled “Usage and limits”Runs, and the sandboxes they use, are paid from your organization’s credits. Your plan’s allowance in the Dexto app doesn’t
apply: it never pays for runs, and it doesn’t limit which model an agent uses. When your credits
run out, the run fails with insufficient_credits. An Owner or Admin can set a monthly spend limit
on the agent, its Cloud project or an API key; once one is reached, the run fails with
spend_limit_reached until the next month or a higher limit.
Use the Cloud SDK
Section titled “Use the Cloud SDK”Sites, Workers and Functions you build in Dexto include the Dexto Cloud SDK, which wraps every
operation as agents. Deployed, they call it with their own DEXTO_API_KEY:
import { createCapabilitiesClient } from "@dexto-cloudflare/capabilities-client";
const dexto = createCapabilitiesClient({ apiKey: env.DEXTO_API_KEY });
const agent = await dexto.agents.create({ name: "Acme support", instructions: "You help Acme customers with orders.", tools: [{ type: "web_search" }],});const session = await dexto.agents.sessions.create(agent.id, { user: "customer_42",});await dexto.agents.sessions.sendMessage(agent.id, session.id, { content: "Where is my order 1042?",});
for await (const { event } of dexto.agents.sessions.events(agent.id, session.id)) { if (event.type === "message.delta") process.stdout.write(event.text); if (event.type === "tool.completed" && event.status !== "success") console.warn(event.error); if (event.type === "run.completed" || event.type === "run.failed") { console.log(event.stop_reason, event.usage.credits); break; }}dexto.agents.sessions.getRun(agentId, sessionId, runId) reads a run with its stop_reason and
usage. dexto.agents.sessions.runs.list(agentId, sessionId) lists a session’s runs, and
dexto.agents.sessions.runs.steps(agentId, sessionId, runId) iterates a run’s steps across pages:
for await (const step of dexto.agents.sessions.runs.steps(agent.id, session.id, run.id)) { if (step.type === "tool_call") console.log(step.name, step.status, step.approval?.status);}dexto.agents.sessions.delete(agentId, sessionId) deletes a session, and
dexto.dataDeletions.create, get and list wrap /v1/data_deletions.
See /v1/openapi.json for every request and response
schema, error and rate limit.
Put a chat page in front of it
Section titled “Put a chat page in front of it”Ask Dexto to build an agent app and it makes a Site with a chat page and its own backend. The agent
is defined in the app’s agent.json: its name, instructions, tools and the page’s title, colour
and starter prompts. The backend creates the agent on first use and updates it when the file
changes, holds the key, and gives each visitor’s browser its own sessions, so no key reaches the
page. The page streams replies as Markdown with code and maths, shows each tool call, asks for
approvals, and can stop a run.