Jump toIntroduction
REST API
The API guide
How every call works: keys, requests, errors, lists, safe retries and following runs as they happen. Read it once; the reference then tells you what each endpoint takes and returns.
Introduction
The MyDeepAgents API is a JSON API over HTTPS. Every resource lives at a predictable address under /v1, with standard verbs and one error shape, so once you have made one call you know how to make the rest.
The API describes itself. Its OpenAPI 3.1 document is generated from the code that answers the requests, so the reference in these docs, and any client you generate from it, always match the API that is running.
| Field | Value |
|---|---|
| Base URL | https://api.mydeepagents.com |
| Format | JSON in and out, UTF-8 |
| Authentication | Bearer key in the Authorization header |
| Version | v1, in the path |
| Description | OpenAPI 3.1, generated from the code · https://api.mydeepagents.com/v1/openapi.json |
Authentication
Every call carries a secret key in the Authorization header. A key belongs to a service account: an identity for software rather than a person, so a key keeps working when people join or leave your team.
Authorization: Bearer mda_live_k7m2p9qa3tq8v1zc_…What a key looks like
A key has three parts. Only the last is secret, and MyDeepAgents keeps only a hash of it, so a lost key can't be shown again: revoke it and make a new one.
mda_live_
The environment chosen when the key was made:
liveortest.k7m2p9qa3tq8v1zc_
The key's ID, 16 characters. The first 8 are your organization's route code. This is what the API keys page and its endpoints call
key_id.Zq3vT8xLmV…
The secret, 43 characters, shown once when the key is created.
What a key may do
A key can do what its service account's role allows, narrowed to the scopes you chose for the key. Scopes are permission names such as agent.view or workflow.run, and every endpoint in the reference shows the one it needs.
| Role | May |
|---|---|
viewer | See agents, workflows, widgets, runs, usage and connections. |
operator | Everything a viewer may, plus start, approve and cancel runs and read what runs said. |
builder | Everything an operator may, plus edit, test and publish agents, workflows and widgets, use connections and manage triggers. |
admin | Everything a builder may, plus manage connections, AI providers, members, roles, keys, notifications, budgets and settings. |
A few permissions belong to the organization's owner alone and can't be given to a key's role: encryption keys, storage, delegations and full exports.
Keep keys safe
- Call the API from servers only. A secret key never belongs in a browser or a mobile app; to talk to visitors on a website, use a widget and its publishable key.
- Give each integration its own key with the fewest scopes it needs, so you can revoke one without touching the others.
- Limit a key to your servers' addresses with an IP allowlist. Calls from anywhere else get
403 ip_not_allowed. - Give keys that are only needed for a while an expiry date.
When a key is refused
| Status | Code | What it means |
|---|---|---|
| 401 | unauthenticated | The key is missing, malformed, unknown, revoked or expired, or its service account is turned off. The answer never says which, so a stolen key teaches nothing. |
| 403 | scope_insufficient | The key's scopes don't include the permission the endpoint needs. |
| 403 | permission_denied | The service account's role doesn't allow it. |
| 403 | ip_not_allowed | The call came from an address outside the key's allowlist. |
| 404 | not_found | The resource doesn't exist, or it belongs to an organization this key can't enter. |
Some endpoints are for people only: your own profile and sign-in security, accepting an invitation, and signing up. They refuse keys, so they are not in the reference.
Requests and responses
Send JSON with Content-Type: application/json. A body with a field the API doesn't know is refused with 422, so a typo never passes silently.
| Field | What it means |
|---|---|
| IDs | A prefix and 26 characters, like agt_01jc…. Treat them as opaque strings; the prefix says what the ID is for, which helps when you read logs. |
| Times | RFC 3339 in UTC with milliseconds: 2026-11-10T09:30:00.000Z. |
| Money | Whole micro-dollars in fields ending _micro_usd: 1250000 is $1.25. |
| Field names | snake_case. Resources have id, object (what kind of thing it is), created_at and updated_at. |
| Fixed values | Lowercase strings. New values can appear inside v1, so handle a value you don't know instead of failing. |
| Leaving out or null | In a PATCH, a field you leave out stays as it is; null clears it. |
ID prefixes you will meet
agt_Agentwf_Workflowwgt_Widgetrun_Agent runwfr_Workflow runtrg_Triggerevt_Trigger eventcon_Connectionmcp_MCP serverws_Workspaceusr_Personsa_Service account
Request IDs
Every answer carries an X-Request-Id header, and every error repeats it as request_id. Send your own (up to 64 letters, digits, dots, colons, dashes and underscores) to follow one call through your logs and ours; anything else is replaced with a new one.
Errors
Errors are problem details (RFC 9457), sent as application/problem+json. Branch on code: it is stable and never reworded. title and detail are words for people and can change.
When fields are wrong, errors lists each one with a JSON Pointer into your request, so you can show each message beside the right input.
{
"type": "https://docs…/errors/validation_failed",
"title": "The request is not valid.",
"status": 422,
"code": "validation_failed",
"detail": "1 field is not valid.",
"request_id": "req_01jc8z6y4m3k9q2w5e7r1t0vbn",
"errors": [
{
"path": "/definition/spec/model/id",
"code": "model_unavailable",
"message": "Model 'gpt-9' is not available here.",
"hint": "Pick one of the models in GET /v1/models."
}
]
}Codes you are most likely to meet
| Status | Code | What it means |
|---|---|---|
| 400 | bad_request | The request is malformed: a bad parameter, cursor, order or time. |
| 401 | unauthenticated | No key, or a key that doesn't work. |
| 403 | scope_insufficient | The key's scopes don't cover this. |
| 403 | permission_denied | The role doesn't allow this. |
| 403 | policy_denied | A compliance rule of your organization doesn't allow this. |
| 404 | not_found | Nothing with that ID that this key can see. |
| 409 | already_exists | Something with that name or slug already exists. |
| 409 | alias_in_use | That alias still points somewhere that needs it. |
| 412 | precondition_failed | Someone changed it after you read it (If-Match). |
| 422 | validation_failed | The body is not valid; errors names each field. |
| 422 | idempotency_key_reuse | The same Idempotency-Key came with a different body. |
| 422 | publish_blocked | The definition has problems that stop it being published. |
| 429 | budget_exceeded | A spending budget has been reached. |
| 429 | llm_busy | The model provider is at capacity; try again shortly. |
| 500 | internal_error | Our fault. Retry later and quote the request_id if it persists. |
| 503 | unavailable | A dependency is briefly down; retry with back-off. |
Every error code, with what it means and what to do
Retry safely
- Retry
429and503with exponential back-off, waiting at leastretry_afterseconds when the answer gives it. - Retry network errors the same way. For run starts, send the same
Idempotency-Keyagain, so a retry can never start a second run. - Don't retry other
4xxanswers: the same request will get the same answer.
Pagination
Lists answer a page at a time, newest first. Ask for up to 200 items with limit (50 by default), then pass next_cursor back as cursor until has_more is false.
Filters are plain parameters named after fields. Repeat one to match any of several values, like state=failed twice with different values.
{
"object": "list",
"data": [
{
"id": "agt_01jctdbahw9btk5p831xs974yq",
"object": "agent",
"name": "Support triage",
"…": "…"
}
],
"has_more": true,
"next_cursor": "b2Zmc2V0OjUwfGFndF8wMWpj…"
}| Parameter | What it means |
|---|---|
limit | 1 to 200, 50 by default. A few small lists, such as members and API keys, answer every row when you leave it out. |
cursor | The previous page's next_cursor. It lasts 24 hours and only answers the question it came from. |
order | A field to sort by, with a leading minus for descending, like -created_at. Each list says which fields it sorts by. |
q | Case-insensitive search in the list's names and text. A pasted ID finds that one item. |
include | total_count adds how many match (counted up to 10,000); facets adds counts for each filter. |
created_after · updated_before | Time ranges, inclusive, in RFC 3339. |
Read every page
CURSOR=""
while : ; do
PAGE=$(curl -s "https://api.mydeepagents.com/v1/agents?limit=200&cursor=$CURSOR" \
-H "Authorization: Bearer $MDA_API_KEY")
echo "$PAGE" | jq -r '.data[].name'
[ "$(echo "$PAGE" | jq -r '.has_more')" = "true" ] || break
CURSOR=$(echo "$PAGE" | jq -r '.next_cursor')
doneIdempotency
Starting a run is the one call you must never make twice by accident: a retried request could run an agent, and spend money, a second time. So both run starts require an Idempotency-Key header; without one the answer is 400.
- Use a fresh value for each run, such as a UUID, up to 255 characters.
- Send the same key when you retry. For 24 hours the API answers with the run it already started and adds
Idempotent-Replayed: true. - A key reused with a different body is refused with
422 idempotency_key_reuse. If the first request is still being handled, the retry waits for it and then gets the same answer.
Keys are kept per organization and per caller, so two integrations can never collide.
Concurrency
Agents, workflows, widgets and workspaces come with an ETag holding their row_version. Send it back in If-Match when you change them: if someone saved in between, you get 412 precondition_failed instead of overwriting their work.
If-Match is optional. It is accepted on draft saves, workflow and workspace changes, member changes and AI provider key changes; the reference marks each. Without it, the last write wins.
GET /v1/workflows/wf_01jctgae7hqqg3qxjfrgs9t89k
HTTP/1.1 200
ETag: "7"
PATCH /v1/workflows/wf_01jctgae7hqqg3qxjfrgs9t89k
If-Match: "7"
Content-Type: application/json
{"name": "New order alert (v2)"}
HTTP/1.1 412
Content-Type: application/problem+json
{"code": "precondition_failed", "title": "The resource has changed since you read it.", "status": 412}Start a run
A run is one piece of work by an agent or a workflow. Starting one answers 202 at once with the run queued; the work happens after the answer, so your request never waits on a model.
target | What it means |
|---|---|
draft | The draft being edited: a builder's test, so it also needs agent.test (or workflow.edit for a workflow). This is the default. |
staging | The version published to staging. |
live | The version published to live. |
Start an agent run
curl -X POST "https://api.mydeepagents.com/v1/agents/agt_01jctdbahw9btk5p831xs974yq/runs" \
-H "Authorization: Bearer $MDA_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"input": {
"message": "Summarise today'\''s open tickets."
},
"target": "live"
}'{
"id": "run_01jcr77659p6hws1zjqkthay73",
"object": "run",
"agent_id": "agt_01jctdbahw9btk5p831xs974yq",
"workspace_id": "ws_01jccj30nr8ad7mx1yc5v9zx3h",
"alias": "live",
"state": "queued",
"trigger": "api",
"is_test": false,
"cost_micro_usd": 0,
"steps_used": 0,
"created_at": "2026-11-10T09:30:00.000Z",
"row_version": 1
}Follow an agent run by streaming its events, below, or by reading GET /v1/runs/{id} until its state is final.
Workflow runs
A workflow run starts the same way, at POST /v1/workflows/{id}/runs with an Idempotency-Key; its input is checked against the workflow's input schema. Follow it by reading GET /v1/workflow_runs/{id}, or let a webhook endpoint tell you with workflow_run.state_changed. Stop it with POST /v1/workflow_runs/{id}/cancel. Workflow runs don't stream yet.
Stream a run
GET /v1/runs/{id}/events sends the run as it happens, as server-sent events. Each event has a type and a JSON payload, and the answer's text arrives piece by piece as the model writes it.
curl -N "https://api.mydeepagents.com/v1/runs/run_01jcr77659p6hws1zjqkthay73/events" \
-H "Authorization: Bearer $MDA_API_KEY" \
-H "Accept: text/event-stream"What a stream looks like
id: 1762767000123-0
event: run.started
data: {"run_id":"run_01jcr77659p6hws1zjqkthay73","agent_version_id":"agv_01jc7jt62eapjyt621bszf6wrs"}
id: 1762767000456-0
event: tool.started
data: {"run_id":"run_01jcr77659p6hws1zjqkthay73","call_id":"call_1","tool":"crm.search_contacts","status":"requested"}
id: 1762767001789-0
event: tool.finished
data: {"run_id":"run_01jcr77659p6hws1zjqkthay73","call_id":"call_1","tool":"crm.search_contacts","status":"ok","ms":412}
id: 1762767002010-0
event: message.delta
data: {"run_id":"run_01jcr77659p6hws1zjqkthay73","message_id":"msg_01jc7qh04g97wt3q32g0wny446","index":0,"text":"Three tickets"}
: keep-alive
id: 1762767003900-0
event: run.completed
data: {"run_id":"run_01jcr77659p6hws1zjqkthay73","status":"completed","usage":{"steps":3,"cost_micro_usd":4210,"input_tokens":1820,"output_tokens":240},"error":null}Events
| Event | When | Data |
|---|---|---|
run.started | The run began. | agent_version_id |
message.delta | A piece of the answer's text. index counts the pieces of one message from 0. | message_id, index, text |
message.completed | A whole message, as stored. | message_id, role, content |
tool.started | The agent asked for a tool. | call_id, tool, status, args |
tool.finished | The tool answered, failed or was refused (ok, error, rejected, expired, cancelled). | call_id, tool, status, ms, preview |
approval.requested | A tool call is waiting for a person to approve it. | approval_id, call_id, tool |
approval.resolved | Someone decided. | approval_id, decision |
run.completed · run.failed · run.cancelled | The run ended. The last event; then the stream closes. | status, usage, error |
How to read a stream
- Reconnect with the last
idyou received inLast-Event-ID; the stream carries on right after it. - A comment line arrives every 15 seconds so proxies keep the connection open. Ignore lines that start with a colon.
- The live copy lasts an hour after the run's last event. After that you get
410 stream_expired: read the finished run instead. - Message events carry the answer's text, so only callers with
run.view_contentreceive them. Everyone else still sees the run's shape: tools, approvals and the end. - New event types can appear inside v1. Skip types you don't know.
Versions and changes
Everything in v1 only grows. New endpoints, new optional fields, new values and new event types can appear, but nothing you rely on is renamed, retyped or removed. A change that would break you comes as v2, with v1 kept running beside it.
- Ignore fields you don't recognise.
- Handle unknown values, for example by showing them as they are.
- Don't depend on the order of fields in JSON.
Build with us
Ready for your first call?
Early access is open. Tell us what you want your agents to do, and we will help you get your first key.