Skip to content

Start building

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.

FieldValue
Base URLhttps://api.mydeepagents.com
FormatJSON in and out, UTF-8
AuthenticationBearer key in the Authorization header
Versionv1, in the path
DescriptionOpenAPI 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.

HTTP
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_
k7m2p9qa3tq8v1zc_
Zq3vT8xLmV4pR8sT1wY6zB3nC5dF0gJ2hK4aE9uI1oQ
  • mda_live_

    The environment chosen when the key was made: live or test.

  • 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.

RoleMay
viewerSee agents, workflows, widgets, runs, usage and connections.
operatorEverything a viewer may, plus start, approve and cancel runs and read what runs said.
builderEverything an operator may, plus edit, test and publish agents, workflows and widgets, use connections and manage triggers.
adminEverything 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

StatusCodeWhat it means
401unauthenticatedThe 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.
403scope_insufficientThe key's scopes don't include the permission the endpoint needs.
403permission_deniedThe service account's role doesn't allow it.
403ip_not_allowedThe call came from an address outside the key's allowlist.
404not_foundThe 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.

FieldWhat it means
IDsA 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.
TimesRFC 3339 in UTC with milliseconds: 2026-11-10T09:30:00.000Z.
MoneyWhole micro-dollars in fields ending _micro_usd: 1250000 is $1.25.
Field namessnake_case. Resources have id, object (what kind of thing it is), created_at and updated_at.
Fixed valuesLowercase strings. New values can appear inside v1, so handle a value you don't know instead of failing.
Leaving out or nullIn a PATCH, a field you leave out stays as it is; null clears it.

ID prefixes you will meet

  • agt_Agent
  • wf_Workflow
  • wgt_Widget
  • run_Agent run
  • wfr_Workflow run
  • trg_Trigger
  • evt_Trigger event
  • con_Connection
  • mcp_MCP server
  • ws_Workspace
  • usr_Person
  • sa_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.

A 422 answer
JSON
{
  "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

StatusCodeWhat it means
400bad_requestThe request is malformed: a bad parameter, cursor, order or time.
401unauthenticatedNo key, or a key that doesn't work.
403scope_insufficientThe key's scopes don't cover this.
403permission_deniedThe role doesn't allow this.
403policy_deniedA compliance rule of your organization doesn't allow this.
404not_foundNothing with that ID that this key can see.
409already_existsSomething with that name or slug already exists.
409alias_in_useThat alias still points somewhere that needs it.
412precondition_failedSomeone changed it after you read it (If-Match).
422validation_failedThe body is not valid; errors names each field.
422idempotency_key_reuseThe same Idempotency-Key came with a different body.
422publish_blockedThe definition has problems that stop it being published.
429budget_exceededA spending budget has been reached.
429llm_busyThe model provider is at capacity; try again shortly.
500internal_errorOur fault. Retry later and quote the request_id if it persists.
503unavailableA dependency is briefly down; retry with back-off.

Every error code, with what it means and what to do

Retry safely

  • Retry 429 and 503 with exponential back-off, waiting at least retry_after seconds when the answer gives it.
  • Retry network errors the same way. For run starts, send the same Idempotency-Key again, so a retry can never start a second run.
  • Don't retry other 4xx answers: 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.

Every list answers in the same envelope
JSON
{
  "object": "list",
  "data": [
    {
      "id": "agt_01jctdbahw9btk5p831xs974yq",
      "object": "agent",
      "name": "Support triage",
      "…": "…"
    }
  ],
  "has_more": true,
  "next_cursor": "b2Zmc2V0OjUwfGFndF8wMWpj…"
}
ParameterWhat it means
limit1 to 200, 50 by default. A few small lists, such as members and API keys, answer every row when you leave it out.
cursorThe previous page's next_cursor. It lasts 24 hours and only answers the question it came from.
orderA field to sort by, with a leading minus for descending, like -created_at. Each list says which fields it sorts by.
qCase-insensitive search in the list's names and text. A pasted ID finds that one item.
includetotal_count adds how many match (counted up to 10,000); facets adds counts for each filter.
created_after · updated_beforeTime 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')
done

Idempotency

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.

HTTP
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.

targetWhat it means
draftThe draft being edited: a builder's test, so it also needs agent.test (or workflow.edit for a workflow). This is the default.
stagingThe version published to staging.
liveThe version published to live.

Start an agent run

POST /v1/agents/{agent_id}/runs
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"
  }'
202 Accepted
JSON
{
  "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.

GET /v1/runs/{run_id}/events
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

text/event-stream
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

EventWhenData
run.startedThe run began.agent_version_id
message.deltaA piece of the answer's text. index counts the pieces of one message from 0.message_id, index, text
message.completedA whole message, as stored.message_id, role, content
tool.startedThe agent asked for a tool.call_id, tool, status, args
tool.finishedThe tool answered, failed or was refused (ok, error, rejected, expired, cancelled).call_id, tool, status, ms, preview
approval.requestedA tool call is waiting for a person to approve it.approval_id, call_id, tool
approval.resolvedSomeone decided.approval_id, decision
run.completed · run.failed · run.cancelledThe run ended. The last event; then the stream closes.status, usage, error

How to read a stream

  • Reconnect with the last id you received in Last-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_content receive 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.