Skip to main content

API Channel Reference

The API channel is a REST API for talking to your AI agents over HTTPS: create a conversation, send one message at a time, and receive the agent's reply synchronously or as a server-sent event stream. Authentication requires an API key, created in Dashboard → Settings → API Keys, sent as Authorization: Bearer be_xxxx. Conversation history is managed server-side, so each request carries only the new message.

Download OpenAPI spec (OpenAPI 3.1)

Overview

The API channel lets external systems converse with a company agent over HTTP. The agent behaves exactly as it does on the call, chat, and email channels: same configuration, same system prompt, and its tools execute on the server as part of handling each request.

The API is stateful: conversation history is maintained on the platform. Each request carries only the new message and a conversation ID — the server reconstructs the full context, runs the agent, and returns the reply. Do not include previous messages in requests; history is managed for you.

  • Base URL: https://staging.ai.binaryelements.com
  • All bodies are JSON (Content-Type: application/json).
  • All timestamps are ISO 8601.

Authentication

Every request needs a per-company API key, created in Dashboard → Settings → API Keys. The raw key is shown exactly once at creation; only its SHA-256 hash is stored. Keys can be revoked at any time and revocation takes effect on the next request (no caching).

Authorization: Bearer be_xxxx

The key identifies your company server-side — you never pass a companyId. Requests for agents or conversations belonging to another company fail with 404/403.

Getting your API key

  1. Sign in to the dashboard with your company account.
  2. Go to Settings → API Keys.
  3. Click Create API Key, give the key a name (e.g. "Production integration"), and copy the key immediately — it is shown only once. Only a SHA-256 hash is stored server-side, so it cannot be recovered later.
  4. Pass it on every request as Authorization: Bearer be_xxxx.
  5. Keys can be revoked at any time from the same page; revocation is immediate — any integration using a revoked key stops working on its next request.

Treat API keys like passwords: store them in a secrets manager, never commit them to source control, and create separate keys per integration so you can revoke them independently.

Quickstart

BASE="https://staging.ai.binaryelements.com"
KEY="be_xxxx"   # from Dashboard → Settings → API Keys

# 1. Start a conversation
CONV=$(curl -s -X POST "$BASE/api/v1/conversations" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"agentId":"9f2b6c1e-1111-4111-8111-111111111111"}' | jq -r .conversationId)

# 2. Send a message (single message only — the server holds the history)
curl -s -X POST "$BASE/api/v1/conversations/$CONV/messages" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"message":"Hi, where is my order 42?"}' | jq .

# 3. Continue the conversation — send only the new message
curl -s -X POST "$BASE/api/v1/conversations/$CONV/messages" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"message":"And when will it arrive?"}' | jq .

# 4. Fetch the conversation + history
curl -s "$BASE/api/v1/conversations/$CONV" -H "Authorization: Bearer $KEY" | jq .

# 5. End the conversation
curl -s -X POST "$BASE/api/v1/conversations/$CONV/end" -H "Authorization: Bearer $KEY" | jq .

Autonomous jobs

Use an autonomous job when the agent should complete a task in the background instead of keeping one HTTP request open. The API accepts the job immediately, returns a triggerId, and lets your integration poll until the work succeeds, fails, expires, or is cancelled. No webhook or SSE callback is required.

IDMeaningStore it for
conversationIdThe shared agent thread and transcriptAdding later jobs to the same context and reading full history
triggerIdOne background job inside that conversationPolling, cancellation, and correlating one request with its outcome
  • The job runs the selected agent's configured prompt and assigned API Tools on BE AI servers. Your client does not execute tool calls.
  • The company API key is the authentication boundary for autonomous jobs. Interactive OTP gates do not pause worker execution.
  • A conversation may have multiple pending jobs (queued, waiting, waiting_for_reply, or running), capped by company setting maxPendingAgentJobs (default 3; editable in superadmin company settings). Only one job runs at a time per conversation; others stay queued or waiting until due. Interactive POST /api/v1/conversations/:id/messages is allowed while jobs are pending. During waiting or waiting_for_reply, the chat agent is told about that state so it can disclose it when relevant. Resume a parked job with POST /api/v1/triggers/:id/reply or asHumanReply: true on a message (exactly one waiting job).
  • From a message or job turn, the agent can call create_background_task to enqueue another job on the same conversation (optional delayMinutes / runAt). Use schedule_trigger_retry only to pause the current job for a later attempt.
  • Use a unique idempotencyKey per company for each business operation. Keys are not scoped to one conversation, and a replay returns the original job even after it has finished — without comparing the new payload.

Autonomous job quickstart

BASE="https://staging.ai.binaryelements.com"
KEY="be_xxxx"
AGENT_ID="9f2b6c1e-1111-4111-8111-111111111111"

# 1. Enqueue work. Save both IDs: one identifies the shared conversation,
#    and one identifies this individual job.
ACCEPTED=$(curl -s -X POST "$BASE/api/v1/triggers" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d "{
    \"agentId\": \"$AGENT_ID\",
    \"task\": \"Check order SO-123 and release it if payment has cleared.\",
    \"successCriteria\": \"The order is released, or the failure reason is reported.\",
    \"businessKeys\": { \"orderId\": \"SO-123\" },
    \"idempotencyKey\": \"release-order-SO-123\"
  }")

CONVERSATION_ID=$(echo "$ACCEPTED" | jq -r .conversationId)
TRIGGER_ID=$(echo "$ACCEPTED" | jq -r .triggerId)

# 2. Poll this URL until status is terminal.
#    If status is waiting_for_reply, resume with /reply (or messages + asHumanReply).
curl -s "$BASE/api/v1/triggers/$TRIGGER_ID" \
  -H "Authorization: Bearer $KEY" | jq .

# 3. When parked for human input, submit a structured decision (or use asHumanReply on messages).
curl -s -X POST "$BASE/api/v1/triggers/$TRIGGER_ID/reply" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"decision":"approved","reason":"Payment cleared in ERP."}' | jq .

# 4. List all jobs attached to the shared conversation.
curl -s "$BASE/api/v1/conversations/$CONVERSATION_ID/triggers?limit=50&offset=0" \
  -H "Authorization: Bearer $KEY" | jq .

# 5. Read the conversation transcript produced by the job.
curl -s "$BASE/api/v1/conversations/$CONVERSATION_ID" \
  -H "Authorization: Bearer $KEY" | jq .

Trigger an autonomous job

POST/api/v1/triggers

This is the recommended endpoint for new integrations. Supply an agentId to create a new API conversation, or supply an existing conversationId to continue that conversation's context. task is always required.

FieldTypeRequiredNotes
agentIdstring (UUID)when conversationId is omittedCompany-owned or public agent that will execute the job
conversationIdstring (UUID)when agentId is omittedExisting company-owned API conversation; keeps its agent and history
taskstring (1–32000)yesConcrete work for the agent to complete
successCriteriastring (1–8000)noObjective condition that tells the agent when the task is complete
businessKeysobjectnoStable business identifiers such as orderId, invoiceId, or accountId
idempotencyKeystring (1–255)recommendedUnique per company and business operation; a replay returns the original job and conversation
metadataobjectnoJob context. When the request creates a new conversation, it is also stored as conversation metadata for later turns
contactIdintegernoBind a company contact so the job can use that contact's memory
contactobjectnoLookup/create hints: phone, email, externalUserId, and optional name
customToolsarray (max 5)noSame shape as Custom tools; merged onto the conversation before the job runs
delayMinutesinteger (1–10080)noDefer start by N minutes (max 7 days). Mutually exclusive with runAt. Job starts as waiting
runAtstringnoAbsolute start: ISO UTC/offset, or company-local YYYY-MM-DDTHH:mm. Within 7 days. Mutually exclusive with delayMinutes

A new job returns 202 Accepted. A replay with the same idempotencyKey returns 200, the same IDs, and idempotent: true. Prefer the IDs from that replay — do not assume the request's conversationId was used if an older key already pointed at a different conversation. If the replay includes customTools, those tools are still merged onto the conversation even though the job is not re-queued. Deferred jobs return status: "waiting" and nextRunAt (UTC). Naive runAt values use the company timezone from settings.

{
  "conversationId": "a1b2c3d4-…",
  "triggerId": "f0e1d2c3-…",
  "status": "queued"
}

Trigger on an existing conversation

POST/api/v1/conversations/:conversationId/triggers

This alternative always uses an existing API conversation. Provide at least one of task (up to 32,000 characters), the legacy message alias (up to 8,000), or eventType. If both task and message are present, task wins. Use eventType for an external event such as invoice.payment_pending, and put the event payload in metadata. Optional customTools (max 5) merge onto the conversation the same way as on POST /api/v1/triggers. The same optional delayMinutes / runAt deferred-start fields apply.

Both trigger endpoints return 409 when the conversation is at its pending-job cap (default 3). On POST /api/v1/triggers, 409 can also mean the conversation has ended, has no agent, or the supplied agent does not match the conversation's assigned agent.

Poll and list autonomous jobs

GET/api/v1/triggers/:triggerId

Poll this endpoint until status is terminal. While a job is waiting, nextRunAt tells you when it becomes eligible for another attempt. waiting_for_reply means processing is parked for human or external input and has no automatic next run. Resume with POST /api/v1/triggers/:triggerId/reply or asHumanReply on messages. The response's timestamps and attempt limits are authoritative for that job.

{
  "triggerId": "f0e1d2c3-…",
  "conversationId": "a1b2c3d4-…",
  "agentId": "9f2b6c1e-…",
  "status": "waiting_for_reply",
  "task": "Check order SO-123 and release it if payment has cleared.",
  "successCriteria": "The order is released, or the failure reason is reported.",
  "businessKeys": { "orderId": "SO-123" },
  "metadata": {
    "source": "erp",
    "waitPayload": { "salesOrderId": "SO-123", "amount": 4200 }
  },
  "approvalId": null,
  "approvalUrl": null,
  "waitPayload": { "salesOrderId": "SO-123", "amount": 4200 },
  "resultSummary": "Manager approval is required before releasing the order.",
  "errorMessage": null,
  "attemptCount": 1,
  "maxAttempts": 3,
  "nextRunAt": null,
  "waitingReason": "Manager approval is required for order SO-123.",
  "waitingSince": "2026-06-12T08:00:07.000Z",
  "expiresAt": "2026-06-13T08:00:00.000Z",
  "lastRunAt": "2026-06-12T08:00:05.000Z",
  "startedAt": "2026-06-12T08:00:05.000Z",
  "completedAt": null,
  "createdAt": "2026-06-12T08:00:00.000Z",
  "updatedAt": "2026-06-12T08:00:07.000Z"
}
FieldHow to use it
resultSummaryLatest agent summary; final result when succeeded
errorMessageFailure or expiry detail; null when no error is recorded
attemptCountNumber of executions already claimed
maxAttemptsMaximum executions allowed for this job
nextRunAtNext eligible execution time for timed waiting; otherwise null
waitingReasonInput required while waiting_for_reply; otherwise null
waitingSinceTime the job began waiting for a reply; otherwise null
waitPayloadStructured context the agent supplied when parking (also under metadata.waitPayload); null when absent
approvalUrlLegacy only — hosted approval link if an older park minted one; null for new parks. Prefer /reply or asHumanReply
expiresAtHard runtime deadline after which the job expires
GET/api/v1/conversations/:conversationId/triggers?limit=50&offset=0

Lists jobs newest first. limit defaults to 50 and accepts 1–100; offset defaults to 0. Use total and hasMore to paginate.

{
  "conversationId": "a1b2c3d4-…",
  "triggers": [
    {
      "triggerId": "f0e1d2c3-…",
      "status": "succeeded",
      "attemptCount": 2,
      "resultSummary": "Order SO-123 was released.",
      "completedAt": "2026-06-12T08:15:04.000Z"
    }
  ],
  "total": 1,
  "hasMore": false
}

Every attempt appends to the same conversation transcript and reloads that history on the next run. Read the transcript with GET /api/v1/conversations/:conversationId. That response may also include best-effort jobId / jobStatus mirrors for the latest job — treat them as helpers, not as your source of truth while polling.

For new integrations, poll /triggers/:triggerId. GET /api/v1/jobs/:jobId is a legacy alias that returns the raw internal job object (including fields such as triggerType, inputMessage, and idempotencyKey).

Cancel an autonomous job

POST/api/v1/triggers/:triggerId/cancel

Cancellation is available while the job is queued, waiting, or waiting_for_reply. A running job is already executing and cannot be interrupted in v1; running and terminal jobs return 409.

curl -s -X POST "$BASE/api/v1/triggers/$TRIGGER_ID/cancel" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"reason":"The order was cancelled in the source system."}' | jq .

reason is optional (1–1000 characters). A successful 200 response returns the full trigger object with status: "cancelled". The reason is stored on the job and returned in errorMessage.

Reply to a waiting job

POST/api/v1/triggers/:triggerId/reply

Submit a structured human decision while the job is waiting_for_reply. The job returns to queued and the worker resumes with the decision injected as system context. Prefer this for machine integrations; use asHumanReply on messages for freeform ops chat.

FieldTypeRequiredNotes
decisionstringyesapproved or rejected
reasonstring (1–4000)noOptional note stored with the human reply
curl -s -X POST "$BASE/api/v1/triggers/$TRIGGER_ID/reply" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"decision":"approved","reason":"Payment cleared in ERP."}' | jq .

A successful 200 returns the trigger object (typically status: "queued"). Jobs that are not waiting_for_reply return 409.

Job lifecycle and retries

StatusMeaningWhat your client should do
queuedAccepted and waiting for a workerKeep polling
runningThe agent is executing the task and toolsKeep polling; do not retry POST
waitingThe agent deliberately scheduled a later follow-upKeep polling; inspect nextRunAt
waiting_for_replyThe agent is parked for human or external inputKeep polling; inspect waitingReason / waitPayload; resume via POST …/reply or asHumanReply on messages
succeededWork completedRead resultSummary and transcript
failedExecution failed or attempts were exhaustedRead errorMessage; decide whether to submit a new job
expiredThe job passed its runtime deadlineSubmit a new job only if the business task is still valid
cancelledA client cancelled active, non-running workNo further action

Normal transitions are queued → running → succeeded/failed. waiting is not an automatic retry of a failure. It only happens when the agent schedules a deliberate follow-up (for example, because payment is still pending): running → waiting → running. Follow-up delays are typically 1 minute to 7 days (or an absolute future nextRunAt). Every follow-up increments attemptCount and keeps the same triggerId and conversation history. Each attempt reloads the conversation's stored custom tools (including encrypted auth), so tools merged while a job is waiting apply on the next attempt — not mid-flight during running.

Human-input parking is separate: running → waiting_for_reply → queued → running. A parked job keeps the same triggerId, is never claimed on a timer, and expires at its existing hard deadline. Resume with company API key via POST /api/v1/triggers/:triggerId/reply (approved / rejected) or POST …/messages with asHumanReply: true when exactly one job is waiting (freeform provided reply). New parks do not mint hosted approval URLs; a legacy approvalUrl may still appear on older jobs until they leave waiting_for_reply.

  • Current defaults are three attempts and a 24-hour runtime window. Always use the returned maxAttempts and expiresAtbecause deployment settings may change these values.
  • Execution, network, credit, or configuration errors become failed. The platform does not automatically re-run a failed execution. If the agent schedules another wait after maxAttempts is reached, the job fails.
  • A follow-up scheduled past expiresAt expires instead of running again.
  • Polling is the v1 delivery contract. There are no trigger webhooks or trigger SSE streams.
  • Do not submit the same operation again while polling. If the create response was lost, repeat the request with the same idempotencyKey.

Create a conversation

POST/api/v1/conversations
FieldTypeRequiredNotes
agentIdstring (UUID)create: yesRequired when minting; optional on resume
conversationIdstring (UUID)noWhen set, resumes that conversation (200) instead of minting (201)
contactIdintegernoBind an existing company contact; cross-tenant ids return 404
contactobjectnoHints: phone, email, externalUserId, optional name. Lookup-first; creates when a stable identifier is new
customSystemMessagestring (1–8000)noApplied to every turn of this conversation; additive to the agent's configured prompt
metadataobjectnoStructured context the agent sees on every turn (see below)

Response 201 (create) / 200 (resume) — always includes contactId (may be null when anonymous):

{
  "conversationId": "a1b2c3d4-…",
  "contactId": null,
  "agentId": "9f2b6c1e-…",
  "createdAt": "2026-06-12T08:00:00.000Z"
}

Conversation metadata

Any metadata object you supply is injected into the agent's context as a system message on every turn, so the model can personalise replies and pull values (IDs, account tier, cart state…) straight into tool calls instead of asking the user:

CONVERSATION METADATA:
{
  "username": "john_doe",
  "accountId": "ACC-12345",
  "accountTier": "premium"
}

Keep it reasonably small (≈2 KB) — it is re-sent on every turn. Use PATCH /api/v1/conversations/:id/metadata to update it as your app's state changes.

Contact identity

Memory scopes: conversation (anonymous conversation transcripts), contact (shared across API / voice / chat after identity), and company operational aggregates. Pass memoryScope: "company" on create (or a later message) to contribute a redacted company memory when the session ends; agents on this channel can call search_company_memory for aggregate questions. Requires COMPANY_MEMORY_ENABLED=true (or 1) on private-api and jambonz; otherwise the API returns 503 instead of silently ignoring the flag. Store both conversationId and contactId from the create/resume response. Pass contactId (or stable contact hints) on the next conversation so the agent shares that contact's memory across API turns — the same store used after contact identification on voice/chat.

  • Anonymous — no contactId / hints → contactId: null; transcripts stay on the conversation until identity is supplied; no lasting contact memory is written.
  • Known contact — pass contactId belonging to your company.
  • External system person — pass contact.externalUserId (and optional phone/email/name); we look up or create once and return the id.
  • Late bind — on POST …/messages (or resume) you may send contactId when the session is still unbound and active (409 if already bound to a different contact, or if the conversation has ended).

Example create with identity:

curl -s -X POST "$BASE/api/v1/conversations" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"agentId":"9f2b6c1e-…","contact":{"externalUserId":"sap-cust-42","name":"Sam"}}'
{
  "conversationId": "a1b2c3d4-…",
  "contactId": 42,
  "agentId": "9f2b6c1e-…",
  "createdAt": "2026-06-12T08:00:00.000Z"
}

Send a message

POST/api/v1/conversations/:conversationId/messages
FieldTypeRequiredNotes
messagestring (1–32000)yesThe user's new message — only the new message
contactIdintegernoLate-bind when the conversation has no contact yet
customSystemMessagestring (1–8000)noAdded for this turn only, on top of the conversation-level one
customToolsarray (max 10)noMerged onto the conversation (by name) and reused on later turns; see Custom tools
streambooleannotrue switches to SSE; default synchronous
asHumanReplybooleannoWhen true, after a successful chat turn resume the single waiting_for_reply job with this message text (decision: "provided"). Default false — exploratory chat does not resume. 409 if zero or multiple jobs are waiting

Synchronous response 200 (returned after all tool calls have run server-side):

{
  "conversationId": "a1b2c3d4-…",
  "contactId": 42,
  "message": { "role": "assistant", "content": "Order 42 shipped yesterday…" },
  "messages": [{ "role": "assistant", "content": "Order 42 shipped yesterday…" }],
  "toolCalls": ["custom_a1b2c3d4-…_lookup_order"],
  "usage": { "promptTokens": 512, "completionTokens": 64, "totalTokens": 576 },
  "handoff": {
    "roleName": "Support",
    "greeting": "Hi, you've reached Support. How can I help you?",
    "agentId": "9f2b6c1e-…"
  },
  "agentId": "9f2b6c1e-…"
}

With asHumanReply: true and exactly one waiting job, the same 200 also includes resume fields (chat still succeeds if resume fails — see resumeError):

{
  "conversationId": "a1b2c3d4-…",
  "contactId": 42,
  "message": { "role": "assistant", "content": "Got it — I'll treat that as approval for SO-123." },
  "messages": [{ "role": "assistant", "content": "Got it — I'll treat that as approval for SO-123." }],
  "toolCalls": [],
  "usage": { "promptTokens": 400, "completionTokens": 40, "totalTokens": 440 },
  "resumedTriggerId": "f0e1d2c3-…",
  "resumedStatus": "queued"
}
  • message.content is the full reply (multiple assistant messages from tool-call continuations are joined with blank lines); messages lists them individually.
  • toolCalls contains the names of tools the agent invoked this turn.
  • When the agent hands off to another configured agent, the response also includes handoff (role name, greeting, target agentId) and top-level agentId for the agent now handling the conversation.
  • resumedTriggerId / resumedStatus appear only after a successful asHumanReply resume. If chat succeeds but resume fails, you still get 200 with resumeError; the job stays waiting_for_reply so /reply can recover.

Streaming ("stream": true)

The response is text/event-stream; each event is a data: <json> line:

EventPayloadMeaning
content{ "type": "content", "content": "…" }A chunk of assistant text
new_message{ "type": "new_message" }A new assistant message starts (after a tool run)
tool{ "type": "tool", "name": "…" }The agent is executing a tool (server-side)
handoff{ "type": "handoff", "roleName": "…", "greeting": "…", "agentId": "…" }The conversation was handed off to another agent (roster must be configured on the entry agent)
done{ "type": "done", "conversationId": "…", "contactId": null, "usage": { … } }Turn complete. With asHumanReply, may also include resumedTriggerId / resumedStatus or resumeError
error{ "type": "error", "error": "…" }An error occurred; the stream ends

Custom tools

Server-executable HTTP tool definitions (the same shape as platform API tools). The server — never your client — calls url with the parameters the LLM chooses. Tools sent on a messages request are merged onto the conversation (by name, incoming wins; omit or [] is a no-op) and reused on later messages until the conversation ends.

  • Per request: messages accept up to 10 tools; triggers accept up to 5.
  • Per conversation: at most 10 stored tools after merge (overflow → 400). Concurrent merges are best-effort.
  • Auth: authConfig is encrypted at rest, never logged, and never returned on conversation/session reads. Authenticated tools require the platform credentials encryption key to be configured.
  • Idempotent triggers: a replay with the same idempotencyKey does not re-queue the job, but any customTools in that replay are still merged.
FieldTypeRequiredNotes
namestring (≤ 20, [a-zA-Z0-9_-])yesExposed to the agent as custom_<conversationId>_<name>
descriptionstring (≤ 1024)yesTells the agent when to use the tool
urlstringyeshttps:// only. Supports {param} path placeholders
methodGET / POST / PUT / DELETEyesGET sends params as query string; POST/PUT as JSON body
authTypenone / basic / apikey / bearerno (default none)How the server authenticates to YOUR endpoint
authConfigobjectno{ username, password } / { headerName, apiKey } / { token } — persisted encrypted for the conversation; never logged or returned on session GET
parametersarray (max 20)no{ name (≤64), type, required, description (≤512) }

Update conversation metadata

PATCH/api/v1/conversations/:conversationId/metadata

Replace the conversation's metadata (and, optionally, its conversation-level customSystemMessage). The new metadata fully replaces the previous metadata and is used on every subsequent turn. No AI response is generated.

FieldTypeRequiredNotes
metadataobjectyesReplaces the stored metadata wholesale
customSystemMessagestring (1–8000)noWhen present, replaces the conversation-level custom system message; left untouched when omitted

Response 200:

{
  "conversationId": "a1b2c3d4-…",
  "metadata": { "accountTier": "enterprise", "upgradeDate": "2026-06-22" },
  "updatedAt": "2026-06-12T08:30:00.000Z"
}

Returns 409 if the conversation has ended.

Add a manual message

POST/api/v1/conversations/:conversationId/manual

Append a message from a human operator or external system without triggering an AI response. It is stored as an admin turn, becomes part of the history, and is replayed into the agent's context on the next message (the model sees [ADMIN MESSAGE - <username>] …). No AI credits are consumed.

FieldTypeRequiredNotes
messagestring (1–32000)yesThe note to add
usernamestring (1–120)noWho added it; defaults to admin when blank or omitted
metadataobjectnoStored alongside the message (e.g. department, externalId)

Response 201:

{
  "conversationId": "a1b2c3d4-…",
  "messageId": "f0e1d2c3-…",
  "addedBy": "Martin",
  "timestamp": "2026-06-12T08:05:00.000Z"
}

Returns 409 if the conversation has ended.

Get a conversation + history

GET/api/v1/conversations/:conversationId
GET/api/v1/conversations/:conversationId?include=all

Response 200:

{
  "conversationId": "a1b2c3d4-…",
  "contactId": 42,
  "agentId": "9f2b6c1e-…",
  "status": "active",
  "createdAt": "2026-06-12T08:00:00.000Z",
  "messages": [
    { "role": "user", "content": "Where is my order 42?", "timestamp": "…" },
    { "role": "assistant", "content": "Order 42 shipped yesterday…", "timestamp": "…" },
    { "role": "admin", "content": "Refund approved manually.", "timestamp": "…", "metadata": { "username": "Martin", "source": "manual" } }
  ]
}

messages contains user/assistant turns plus any admin (manual) messages by default; admin entries include a metadata object with username and source. ?include=all additionally adds system/tool entries.

End a conversation

POST/api/v1/conversations/:conversationId/end

Response 200: { "conversationId": "…", "status": "ended" }. Transcripts are retained and remain retrievable; further messages to the conversation return 409.

Errors

StatusWhen
400Invalid body (details in details), e.g. non-https custom tool URL
401Missing, malformed, unknown, or revoked API key
402Company has insufficient AI credits (minCredits / totalBalance included)
403Conversation exists but belongs to a different company
404Unknown conversation/agent, or the session is not an API-channel session
409Conversation ended or mismatched; pending job limit reached; trigger cannot be cancelled or replied from its current status; asHumanReply with zero or multiple waiting jobs
503Authenticated custom tools cannot be stored or resolved because credentials encryption is not configured on the server
500Unexpected processing failure
504Agent turn exceeded the 120 s timeout

Error shape: { "error": "<message>" } (plus details for validation errors).

Behaviour notes

  • Agent config is intact. The agent's configured system prompt, model, and tools all apply. Custom system messages and custom tools are strictly additive — they can never replace or bypass the agent's configuration. Conversation custom tools persist across turns; they remain additive to dashboard API Tools assigned to the agent.
  • Channel-inapplicable tools are excluded: end_conversation, barge_in, call_internal_contact, end_session are never offered on this channel even if configured on the agent.
  • Agent handoff: when handoff_to_agent is enabled on the entry agent and its handoffRoster lists target agents, the API channel supports the same roster-based handoff as chat. Verification requirements (OTP, etc.) apply when configured on target agents. After handoff, subsequent messages use the target agent and the session's agentId is updated.
  • Contact memory: when contactId is set on the session (create, resume, late-bind while active, or agent identify_contact / create_contact), memory tools and post-session memory use that contact — same path as identified voice/chat. Anonymous API conversations stay conversation-scoped (transcripts only) until a contact is bound. Public API cannot bind identity after the conversation has ended (409).
  • Billing: each turn consumes AI credits under the api reference type (same minimum-credit setting as chat). PAYG companies are billed by invoice and are not credit-blocked.
  • Sessions are stored with channel='api' and are not shown in the dashboard chat views in v1.
  • Timeout: synchronous turns are capped at 120 s (long tool chains should use stream: true). Autonomous jobs have their own runtime and attempt limits shown by the trigger polling response.