How to connect a CallAIder assistant to an external channel via API

External Conversation Bridge is a way to connect a CallAIder text assistant to your own communication channel via API. The channel can be a Telegram bot, WhatsApp, Instagram, Viber, a CRM chat, a mobile app chat, your own operator interface, or any other system that can receive user messages and make HTTP requests from the backend.

The core idea is simple: CallAIder does not take your messenger tokens and does not control message delivery in your channel. Your application receives a user message, sends the text and context to CallAIder, receives the assistant response, and delivers it back to the user on its own.

This guide is written for two audiences:

  • platform clients who need to understand what must be configured in the CallAIder account;
  • client integration developers who will implement the bridge server, payload, retry, handoff, and response delivery into the external channel.

When to use External Conversation Bridge

Use the external API channel when you already have your own customer contact point and want to connect an AI assistant to it without moving the channel inside CallAIder.

Typical scenarios:

  • your Telegram bot is already running in production and has its own commands, menus, group rules, and forum topics;
  • your CRM already receives messages from the website, messengers, or support forms;
  • operators work in your own interface, and AI should respond only until handoff;
  • you need to connect an assistant to a channel that does not have a ready-made native connector in CallAIder;
  • you do not want to pass Telegram, WhatsApp, Instagram, Viber, or other external platform tokens to CallAIder.

In this mode, CallAIder works as a text assistant for your channel: it receives inbound text, maintains the conversation context, processes attachments when needed, generates a text response, and returns it to your application.

What stays on your side

External Conversation Bridge is not a ready-made adapter for a specific messenger. Keep this in mind before development starts.

The client application remains responsible for:

  • receiving messages from the external channel;
  • storing Telegram, WhatsApp, CRM, or other system tokens;
  • webhooks, polling, group topics, routing, and channel logic;
  • converting an inbound message into a CallAIder payload;
  • sending CallAIder responses back to the user;
  • operator handoff;
  • retry, timeouts, queues, and rate limiting;
  • filtering messages that AI should not answer.

CallAIder is responsible for:

  • API key validation;
  • finding the assistant by assistantId;
  • checking that the external API channel is enabled;
  • creating or restoring the assistant conversation;
  • storing message history for context;
  • maintaining context by externalConversationId;
  • generating the assistant response;
  • blocking the response if the conversation is set to paused or blocked;
  • automatically starting a new request after closed when the user writes again in the same conversation;
  • basic protection against reprocessing the latest messageId.

Overall flow

In a production scenario, your backend or bridge server usually sits between your channel and CallAIder. It stores secrets and makes server-to-server requests.

User in an external channel
        |
        v
Your backend / bridge server
        |
        | POST /v1/assistants/:assistantId/external-conversations/messages
        | Authorization: Bearer cld_...
        v
CallAIder public API
        |
        v
CallAIder assistant
        |
        v
JSON response: messages[]
        |
        v
Your backend delivers messages[] to Telegram, CRM, website, or another channel

Do not call this API directly from the end user’s browser or a mobile application if the key can be extracted from client-side code. The API key must live only on the server side.

What to prepare before integration

Before writing code, make sure you have:

  1. An active assistant in CallAIder.
  2. Access to that assistant’s settings.
  3. The external API channel enabled for the assistant.
  4. The assistantId, shown in the account as API bot identifier.
  5. A company API key in the cld_... format.
  6. A backend service that will receive messages from your channel and call the CallAIder API.
  7. The public API base URL for your connection, for example https://api.callaider.ai/v1.

If API access is not enabled in your tariff or agreement, contact your CallAIder manager or support team. A key may exist in the account, but API requests still need to pass the availability check for the corresponding company feature.

Step 1. Open the assistant channels tab

In your CallAIder account, open the assistant you want to edit. Then go to the Channels tab.

Path in the account:

Assistants -> Edit -> Channels

Channels tab in assistant settings

This page may contain different channels, for example Telegram and the external API channel. For an External Conversation Bridge integration, you need the External API channel block.

Step 2. Enable the external API channel

In the External API channel block, enable the Enable external API channel toggle and save the settings.

External API channel settings

After saving, check two things:

  • the block status is active;
  • the API bot identifier field shows the assistant ID.

This ID is used in the API URL as assistantId.

Example:

POST /v1/assistants/17/external-conversations/messages

In this example, 17 is the value from the API bot identifier field.

The public API base URL is not configured in this block. Take it from the documentation or from the CallAIder team for your connection. The examples below use the placeholder URL https://api.example.com/v1; replace it with the actual URL in a real integration.

Step 3. Create an API key for API access

The API key is required so CallAIder can verify that the request comes from your company. It is not a Telegram token, not a widget preview token, and not a user JWT. It is a separate company-level server-to-server key.

In the account, open the Integrations section and find the API keys block. Click Create key.

Creating an API key in the Integrations section

Recommended flow:

  1. Open Integrations.
  2. In the API keys block, click Create key.
  3. Enter a clear name, for example external-bridge-production or crm-chat-staging.
  4. Copy the key in the cld_... format.
  5. Store the key in your backend service secrets.
  6. Do not add the key to frontend code, a mobile app, open logs, or a public repository.

Usually, after creation, the key is shown masked in the list. Copy the full value immediately during creation and pass it to the developer through a secure channel.

Example environment variables on your backend:

CALLAIDER_API_BASE_URL="https://api.callaider.ai/v1"
CALLAIDER_ASSISTANT_ID="17"
CALLAIDER_API_KEY="cld_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Every HTTP request passes the key in the Authorization header:

Authorization: Bearer cld_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

The public API OpenAPI specification is available in the NBM-Labs/callaider_openapi repository. It is useful for checking contracts, generating clients, and keeping backend development aligned with the current endpoints.

Message endpoint

The main endpoint receives the user’s inbound message and synchronously returns the assistant response.

POST /v1/assistants/:assistantId/external-conversations/messages
Authorization: Bearer <API_KEY>
Content-Type: application/json

The full URL consists of the base URL, assistant ID, and endpoint path:

https://api.example.com/v1/assistants/17/external-conversations/messages

The endpoint works synchronously:

  1. receives the message;
  2. finds or creates a conversation by your externalConversationId;
  3. restores the context of this conversation;
  4. runs the assistant;
  5. waits for a response;
  6. returns messages[].

Because of this, your HTTP client must have a sufficient timeout. For production integrations, plan for at least 30-60 seconds.

Minimal payload

The smallest valid request contains a stable conversation ID, the user ID in your channel, the message ID, and the text.

{
  "externalConversationId": "tg:123456",
  "externalUserId": "123456",
  "messageId": "789",
  "text": "Hello"
}

Example curl:

curl -X POST \
  "https://api.example.com/v1/assistants/17/external-conversations/messages" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <API_KEY>" \
  -d '{
    "externalConversationId": "tg:123456",
    "externalUserId": "123456",
    "messageId": "789",
    "text": "Hello"
  }'

Payload fields

FieldTypeRequiredPurpose
externalConversationIdstringYesStable conversation ID on your side. CallAIder restores context by this value.
externalUserIdstringNoUser ID in your external channel. Useful for audit and search.
messageIdstringNo, but recommendedInbound message ID. Used to protect against repeated processing during retry.
textstringYes, if there are no attachmentsText that will be passed to the assistant.
attachmentsarrayNoUp to 5 attachments of the remote_url type.
metadataobjectNoAdditional JSON context for audit or integration.

In practice, you should almost always pass messageId, even though the field is not strictly required. Without it, it is harder for your integration to safely repeat a request after a timeout or network error.

How to choose externalConversationId

externalConversationId is the conversation key. If it stays the same, CallAIder treats messages as part of the same context. If it changes, a different conversation is created.

Choose the ID depending on how assistant memory should work in your channel.

ScenarioExample externalConversationIdBehavior
One conversation per Telegram usertg:user:123456The assistant remembers the context of a specific user.
One conversation per Telegram chattg:chat:-1001234567890Context is shared for the chat.
Separate conversation per forum topictg:-1001234567890:topic:42Each topic has its own context.
One conversation per CRM ticketcrm:ticket:ABC-1001Context is tied to a CRM request.
One conversation per website chatsite:conversation:9f3aContext is tied to a website request or chat.

Different assistants can use the same externalConversationId without conflict, because context is tied to the combination:

assistantId + externalConversationId

Do not accidentally change externalConversationId between messages in the same conversation. If every message has a new ID, the assistant will not be able to restore the previous conversation context.

How to use messageId and retry

messageId must be a stable ID of the inbound message in your channel. If you repeat the same request because of a timeout, network error, or 5xx, use the same messageId.

Correct behavior:

  • the user sends message 789;
  • your bridge server sends it to CallAIder with messageId: "789";
  • the request hangs or the response does not reach your server;
  • the bridge server repeats the request with the same messageId: "789";
  • if CallAIder has already processed this message, the response contains duplicate: true and an empty messages[].

Do not generate a new messageId for a retry of the same message. Otherwise, CallAIder will treat the retry as a new user message.

Duplicate protection is designed for a typical retry of the latest message in a specific conversation. For high-load integrations, you should still have your own idempotency or queue on the client bridge server side.

Full payload with metadata and attachment

When you need to pass additional context or a file, use metadata and attachments.

{
  "externalConversationId": "crm:ticket:ABC-1001",
  "externalUserId": "customer:555",
  "messageId": "crm-msg-9002",
  "text": "Please check this invoice",
  "attachments": [
    {
      "type": "remote_url",
      "url": "https://client.example.com/downloads/invoice-1001.pdf",
      "mimeType": "application/pdf",
      "filename": "invoice-1001.pdf",
      "sizeBytes": 245760,
      "sha256": "f2c7d0b4f4e8f1b7a12a4d34a9f6b7c0d1e2f3a4567890abcdef1234567890ab"
    }
  ],
  "metadata": {
    "source": "crm",
    "ticketId": "ABC-1001",
    "customerPlan": "business",
    "operatorQueue": "support-l2"
  }
}

metadata does not automatically control channel logic, but it helps with audit, tracing, and future integration logic. Pass only the technical and business context that is truly needed for your scenario.

Attachments via remote_url

External Conversation Bridge supports inbound attachments in the remote_url format. This means your application does not send the file itself inside JSON. Instead, it passes a short-lived HTTPS URL that the CallAIder backend can use to download the file while processing the message.

Format of one attachment:

{
  "type": "remote_url",
  "url": "https://client.example.com/files/photo.jpg",
  "mimeType": "image/jpeg",
  "filename": "photo.jpg",
  "sizeBytes": 734003,
  "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}

Attachment fields:

FieldTypeRequiredDescription
typestringYesCurrently only remote_url is supported.
urlstringYesPublic HTTPS URL available to the CallAIder backend during processing.
mimeTypestringNo, but recommendedExpected MIME type. Especially useful for signed URLs that return application/octet-stream.
filenamestringNoFile name that will be shown in the conversation history and during processing.
sizeBytesnumberNoExpected file size. If it is larger than the limit, the request is rejected before download.
sha256stringNoSHA-256 checksum. If passed, CallAIder verifies the file after download.

Supported file types:

  • images: image/png, image/jpeg, image/webp, image/heic, image/heif, image/gif;
  • video: video/mp4;
  • audio: audio/wav, audio/x-wav, audio/wave, audio/mp3, audio/mpeg, audio/aiff, audio/x-aiff, audio/aac, audio/ogg, audio/flac;
  • documents: application/pdf.

Current limits:

  • up to 5 attachments in one message;
  • up to 20 MB per file;
  • https only;
  • no username/password in the URL;
  • hostname must resolve to a public IP address;
  • private, localhost, link-local, multicast, and service IP ranges are blocked;
  • redirects are limited and checked again;
  • Content-Length and actual size are checked;
  • after download, the file is prepared for assistant analysis without another request to your URL.

Do not pass long-lived URLs with private secrets in the query string. It is better to use a short-lived signed URL or your own one-time download endpoint that lives long enough to process one message.

Successful message response

If the message is processed, the API returns ok: true, the current conversation status, and the messages array.

{
  "ok": true,
  "status": "active",
  "messages": [
    {
      "text": "Hello! How can I help?"
    }
  ]
}

messages is an array of text responses that your application must deliver into its channel in the same order. In most scenarios there will be one item, but the contract uses an array from the start so future scenarios with multiple responses do not require a format change.

CallAIder does not deliver these messages to Telegram, CRM, or a messenger on its own. Delivery is always on your side.

Response to a duplicate message

If messageId matches the latest processed inbound message in this conversation, generation is not run again.

{
  "ok": true,
  "status": "active",
  "duplicate": true,
  "messages": []
}

For your application, this means: do not deliver anything to the user. This response is normal for retry if the previous request has already been processed.

Response when AI is paused

If the conversation has the paused or blocked status, the assistant does not answer new messages.

{
  "ok": true,
  "status": "paused",
  "skipped": true,
  "messages": []
}

This is useful for operator handoff. For example, an operator takes over the conversation, your application moves the conversation to paused, and all subsequent user messages can still enter the system, but AI does not interfere.

Closed conversation and a new request

The closed status means the current request is finished. When you move a conversation to closed, CallAIder ends the current request context.

If the user later writes to the same externalConversationId, you do not need to create a new conversation manually. Just send a regular POST /messages with a new messageId. CallAIder automatically starts a new request and returns the conversation to active.

{
  "ok": true,
  "status": "active",
  "reopened": true,
  "messages": [
    {
      "text": "Hello! How can I help?"
    }
  ]
}

This lets you use the same externalConversationId for a long-lived channel, for example a Telegram chat, while separating individual requests with the closed status.

Endpoint for changing conversation state

The second endpoint controls the conversation state. It is used for handoff, returning the conversation to the assistant, and closing a request.

POST /v1/assistants/:assistantId/external-conversations/:externalConversationId/state
Authorization: Bearer <API_KEY>
Content-Type: application/json

Example URL:

https://api.example.com/v1/assistants/17/external-conversations/tg:123456/state

If externalConversationId contains characters that have special meaning in URLs, encode it with URL encoding.

Supported statuses

Input valueStored statusBehavior
activeactiveThe assistant can respond.
ai_activeactiveAlias for active.
pausedpausedThe assistant does not respond.
handoffpausedAlias for paused, convenient for operator scenarios.
human_handoffpausedAlias for paused.
closedclosedThe current request is closed. The next inbound message opens a new request.
blockedblockedThe assistant does not respond.

Pause a conversation

When an operator takes the conversation, move it to paused or handoff.

curl -X POST \
  "https://api.example.com/v1/assistants/17/external-conversations/tg:123456/state" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <API_KEY>" \
  -d '{
    "status": "handoff",
    "metadata": {
      "reason": "operator_joined",
      "operatorId": "op-17"
    }
  }'

Expected response:

{
  "ok": true,
  "status": "paused"
}

After that, new POST /messages calls for this conversation will return skipped: true and an empty messages[].

Return the conversation to the assistant

If the operator has finished their part but the request is not closed yet, return the conversation to active.

curl -X POST \
  "https://api.example.com/v1/assistants/17/external-conversations/tg:123456/state" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <API_KEY>" \
  -d '{
    "status": "active",
    "metadata": {
      "reason": "operator_left"
    }
  }'

The next user message will again be processed by the assistant within the current context.

Close a request

When the customer’s issue is resolved, move the conversation to closed.

curl -X POST \
  "https://api.example.com/v1/assistants/17/external-conversations/tg:123456/state" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <API_KEY>" \
  -d '{
    "status": "closed",
    "metadata": {
      "reason": "operator_closed_issue"
    }
  }'

The next message with the same externalConversationId automatically starts a new request. This is convenient for CRM tickets, messenger support, and repeat customer requests in the same chat.

Example client integration in TypeScript

Below is a simplified backend code example. It only shows interaction with CallAIder. The sendTextToExternalChannel function must be implemented in your system for a specific channel.

type BridgeMessage = {
  externalConversationId: string;
  externalUserId?: string;
  messageId?: string;
  text: string;
  metadata?: Record<string, unknown>;
};

type CallaiderBridgeResponse = {
  ok: boolean;
  status: string;
  duplicate?: boolean;
  skipped?: boolean;
  reopened?: boolean;
  messages: Array<{ text: string }>;
};

const baseUrl = process.env.CALLAIDER_API_BASE_URL;
const assistantId = process.env.CALLAIDER_ASSISTANT_ID;
const apiKey = process.env.CALLAIDER_API_KEY;

async function askCallaider(message: BridgeMessage): Promise<CallaiderBridgeResponse> {
  const response = await fetch(
    `${baseUrl}/assistants/${assistantId}/external-conversations/messages`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${apiKey}`,
      },
      body: JSON.stringify(message),
    },
  );

  if (!response.ok) {
    const text = await response.text();
    throw new Error(`CallAIder bridge failed: ${response.status} ${text}`);
  }

  return response.json() as Promise<CallaiderBridgeResponse>;
}

async function handleIncomingText(input: {
  chatId: string;
  userId: string;
  messageId: string;
  text: string;
}) {
  const result = await askCallaider({
    externalConversationId: `tg:chat:${input.chatId}`,
    externalUserId: input.userId,
    messageId: input.messageId,
    text: input.text,
    metadata: {
      source: 'telegram',
      chatId: input.chatId,
    },
  });

  if (result.duplicate || result.skipped) {
    return;
  }

  for (const message of result.messages) {
    const text = message.text.trim();
    if (text) {
      await sendTextToExternalChannel(input.chatId, text);
    }
  }
}

For production code, add:

  • HTTP timeout;
  • retry only with the same messageId;
  • logging of request id or your own correlation id;
  • a queue or lock for one externalConversationId if your channel expects a sequential conversation;
  • handling delivery errors when sending the response to the external channel.

Example for Telegram forum topic

If you use a Telegram group with forum topics, build externalConversationId so different topics do not mix context.

curl -X POST \
  "https://api.example.com/v1/assistants/17/external-conversations/messages" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <API_KEY>" \
  -d '{
    "externalConversationId": "tg:-1001234567890:topic:42",
    "externalUserId": "123456",
    "messageId": "789",
    "text": "I need help with payment",
    "metadata": {
      "chatId": "-1001234567890",
      "messageThreadId": 42,
      "username": "client_user"
    }
  }'

With this scheme, topic 42 has a separate context from topic 43, even inside the same Telegram group.

Example for CRM chat

For CRM scenarios, it is often most convenient to bind the conversation to a ticket or request.

curl -X POST \
  "https://api.example.com/v1/assistants/17/external-conversations/messages" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <API_KEY>" \
  -d '{
    "externalConversationId": "crm:ticket:ABC-1001",
    "externalUserId": "customer:555",
    "messageId": "crm-msg-9001",
    "text": "I want to change the appointment date",
    "metadata": {
      "crm": "custom",
      "ticketId": "ABC-1001",
      "priority": "normal"
    }
  }'

When the operator closes the ticket, call /state with the closed status. If the customer creates a new ticket, use a new externalConversationId. If the customer writes in the same long-lived chat but you want to start a new request, you can also close the old conversation and let CallAIder start a new context on the next message.

Typical handoff scenario

Operator handoff usually looks like this:

  1. The user writes in Telegram, WhatsApp, CRM, or website chat.
  2. Your backend sends the message to POST /messages.
  3. CallAIder returns messages[].
  4. Your backend delivers the response to the user.
  5. An operator decides to take over the conversation.
  6. Your backend calls /state with the handoff or paused status.
  7. New user messages can continue to enter CallAIder, but the API returns skipped: true.
  8. If the operator returns the conversation to AI, the backend calls /state with the active status.
  9. If the operator closes the issue, the backend calls /state with the closed status.
  10. When the user writes again, a regular POST /messages automatically starts a new request.

This approach prevents AI and human responses from mixing. The key point is that your system must be the source of truth for who is currently handling the conversation.

Timeouts, queues, and parallel messages

/messages returns a response only after the assistant generates it, so the integration must be ready for longer HTTP requests.

Recommendations:

  • set a timeout of 30-60 seconds or more if your scenario allows longer responses;
  • repeat the request only with the same messageId;
  • do not run many parallel requests for one externalConversationId if response order matters;
  • use a queue or lock at the conversation level;
  • log enough data to find externalConversationId, messageId, HTTP status, and processing time;
  • if the CallAIder response was received but delivery into the external channel failed, retry the delivery to the channel rather than generating a new response unnecessarily.

External Conversation Bridge returns the response synchronously in the HTTP response. A separate callback URL is not needed in this scenario.

Errors and diagnostics

Typical HTTP statuses:

HTTP statusWhat to check
400Payload is incomplete or invalid: missing externalConversationId, missing text when there are no attachments, or invalid status.
403The assistant or external API channel is disabled, or the company does not have access to the required API capability.
404No enabled External API channel was found for this assistant under the company of the API key.
500Unexpected server error. You can repeat the request with the same messageId.

If the request fails authorization, check:

  • whether the Authorization: Bearer <API_KEY> header is passed;
  • whether the API key is truncated;
  • whether the key is active in the account;
  • whether the key belongs to the same company as the assistant;
  • whether API access is enabled for your company.

If an error happens while the assistant is processing the message, the API may return a standard error text as a regular item in messages[]. In that case, your application can deliver it to the user like other assistant responses, or handle it according to your own support rules.

Security

For a secure integration, follow these rules:

  • use HTTPS only;
  • store the API key only on the backend side;
  • do not call External Conversation Bridge directly from the user’s browser;
  • do not add the API key to a mobile app, frontend bundle, or public repository;
  • do not log the API key in open logs;
  • do not pass Telegram, WhatsApp, CRM, or other channel tokens to CallAIder;
  • restrict access to your bridge server;
  • add rate limiting at the reverse proxy or backend level;
  • use short-lived URLs for attachments;
  • for production integrations, keep request audit logs without secrets.

The API key and the external channel token are different secrets. The API key lets your backend call CallAIder. A Telegram, WhatsApp, or CRM token lets your backend work with that channel. Do not mix them and do not pass channel tokens in the CallAIder payload.

Integration test checklist

Before launching to production, run the full test scenario:

  1. Enable External API channel in the Channels tab.
  2. Copy the API bot identifier.
  3. Create an API key in the Integrations section.
  4. Store CALLAIDER_API_BASE_URL, CALLAIDER_ASSISTANT_ID, and CALLAIDER_API_KEY in backend secrets.
  5. Send a test POST /messages with a unique externalConversationId.
  6. Make sure the response contains ok: true, status: active, and a non-empty messages[].
  7. Repeat the same request with the same messageId.
  8. Make sure the response contains duplicate: true and messages: [].
  9. Call /state with the paused or handoff status.
  10. Send a new message to the same externalConversationId.
  11. Make sure the response contains skipped: true and messages: [].
  12. Call /state with the active status.
  13. Send a new message and verify that the assistant responds again.
  14. Call /state with the closed status.
  15. Send a new message with the same externalConversationId and a new messageId.
  16. Make sure the response contains status: active, reopened: true, and new messages[].
  17. Check response delivery in your real channel.
  18. Check retry, timeout, and logging on your bridge server side.

Short summary

External Conversation Bridge is suitable for integrations where the client system owns the channel, tokens, operator logic, and message delivery. In this scheme, CallAIder handles the AI part: conversation context, message processing, and response generation.

To launch it, you need three basic things: the external API channel enabled in the assistant, the assistantId from the account, and the company API key. After that, your backend sends messages to /messages, delivers messages[] into its channel, and manages conversation state through /state when needed.