Developers

Agent API

Create, configure, and test voice agents over HTTP.

This reference covers the agent-scoped surface only: agents, their sub-resources, and test suites. Workspace, team, billing, telephony, and conversation endpoints are deliberately out of scope.

Version 2026-07-24https://api.speakai.co37 endpoints

Quickstart

Four commands from a fresh key to a list of agents. Every call after this follows the same two-header pattern.

# 1. Your key, from Dashboard → Developers → API Keys
export API_KEY="sk_live_..."
export BASE_URL="https://api.speakai.co"

# 2. Find the workspace the key can reach
curl -s "$BASE_URL/api/org" -H "Authorization: Bearer $API_KEY"

# 3. Use its orgId on every later call
export WORKSPACE_ID="<orgId from step 2>"

# 4. List the agents in that workspace
curl -s "$BASE_URL/api/agents" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"

Authentication

Every request needs two things: an API key in the Authorization header, and — for most endpoints — a workspace id in the X-Workspace-Id header.

HeaderValueRequiredNotes
AuthorizationBearer sk_live_...alwaysThe key is hashed with HMAC-SHA256 server-side; the raw value is shown once at creation and never again. A key inherits the identity of the user who created it.
X-Workspace-Idyour workspace (org) idmost endpointsScopes the request to one workspace. Required, optional, or effectively-required depending on the endpoint — every endpoint below states which. Get the id from GET /api/org.
Content-Typeapplication/jsonon requests with a bodyRequired for POST, PUT, and PATCH.

Keys look like sk_live_ followed by 64 hex characters. Create one at Dashboard → Developers → API Keys.

API keys cannot manage API keys. The /api/api-keys endpoints reject key auth and accept only a session JWT, so keys can never mint or revoke other keys.

Auth and workspace errors

StatusBodyCause
401{"success":false,"error":"Authentication required","message":"Please provide a valid JWT token or API key"}No Authorization header.
401{"success":false,"error":"Authentication required","message":"Invalid API key"}The key is malformed, revoked, or does not exist.
400{"error":"X-Workspace-Id header required"}The endpoint requires a workspace and the header was absent.
403{"error":"Not a member of this workspace"}The key's user is not a member of the workspace you named.
404{"error":"Workspace not found"}No workspace exists with that id.

The workspace header

X-Workspace-Id decides which workspace a request acts on. It is not uniform across the API, so each endpoint below is labelled. The four cases:

Required
The request is rejected with a 400 if the header is missing. All testing and resource endpoints work this way.
Required in practice
The middleware lets the request through, but it fails deeper down. POST /api/agents returns a 500 without it.
Optional
Works either way. With the header you get workspace scope; without it you fall back to the key user's own records. Send it anyway.
Not used
The endpoint ignores the header entirely and scopes by the key's user. Sending it is harmless.

The safe default is to send it on every call. The only endpoint that must not depend on it is GET /api/org, which is how you discover the id in the first place.

Things that will bite you

Behaviour that is surprising enough to cost you an afternoon. Each one was reproduced against a running server.

high

PUT replaces nested objects wholesale — except voice

When you PUT a nested object such as chatSettings, stt, llm, telephony, or shareSettings, the whole subdocument is overwritten in MongoDB. Fields you leave out are deleted, then silently re-filled with schema defaults on the next read — so the response can look correct while your stored data changed. voice is the one exception: it is flattened into dotted paths server-side, so a partial voice update merges and preserves pronunciation rules and dictionary ids.

What happens
{
  "before": {
    "chatSettings": {
      "welcomeMessage": "",
      "language": "en-US",
      "maxSessionLength": 3,
      "maxResponseLength": 30
    }
  },
  "request": {
    "chatSettings": {
      "welcomeMessage": "Hi there!"
    }
  },
  "after": {
    "chatSettings": {
      "welcomeMessage": "Hi there!"
    }
  },
  "readsBackAs": {
    "chatSettings": {
      "welcomeMessage": "Hi there!",
      "language": "multi",
      "maxSessionLength": 3,
      "maxResponseLength": 30
    }
  },
  "note": "language silently moved from en-US to the multi default."
}

What to do: Read the agent first and send the complete nested object back, or address a single leaf with a dotted key such as {"voice.voiceId": "..."}.

high

POST /api/agents fails with a 500 if X-Workspace-Id is missing

The workspace middleware on agent routes is optional, so a create request without the header is allowed through — and then fails at the database layer. You get a 500 with a raw Mongoose message rather than a 400.

What happens
{
  "response": {
    "success": false,
    "error": "Failed to create agent",
    "message": "Agent validation failed: orgId: Org ID is required"
  }
}

What to do: Always send X-Workspace-Id when creating an agent.

medium

Updating instructions, personality, or chatSettings can start a billable test run

A successful update that touches instructions, personality, or chatSettings emits an internal agent.updated event. If the agent's test suite has autoRunOnInstructionSave enabled, a test run starts on its own and consumes credits.

What to do: Set autoRunOnInstructionSave to false on the suite if you are driving frequent updates from a script.

low

Response envelopes are not uniform

Agent endpoints return the object at the top level ({ success, agent }). Testing endpoints nest it ({ success, data: { suite } }). Workspace-header errors return a bare { error } with no success field. Parse defensively rather than assuming one shape.

What to do: Check the HTTP status first, then read the shape documented for that specific endpoint.

low

Deletes are soft and not idempotent

DELETE sets isDeleted and deletedAt rather than removing the document. The first call returns 200; every call after that returns 404 because the agent is already filtered out of reads.

What to do: Treat a 404 on delete as already-deleted rather than an error.

Download the collection

Both files are generated from the same source as this page, so they never drift from what you see here.

Finding your workspace id

Not an agent endpoint, but you need it before anything else works. This is the only call that does not require the X-Workspace-Id header.

GET/api/org

List the workspaces your key can reach

Returns every workspace the key's user is a member of, with your role and the agent count in each. Use the orgId field as X-Workspace-Id on all later calls.

X-Workspace-Id: Not used

Responses

200 — Workspaces the key's user belongs to.
{
  "success": true,
  "workspaces": [
    {
      "orgId": "a1013f16-7149-4f28-b190-c51a0fd29ed5",
      "name": "Acme Workspace",
      "slug": "acme-workspace",
      "ownerId": "bbf36313-fad0-498d-b965-427568726d3d",
      "members": [
        {
          "userId": "bbf36313-fad0-498d-b965-427568726d3d",
          "email": "you@example.com",
          "role": "owner",
          "joinedAt": "2026-02-26T15:54:56.496Z"
        }
      ],
      "myRole": "owner",
      "agentCount": 2
    }
  ]
}
curl
curl -X GET "$BASE_URL/api/org" \
  -H "Authorization: Bearer $API_KEY"

Agents

Create, read, update, publish, and delete agents.

GET/api/agents

List agents

With X-Workspace-Id, returns every agent in that workspace. Without it, falls back to agents owned by the key's user — which is usually the same set but not guaranteed, so send the header.

X-Workspace-Id: Optional

Responses

200 — Agents, newest first.
{
  "success": true,
  "agents": [
    {
      "agentId": "e666809a-65c6-4c58-9690-19b3d83e7133",
      "name": "Nimbus from Nimbus Labs",
      "status": "active",
      "orgId": "a1013f16-7149-4f28-b190-c51a0fd29ed5",
      "userId": "bbf36313-fad0-498d-b965-427568726d3d",
      "conversationMode": "voice"
    }
  ]
}
curl
curl -X GET "$BASE_URL/api/agents" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
GET/api/agents/{id}

Get one agent

Returns the full agent document. Draft agents automatically include their generation state; add ?include=generation to force it on an active agent.

X-Workspace-Id: Optional

Path parameters

NameTypeDescription
idstringThe agentId (a UUID), not the Mongo _id.

Query parameters

NameTypeReq.Description
includestringnoSet to generation to attach the AI generation record.

Responses

200 — The agent.
{
  "success": true,
  "agent": {
    "agentId": "3f5e236b-204f-4ae1-bb93-02ba694e636a",
    "name": "Docs Probe",
    "status": "active"
  }
}
404 — No such agent in scope.
{
  "success": false,
  "error": "Agent not found"
}
curl
curl -X GET "$BASE_URL/api/agents/{id}" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
POST/api/agents

Create an agent

Only name is meaningfully required — every other field falls back to a default. X-Workspace-Id is effectively required: without it the request fails with a 500 from the database layer.

X-Workspace-Id: Required in practice

Request body

NameTypeReq.Description
namestringyesDisplay name.
personalitystringnoTone and character. Defaults to "Friendly and helpful assistant".
instructionsstringnoThe system prompt. Defaults to "You are a helpful AI assistant."
voice.providerenumnoOne of elevenlabs, openai, deepgram, cartesia, google, azure.
voice.voiceIdstringnoProvider-specific voice id.
llm.providerenumnoopenai or google. Defaults to openai.
llm.modelstringnoDefaults to gpt-5.4.
stt.providerenumnodeepgram, openai, google, azure, groq, or assemblyai. Defaults to deepgram.
conversationModeenumnovoice or avatar. Defaults to voice.
creativityLevelnumberno0 to 1. Defaults to 0.5.
chatSettingsobjectnowelcomeMessage, conversationStarters, maxResponseLength (10-50), topicsToAvoid, maxSessionLength, language.
recordingEnabledbooleannoDefaults to false.
websiteUrlstringnoValidated as a URL when present.
Request
{
  "name": "Support Agent",
  "personality": "friendly and concise",
  "instructions": "Answer billing questions. Escalate refunds to a human.",
  "voice": {
    "provider": "elevenlabs",
    "voiceId": "21m00Tcm4TlvDq8ikWAM"
  },
  "llm": {
    "provider": "openai",
    "model": "gpt-5.4"
  },
  "chatSettings": {
    "welcomeMessage": "Hi, how can I help?",
    "maxResponseLength": 30,
    "language": "en-US"
  }
}

Responses

201 — Created. The response contains the fully defaulted agent.
{
  "success": true,
  "agent": {
    "agentId": "3f5e236b-204f-4ae1-bb93-02ba694e636a",
    "name": "Support Agent",
    "personality": "friendly and concise",
    "instructions": "Answer billing questions. Escalate refunds to a human.",
    "status": "active",
    "conversationMode": "voice",
    "creativityLevel": 0.5,
    "stt": {
      "provider": "deepgram",
      "model": "nova-3",
      "lexiconTerms": []
    },
    "voice": {
      "provider": "elevenlabs",
      "voiceId": "21m00Tcm4TlvDq8ikWAM",
      "pronunciationSyncFailed": false
    },
    "llm": {
      "provider": "openai",
      "model": "gpt-5.4"
    },
    "chatSettings": {
      "welcomeMessage": "Hi, how can I help?",
      "conversationStarters": [],
      "topicsToAvoid": [],
      "maxSessionLength": 3,
      "language": "en-US",
      "maxResponseLength": 30
    },
    "shareSettings": {
      "isPublic": false,
      "allowedDomains": []
    },
    "isDeleted": false,
    "createdAt": "2026-07-24T19:37:36.742Z"
  }
}
500 — X-Workspace-Id was missing.
{
  "success": false,
  "error": "Failed to create agent",
  "message": "Agent validation failed: orgId: Org ID is required"
}
curl
curl -X POST "$BASE_URL/api/agents" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{"name":"Support Agent","personality":"friendly and concise","instructions":"Answer billing questions. Escalate refunds to a human.","voice":{"provider":"elevenlabs","voiceId":"21m00Tcm4TlvDq8ikWAM"},"llm":{"provider":"openai","model":"gpt-5.4"},"chatSettings":{"welcomeMessage":"Hi, how can I help?","maxResponseLength":30,"language":"en-US"}}'
PUT/api/agents/{id}

Update an agent

Partial update at the top level: fields you omit are left alone. Nested objects are the trap — everything except voice is replaced wholesale.

X-Workspace-Id: Optional

Path parameters

NameTypeDescription
idstringThe agentId.

Request body

NameTypeReq.Description
any writable agent fieldmixednoSame field set as create.
dotted keysstringnoKeys such as voice.voiceId update a single leaf without touching siblings.
Request
{
  "name": "Support Agent v2",
  "creativityLevel": 0.8,
  "voice.voiceId": "NEW_VOICE_ID"
}

Responses

200 — The updated agent.
{
  "success": true,
  "agent": {
    "agentId": "3f5e236b-204f-4ae1-bb93-02ba694e636a",
    "name": "Support Agent v2",
    "creativityLevel": 0.8
  }
}
400 — A validation error the server considers user-facing, such as a phone number already assigned to another agent.
{
  "success": false,
  "error": "Phone number is already assigned to another agent"
}
404 — No such agent in scope.
{
  "success": false,
  "error": "Agent not found"
}
curl
curl -X PUT "$BASE_URL/api/agents/{id}" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{"name":"Support Agent v2","creativityLevel":0.8,"voice.voiceId":"NEW_VOICE_ID"}'
POST/api/agents/{id}/save-draft

Save draft changes

Same update semantics as PUT, but intended for in-progress edits during the build flow. Returns the agent rather than a status-only body.

X-Workspace-Id: Optional

Path parameters

NameTypeDescription
idstringThe agentId.

Request body

NameTypeReq.Description
any writable agent fieldmixednoPartial agent payload.
Request
{
  "personality": "calm and concise"
}

Responses

200 — The saved agent.
{
  "success": true,
  "agent": {
    "agentId": "3f5e236b-204f-4ae1-bb93-02ba694e636a"
  }
}
curl
curl -X POST "$BASE_URL/api/agents/{id}/save-draft" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{"personality":"calm and concise"}'
POST/api/agents/{id}/publish

Publish an agent

Moves the agent to active so it can take conversations. Takes no body. Publishing an already-active agent is harmless.

X-Workspace-Id: Optional

Path parameters

NameTypeDescription
idstringThe agentId.

Responses

200 — The published agent.
{
  "success": true,
  "agent": {
    "agentId": "3f5e236b-204f-4ae1-bb93-02ba694e636a",
    "status": "active"
  }
}
404 — Not found or not publishable.
{
  "success": false,
  "error": "Agent not found or not ready to publish"
}
curl
curl -X POST "$BASE_URL/api/agents/{id}/publish" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
DELETE/api/agents/{id}

Delete an agent

Soft delete. The document stays in the database with isDeleted true and a deletedAt timestamp, and disappears from every read.

X-Workspace-Id: Optional

Path parameters

NameTypeDescription
idstringThe agentId.

Responses

200 — Deleted.
{
  "success": true,
  "message": "Agent deleted successfully"
}
404 — Already deleted, or never existed.
{
  "success": false,
  "error": "Agent not found"
}
curl
curl -X DELETE "$BASE_URL/api/agents/{id}" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"

Test suites

Define scenarios and grading criteria for an agent, then run them and read the scores. Every endpoint here requires X-Workspace-Id, and every response nests its payload under data.

GET/api/testing/{agentId}/suite

Get the test suite

Returns null when the agent has no suite yet, with a 200 rather than a 404.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.

Responses

200 — The suite, or null.
{
  "success": true,
  "data": {
    "suite": null
  }
}
403 — The agent belongs to a different workspace.
{
  "success": false,
  "error": "Forbidden"
}
curl
curl -X GET "$BASE_URL/api/testing/{agentId}/suite" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
PUT/api/testing/{agentId}/suite

Create or replace the test suite

Creates the suite if none exists, otherwise updates it. Top-level fields you omit are preserved, so sending only maxCostPerRun keeps your scenarios. Unknown fields are rejected outright.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.

Request body

NameTypeReq.Description
scenariosarraynoTest scenarios. See the scenario fields below.
scenarios[].scenarioIdstringyesYour own stable id for the scenario.
scenarios[].namestringyesMust be non-empty when isEnabled is true.
scenarios[].descriptionstringnoFree text.
scenarios[].userMessagesstring[]yesThe simulated user turns. Must have at least one entry when isEnabled is true.
scenarios[].categoryenumyesgreeting, kb_retrieval, off_topic, edge_case, or custom.
scenarios[].isEnabledbooleanyesDisabled scenarios are kept but skipped, and relax the validation on name and userMessages.
scenarios[].criteriaarrayyesGrading criteria. May be empty.
criteria[].criterionIdstringyesYour own stable id.
criteria[].namestringyesLabel. May be empty.
criteria[].evaluationPromptstringyesWhat the judge is asked. May be empty for non-LLM types.
criteria[].weightnumberyes0 to 10.
criteria[].isCriticalbooleanyesA failed critical criterion fails the whole scenario.
criteria[].typeenumnollm_judged, response_length, regex_match, or tool_called.
criteria[].maxWordsnumbernoFor response_length.
criteria[].regexPatternstringnoFor regex_match.
criteria[].mustMatchbooleannoFor regex_match: whether matching means pass.
criteria[].expectedToolNamestringnoFor tool_called.
maxCostPerRunnumberno0 to 100, in dollars. The run aborts if it would exceed this.
autoRunOnKbUpdatebooleannoStart a run when the knowledge base changes.
autoRunOnInstructionSavebooleannoStart a run when instructions, personality, or chatSettings change.
scheduledCronstring | nullnoCron expression for scheduled runs, or null.
Request
{
  "scenarios": [
    {
      "scenarioId": "greeting-1",
      "name": "Greets the caller",
      "description": "Agent should greet and offer help",
      "userMessages": [
        "Hi there"
      ],
      "criteria": [
        {
          "criterionId": "c1",
          "name": "Greeting present",
          "evaluationPrompt": "Did the agent greet the user and offer help?",
          "weight": 5,
          "isCritical": true,
          "type": "llm_judged"
        }
      ],
      "category": "greeting",
      "isEnabled": true
    }
  ],
  "maxCostPerRun": 1,
  "autoRunOnKbUpdate": false,
  "autoRunOnInstructionSave": false,
  "scheduledCron": null
}

Responses

200 — The saved suite.
{
  "success": true,
  "data": {
    "suite": {
      "suiteId": "7ea03962-42a9-4e0a-b382-9030a5b1d895",
      "agentId": "3f5e236b-204f-4ae1-bb93-02ba694e636a",
      "orgId": "a1013f16-7149-4f28-b190-c51a0fd29ed5",
      "scenarios": [
        {
          "scenarioId": "greeting-1",
          "name": "Greets the caller",
          "isEnabled": true,
          "category": "greeting"
        }
      ],
      "maxCostPerRun": 1,
      "autoRunOnKbUpdate": false,
      "autoRunOnInstructionSave": false,
      "scheduledCron": null
    }
  }
}
400 — Validation failed. The message names the offending path.
{
  "success": false,
  "error": "\"scenarios[0].userMessages\" must contain at least 1 items"
}
curl
curl -X PUT "$BASE_URL/api/testing/{agentId}/suite" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{"scenarios":[{"scenarioId":"greeting-1","name":"Greets the caller","description":"Agent should greet and offer help","userMessages":["Hi there"],"criteria":[{"criterionId":"c1","name":"Greeting present","evaluationPrompt":"Did the agent greet the user and offer help?","weight":5,"isCritical":true,"type":"llm_judged"}],"category":"greeting","isEnabled":true}],"maxCostPerRun":1,"autoRunOnKbUpdate":false,"autoRunOnInstructionSave":false,"scheduledCron":null}'
POST/api/testing/{agentId}/suite/generate

Generate a default suite

Uses an LLM to derive scenarios from the agent's own configuration and saves them. This overwrites the existing scenarios array. Takes no body and costs a model call.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.

Responses

200 — The generated suite.
{
  "success": true,
  "data": {
    "suite": {
      "suiteId": "7ea03962-42a9-4e0a-b382-9030a5b1d895",
      "scenarios": []
    }
  }
}
curl
curl -X POST "$BASE_URL/api/testing/{agentId}/suite/generate" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
POST/api/testing/{agentId}/run

Start a test run

Creates the run and returns immediately; scenarios execute in the background. This spends credits and calls your configured model and voice providers.

X-Workspace-Id: RequiredSpends credits

Path parameters

NameTypeDescription
agentIdstringThe agentId.

Responses

200 — The run record, already in progress.
{
  "success": true,
  "data": {
    "run": {
      "runId": "…",
      "status": "running",
      "trigger": "manual"
    }
  }
}
404 — The agent has no suite.
{
  "success": false,
  "error": "No test suite configured"
}
409 — A run is already active, credits are insufficient, or no scenario is enabled.
{
  "success": false,
  "error": "Insufficient credits"
}
curl
curl -X POST "$BASE_URL/api/testing/{agentId}/run" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
GET/api/testing/{agentId}/run/active

Get the active run

Returns the running or paused run, or null when nothing is in flight.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.

Responses

200 — The active run, or null.
{
  "success": true,
  "data": {
    "run": null
  }
}
curl
curl -X GET "$BASE_URL/api/testing/{agentId}/run/active" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
POST/api/testing/{agentId}/run/{runId}/pause

Pause a run

Stops after the current scenario finishes. Takes no body.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.
runIdstringThe runId.

Responses

200 — Paused.
{
  "success": true
}
404 — No such run in this workspace.
{
  "success": false,
  "error": "Run not found"
}
curl
curl -X POST "$BASE_URL/api/testing/{agentId}/run/{runId}/pause" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
POST/api/testing/{agentId}/run/{runId}/resume

Resume a run

Continues a paused run from where it stopped. Takes no body.

X-Workspace-Id: RequiredSpends credits

Path parameters

NameTypeDescription
agentIdstringThe agentId.
runIdstringThe runId.

Responses

200 — Resumed.
{
  "success": true
}
curl
curl -X POST "$BASE_URL/api/testing/{agentId}/run/{runId}/resume" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
POST/api/testing/{agentId}/run/{runId}/cancel

Cancel a run

Ends the run permanently. Scenarios already scored keep their results. Takes no body.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.
runIdstringThe runId.

Responses

200 — Cancelled.
{
  "success": true
}
curl
curl -X POST "$BASE_URL/api/testing/{agentId}/run/{runId}/cancel" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
GET/api/testing/{agentId}/runs

List past runs

Newest first.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.

Query parameters

NameTypeReq.Description
limitnumbernoDefaults to 20, capped at 100.

Responses

200 — The runs.
{
  "success": true,
  "data": {
    "runs": []
  }
}
curl
curl -X GET "$BASE_URL/api/testing/{agentId}/runs" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
GET/api/testing/{agentId}/runs/{runId}

Get one run in full

Includes per-scenario results, per-criterion scores, and any recommendations produced by the run.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.
runIdstringThe runId.

Responses

200 — The run.
{
  "success": true,
  "data": {
    "run": {
      "runId": "…",
      "status": "completed",
      "overallScore": 86
    }
  }
}
404 — No such run in this workspace.
{
  "success": false,
  "error": "Run not found"
}
curl
curl -X GET "$BASE_URL/api/testing/{agentId}/runs/{runId}" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
POST/api/testing/{agentId}/runs/{runId}/recommendations/{recId}/apply

Apply a recommendation

Writes a run's suggested fix into the agent configuration. Takes no body. This mutates the agent.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.
runIdstringThe runId.
recIdstringThe recommendation id from the run detail.

Responses

200 — Applied.
{
  "success": true,
  "data": {}
}
400 — The recommendation carries no actionable fix.
{
  "success": false,
  "error": "Recommendation has no suggested fix"
}
404 — Run or recommendation not found.
{
  "success": false,
  "error": "Recommendation not found"
}
curl
curl -X POST "$BASE_URL/api/testing/{agentId}/runs/{runId}/recommendations/{recId}/apply" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
GET/api/testing/{agentId}/baseline

Get the baseline

The reference snapshot later runs are compared against. Null until a baseline is set.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.

Responses

200 — The baseline, or null.
{
  "success": true,
  "data": {
    "baseline": null
  }
}
curl
curl -X GET "$BASE_URL/api/testing/{agentId}/baseline" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
GET/api/testing/{agentId}/score-history

Get score history

Flattened points for charting: runId, score, timestamp, and what triggered the run.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.

Responses

200 — The history.
{
  "success": true,
  "data": {
    "history": [
      {
        "runId": "…",
        "score": 86,
        "timestamp": "2026-07-24T19:38:48.792Z",
        "trigger": "manual"
      }
    ]
  }
}
curl
curl -X GET "$BASE_URL/api/testing/{agentId}/score-history" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"

Agent resources

Links and documents the agent can surface during a conversation. All of these require X-Workspace-Id.

GET/api/agents/{agentId}/resources

List resources

Paginated, with optional text search.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.

Query parameters

NameTypeReq.Description
pagenumbernoDefaults to 1.
limitnumberno1 to 100. Defaults to 10.
searchstringnoFree-text filter.

Responses

200 — The resources.
{
  "success": true,
  "data": {
    "resources": [],
    "total": 0,
    "page": 1,
    "limit": 10
  }
}
curl
curl -X GET "$BASE_URL/api/agents/{agentId}/resources" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
POST/api/agents/{agentId}/resources

Add a resource

All four of url, title, description, and action are required.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.

Request body

NameTypeReq.Description
urlstringyesMust be a valid URI.
titlestringyesUp to 200 characters.
descriptionstringyesUp to 1000 characters.
actionenumyeslink or presentation.
contentTypeenumnovideo, pdf, or image.
Request
{
  "url": "https://example.com/pricing",
  "title": "Pricing page",
  "description": "Current plans and prices",
  "action": "link"
}

Responses

201 — Created.
{
  "success": true,
  "data": {
    "resource": {
      "resourceId": "…"
    }
  }
}
curl
curl -X POST "$BASE_URL/api/agents/{agentId}/resources" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/pricing","title":"Pricing page","description":"Current plans and prices","action":"link"}'
POST/api/agents/{agentId}/resources/bulk

Add resources in bulk

Between 1 and 100 resources per call, each with the same field rules as a single create.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.

Request body

NameTypeReq.Description
resourcesarrayyes1 to 100 resource objects.
Request
{
  "resources": [
    {
      "url": "https://example.com/docs",
      "title": "Docs",
      "description": "Product documentation",
      "action": "link"
    }
  ]
}

Responses

201 — Created.
{
  "success": true,
  "data": {
    "created": 1
  }
}
curl
curl -X POST "$BASE_URL/api/agents/{agentId}/resources/bulk" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{"resources":[{"url":"https://example.com/docs","title":"Docs","description":"Product documentation","action":"link"}]}'
PUT/api/agents/{agentId}/resources/{resourceId}

Update a resource

Send at least one field. All fields are optional individually.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.
resourceIdstringThe resourceId.

Request body

NameTypeReq.Description
url | title | description | action | contentTypemixednoAt least one must be present.
Request
{
  "title": "Pricing (2026)"
}

Responses

200 — Updated.
{
  "success": true,
  "data": {
    "resource": {}
  }
}
curl
curl -X PUT "$BASE_URL/api/agents/{agentId}/resources/{resourceId}" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{"title":"Pricing (2026)"}'
DELETE/api/agents/{agentId}/resources/{resourceId}

Delete a resource

Soft delete, same as agents.

X-Workspace-Id: Required

Path parameters

NameTypeDescription
agentIdstringThe agentId.
resourceIdstringThe resourceId.

Responses

200 — Deleted.
{
  "success": true
}
curl
curl -X DELETE "$BASE_URL/api/agents/{agentId}/resources/{resourceId}" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"

Sharing and the widget

Publish an agent as a public widget and control how the bubble looks. These endpoints authenticate with the key but do not consult X-Workspace-Id.

POST/api/agents/{id}/share

Create or update the share link

Returns a long-lived share token plus a short code and a ready-made widget URL.

X-Workspace-Id: Not used

Path parameters

NameTypeDescription
idstringThe agentId.

Request body

NameTypeReq.Description
isPublicbooleannoWhether the link works without further auth.
allowedDomainsstring[]noRestrict embedding to these origins.
expiresAtstringnoISO timestamp after which the link stops working.
Request
{
  "isPublic": true,
  "allowedDomains": [
    "example.com"
  ]
}

Responses

200 — The share details.
{
  "success": true,
  "shareToken": "b5a4a9bc11e6ff2f40e70448ecc0f9e153f5d283416dbfd663a4b134cd424a8e",
  "shareLink": "https://agents.speakai.co/widget/nkqOg5iJtU",
  "shortCode": "nkqOg5iJtU"
}
curl
curl -X POST "$BASE_URL/api/agents/{id}/share" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"isPublic":true,"allowedDomains":["example.com"]}'
DELETE/api/agents/{id}/share

Revoke the share link

Invalidates the token and short code immediately.

X-Workspace-Id: Not used

Path parameters

NameTypeDescription
idstringThe agentId.

Responses

200 — Revoked.
{
  "success": true
}
curl
curl -X DELETE "$BASE_URL/api/agents/{id}/share" \
  -H "Authorization: Bearer $API_KEY"
PATCH/api/agents/{id}/widget-config

Update widget appearance

Partial update of shareSettings.widgetConfig. Returns a message rather than the agent.

X-Workspace-Id: Not used

Path parameters

NameTypeDescription
idstringThe agentId.

Request body

NameTypeReq.Description
displayModeenumnofloating or inline.
positionenumnobottom-left or bottom-right.
themeenumnolight, dark, or auto.
buttonSizeenumnosmall, medium, or large.
bubbleIconTypeenumnoavatar or generic.
buttonColorstringnoCSS colour.
backgroundColorstringnoCSS colour.
fontFamilystringnoCSS font stack.
borderRadiusnumbernoPixels.
compactModebooleannoDenser layout.
showAvatarbooleannoShow the avatar in the panel.
voiceEnabledbooleannoOffer voice mode.
enableUserCamerabooleannoAsk for the visitor's camera.
chatOpenByDefaultbooleannoOpen the panel on load.
Request
{
  "theme": "dark",
  "position": "bottom-left"
}

Responses

200 — Updated.
{
  "success": true,
  "message": "Widget config updated successfully"
}
curl
curl -X PATCH "$BASE_URL/api/agents/{id}/widget-config" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"theme":"dark","position":"bottom-left"}'

Pronunciation

Teach the agent how to say brand names and jargon. Rules sync to ElevenLabs when that is the configured voice provider.

GET/api/agents/{id}/pronunciation

Get pronunciation rules

Returns the rules and the ElevenLabs sync state. syncStatus is not_applicable when the agent does not use ElevenLabs.

X-Workspace-Id: Optional

Path parameters

NameTypeDescription
idstringThe agentId.

Responses

200 — Rules and sync state.
{
  "success": true,
  "rules": [],
  "elevenLabsDictId": null,
  "elevenLabsVersionId": null,
  "syncedAt": null,
  "syncStatus": "not_applicable"
}
curl
curl -X GET "$BASE_URL/api/agents/{id}/pronunciation" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"
PUT/api/agents/{id}/pronunciation

Replace pronunciation rules

Replaces the whole rule set, up to 500 rules, then syncs to the provider. Alias rules must be plain English respelling — IPA characters are rejected, because the voice models in use would read them aloud literally.

X-Workspace-Id: Optional

Path parameters

NameTypeDescription
idstringThe agentId.

Request body

NameTypeReq.Description
rulesarrayyesUp to 500 rules.
rules[].termstringyesThe written form, 1 to 200 characters.
rules[].pronounceAsstringyesPlain English respelling such as mih-TREKS, or a CMU ARPAbet string when type is phoneme.
rules[].typeenumnoalias (default) or phoneme.
rules[].caseSensitivebooleannoDefaults to false.
rules[].wordBoundariesbooleannoDefaults to true.
Request
{
  "rules": [
    {
      "term": "Mitrex",
      "pronounceAs": "mih-TREKS",
      "type": "alias"
    }
  ]
}

Responses

200 — Saved and synced.
{
  "success": true,
  "rules": [
    {
      "term": "Mitrex",
      "pronounceAs": "mih-TREKS"
    }
  ]
}
400 — IPA characters were found in an alias rule.
{
  "success": false,
  "error": "Pronunciation must use plain English respelling, not IPA. Remove IPA characters (ɪ, ɛ, ə, æ, etc.) and use capital letters, dashes, or apostrophes to force pronunciation. Example: 'Mitrex' → 'mih-TREKS'."
}
curl
curl -X PUT "$BASE_URL/api/agents/{id}/pronunciation" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{"rules":[{"term":"Mitrex","pronounceAs":"mih-TREKS","type":"alias"}]}'
POST/api/agents/{id}/pronunciation/copy-from/{sourceAgentId}

Copy rules from another agent

Replaces this agent's rules with the source agent's, then syncs. Takes no body.

X-Workspace-Id: Optional

Path parameters

NameTypeDescription
idstringThe destination agentId.
sourceAgentIdstringThe agent to copy from.

Responses

200 — Copied.
{
  "success": true,
  "rules": []
}
curl
curl -X POST "$BASE_URL/api/agents/{id}/pronunciation/copy-from/{sourceAgentId}" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"

Data collection fields

Fields the agent gathers during a conversation. Fields are instances of a shared template.

GET/api/agents/{agentId}/data-collection/fields

List collection fields

The only agent endpoint that is public — it sits above the auth middleware so the widget and worker can read it without a key.

X-Workspace-Id: Not used

Path parameters

NameTypeDescription
agentIdstringThe agentId.

Responses

200 — The fields, in display order.
{
  "success": true,
  "data": {
    "fields": []
  }
}
curl
curl -X GET "$BASE_URL/api/agents/{agentId}/data-collection/fields" \
  -H "Authorization: Bearer $API_KEY"
POST/api/agents/{agentId}/data-collection/fields

Add a field

Attaches an existing template to the agent.

X-Workspace-Id: Optional

Path parameters

NameTypeDescription
agentIdstringThe agentId.

Request body

NameTypeReq.Description
templateIdstringyesAn existing data collection template.
requiredbooleannoWhether the agent must obtain it.
customConfigobjectnoPer-agent overrides of the template.
maxPromptAttemptsnumbernoHow many times to ask before giving up.
noResponseBehaviorstringnoWhat to do when the visitor will not answer.
triggerConditionstringnoWhen to ask.
ordernumbernoDisplay and ask order.
Request
{
  "templateId": "tmpl_email",
  "required": true,
  "maxPromptAttempts": 2
}

Responses

201 — Added.
{
  "success": true,
  "data": {
    "field": {}
  }
}
404 — The template does not exist.
{
  "success": false,
  "error": "Template not found"
}
curl
curl -X POST "$BASE_URL/api/agents/{agentId}/data-collection/fields" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{"templateId":"tmpl_email","required":true,"maxPromptAttempts":2}'
PUT/api/agents/{agentId}/data-collection/fields/reorder

Reorder fields

Send the field ids in the order you want them asked.

X-Workspace-Id: Optional

Path parameters

NameTypeDescription
agentIdstringThe agentId.

Request body

NameTypeReq.Description
fieldIdsstring[]yesOrdered field ids.
Request
{
  "fieldIds": [
    "fld_2",
    "fld_1"
  ]
}

Responses

200 — Reordered.
{
  "success": true
}
curl
curl -X PUT "$BASE_URL/api/agents/{agentId}/data-collection/fields/reorder" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{"fieldIds":["fld_2","fld_1"]}'
PUT/api/agents/{agentId}/data-collection/fields/{fieldId}

Update a field

Partial update of one field's configuration.

X-Workspace-Id: Optional

Path parameters

NameTypeDescription
agentIdstringThe agentId.
fieldIdstringThe fieldId.

Request body

NameTypeReq.Description
required | customConfig | maxPromptAttempts | noResponseBehavior | triggerCondition | ordermixednoAny subset.
Request
{
  "required": false
}

Responses

200 — Updated.
{
  "success": true,
  "data": {
    "field": {}
  }
}
curl
curl -X PUT "$BASE_URL/api/agents/{agentId}/data-collection/fields/{fieldId}" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{"required":false}'
DELETE/api/agents/{agentId}/data-collection/fields/{fieldId}

Remove a field

Detaches the field from the agent. The underlying template is untouched.

X-Workspace-Id: Optional

Path parameters

NameTypeDescription
agentIdstringThe agentId.
fieldIdstringThe fieldId.

Responses

200 — Removed.
{
  "success": true
}
curl
curl -X DELETE "$BASE_URL/api/agents/{agentId}/data-collection/fields/{fieldId}" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Workspace-Id: $WORKSPACE_ID"

Accepted values

AgentStatus

draftprocessingactiveinactive

STTProvider

deepgramopenaigoogleazuregroqassemblyai

TTSProvider

elevenlabsopenaideepgramcartesiagoogleazure

LLMProvider

openaigoogle

AvatarProvider

beytavusheygensynthesiad-id

ConversationMode

voiceavatar

ScenarioCategory

greetingkb_retrievaloff_topicedge_casecustom

CriterionType

llm_judgedresponse_lengthregex_matchtool_called

ResourceAction

linkpresentation

ResourceContentType

videopdfimage