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.
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_xxxxThe 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
- Sign in to the dashboard with your company account.
- Go to Settings → API Keys.
- 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.
- Pass it on every request as
Authorization: Bearer be_xxxx. - 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.
| ID | Meaning | Store it for |
|---|---|---|
conversationId | The shared agent thread and transcript | Adding later jobs to the same context and reading full history |
triggerId | One background job inside that conversation | Polling, 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, orrunning), capped by company settingmaxPendingAgentJobs(default 3; editable in superadmin company settings). Only one job runs at a time per conversation; others stay queued or waiting until due. InteractivePOST /api/v1/conversations/:id/messagesis allowed while jobs are pending. Duringwaitingorwaiting_for_reply, the chat agent is told about that state so it can disclose it when relevant. Resume a parked job withPOST /api/v1/triggers/:id/replyorasHumanReply: trueon a message (exactly one waiting job). - From a message or job turn, the agent can call
create_background_taskto enqueue another job on the same conversation (optionaldelayMinutes/runAt). Useschedule_trigger_retryonly to pause the current job for a later attempt. - Use a unique
idempotencyKeyper 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
/api/v1/triggersThis 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.
| Field | Type | Required | Notes |
|---|---|---|---|
agentId | string (UUID) | when conversationId is omitted | Company-owned or public agent that will execute the job |
conversationId | string (UUID) | when agentId is omitted | Existing company-owned API conversation; keeps its agent and history |
task | string (1–32000) | yes | Concrete work for the agent to complete |
successCriteria | string (1–8000) | no | Objective condition that tells the agent when the task is complete |
businessKeys | object | no | Stable business identifiers such as orderId, invoiceId, or accountId |
idempotencyKey | string (1–255) | recommended | Unique per company and business operation; a replay returns the original job and conversation |
metadata | object | no | Job context. When the request creates a new conversation, it is also stored as conversation metadata for later turns |
contactId | integer | no | Bind a company contact so the job can use that contact's memory |
contact | object | no | Lookup/create hints: phone, email, externalUserId, and optional name |
customTools | array (max 5) | no | Same shape as Custom tools; merged onto the conversation before the job runs |
delayMinutes | integer (1–10080) | no | Defer start by N minutes (max 7 days). Mutually exclusive with runAt. Job starts as waiting |
runAt | string | no | Absolute 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
/api/v1/conversations/:conversationId/triggersThis 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
/api/v1/triggers/:triggerIdPoll 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"
}| Field | How to use it |
|---|---|
resultSummary | Latest agent summary; final result when succeeded |
errorMessage | Failure or expiry detail; null when no error is recorded |
attemptCount | Number of executions already claimed |
maxAttempts | Maximum executions allowed for this job |
nextRunAt | Next eligible execution time for timed waiting; otherwise null |
waitingReason | Input required while waiting_for_reply; otherwise null |
waitingSince | Time the job began waiting for a reply; otherwise null |
waitPayload | Structured context the agent supplied when parking (also under metadata.waitPayload); null when absent |
approvalUrl | Legacy only — hosted approval link if an older park minted one; null for new parks. Prefer /reply or asHumanReply |
expiresAt | Hard runtime deadline after which the job expires |
/api/v1/conversations/:conversationId/triggers?limit=50&offset=0Lists 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
/api/v1/triggers/:triggerId/cancelCancellation 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.
Job lifecycle and retries
| Status | Meaning | What your client should do |
|---|---|---|
queued | Accepted and waiting for a worker | Keep polling |
running | The agent is executing the task and tools | Keep polling; do not retry POST |
waiting | The agent deliberately scheduled a later follow-up | Keep polling; inspect nextRunAt |
waiting_for_reply | The agent is parked for human or external input | Keep polling; inspect waitingReason / waitPayload; resume via POST …/reply or asHumanReply on messages |
succeeded | Work completed | Read resultSummary and transcript |
failed | Execution failed or attempts were exhausted | Read errorMessage; decide whether to submit a new job |
expired | The job passed its runtime deadline | Submit a new job only if the business task is still valid |
cancelled | A client cancelled active, non-running work | No 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
maxAttemptsandexpiresAtbecause 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 aftermaxAttemptsis reached, the job fails. - A follow-up scheduled past
expiresAtexpires 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
/api/v1/conversations| Field | Type | Required | Notes |
|---|---|---|---|
agentId | string (UUID) | create: yes | Required when minting; optional on resume |
conversationId | string (UUID) | no | When set, resumes that conversation (200) instead of minting (201) |
contactId | integer | no | Bind an existing company contact; cross-tenant ids return 404 |
contact | object | no | Hints: phone, email, externalUserId, optional name. Lookup-first; creates when a stable identifier is new |
customSystemMessage | string (1–8000) | no | Applied to every turn of this conversation; additive to the agent's configured prompt |
metadata | object | no | Structured 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
contactIdbelonging 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 sendcontactIdwhen 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
/api/v1/conversations/:conversationId/messages| Field | Type | Required | Notes |
|---|---|---|---|
message | string (1–32000) | yes | The user's new message — only the new message |
contactId | integer | no | Late-bind when the conversation has no contact yet |
customSystemMessage | string (1–8000) | no | Added for this turn only, on top of the conversation-level one |
customTools | array (max 10) | no | Merged onto the conversation (by name) and reused on later turns; see Custom tools |
stream | boolean | no | true switches to SSE; default synchronous |
asHumanReply | boolean | no | When 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.contentis the full reply (multiple assistant messages from tool-call continuations are joined with blank lines);messageslists them individually.toolCallscontains 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, targetagentId) and top-levelagentIdfor the agent now handling the conversation. resumedTriggerId/resumedStatusappear only after a successfulasHumanReplyresume. If chat succeeds but resume fails, you still get200withresumeError; the job stayswaiting_for_replyso/replycan recover.
Streaming ("stream": true)
The response is text/event-stream; each event is a data: <json> line:
| Event | Payload | Meaning |
|---|---|---|
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
10tools; triggers accept up to5. - Per conversation: at most
10stored tools after merge (overflow →400). Concurrent merges are best-effort. - Auth:
authConfigis 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
idempotencyKeydoes not re-queue the job, but anycustomToolsin that replay are still merged.
| Field | Type | Required | Notes |
|---|---|---|---|
name | string (≤ 20, [a-zA-Z0-9_-]) | yes | Exposed to the agent as custom_<conversationId>_<name> |
description | string (≤ 1024) | yes | Tells the agent when to use the tool |
url | string | yes | https:// only. Supports {param} path placeholders |
method | GET / POST / PUT / DELETE | yes | GET sends params as query string; POST/PUT as JSON body |
authType | none / basic / apikey / bearer | no (default none) | How the server authenticates to YOUR endpoint |
authConfig | object | no | { username, password } / { headerName, apiKey } / { token } — persisted encrypted for the conversation; never logged or returned on session GET |
parameters | array (max 20) | no | { name (≤64), type, required, description (≤512) } |
Update conversation metadata
/api/v1/conversations/:conversationId/metadataReplace 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.
| Field | Type | Required | Notes |
|---|---|---|---|
metadata | object | yes | Replaces the stored metadata wholesale |
customSystemMessage | string (1–8000) | no | When 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
/api/v1/conversations/:conversationId/manualAppend 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.
| Field | Type | Required | Notes |
|---|---|---|---|
message | string (1–32000) | yes | The note to add |
username | string (1–120) | no | Who added it; defaults to admin when blank or omitted |
metadata | object | no | Stored 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
/api/v1/conversations/:conversationId/api/v1/conversations/:conversationId?include=allResponse 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
/api/v1/conversations/:conversationId/endResponse 200: { "conversationId": "…", "status": "ended" }. Transcripts are retained and remain retrievable; further messages to the conversation return 409.
Errors
| Status | When |
|---|---|
400 | Invalid body (details in details), e.g. non-https custom tool URL |
401 | Missing, malformed, unknown, or revoked API key |
402 | Company has insufficient AI credits (minCredits / totalBalance included) |
403 | Conversation exists but belongs to a different company |
404 | Unknown conversation/agent, or the session is not an API-channel session |
409 | Conversation ended or mismatched; pending job limit reached; trigger cannot be cancelled or replied from its current status; asHumanReply with zero or multiple waiting jobs |
503 | Authenticated custom tools cannot be stored or resolved because credentials encryption is not configured on the server |
500 | Unexpected processing failure |
504 | Agent 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_sessionare never offered on this channel even if configured on the agent. - Agent handoff: when
handoff_to_agentis enabled on the entry agent and itshandoffRosterlists 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'sagentIdis updated. - Contact memory: when
contactIdis set on the session (create, resume, late-bind while active, or agentidentify_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
apireference 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.
