SDK reference

Python SDK

Complete reference for caspian-sdk: every public method on CommClient and Message, with real signatures. One client, one on_message handler, every channel. Install with pip install caspian-sdk (Python 3.12+).

Method names are shown Python-first. The JavaScript SDK mirrors every one in camelCase; see the JavaScript SDK reference, or the JS tab under each example here. Both read COMM_API_KEY and COMM_BASE_URL from the environment or a ./.env file next to your code.

Every method that talks to the gateway raises CommError on a non-2xx response. Provisioning helpers (connect_*) additionally raise on timeout (408) or a failed provision (502).

Client setup

Construct one CommClient and reuse it. With nothing passed it reads its key and base URL from the environment or ./.env; a missing key raises CommError(401) immediately.

CommClient(
    api_key: str | None = None,      # else COMM_API_KEY (env or ./.env)
    base_url: str | None = None,     # else COMM_BASE_URL, default http://127.0.0.1:8000
    http: httpx.Client | None = None,
    timeout: float = 30.0,           # per-request timeout, seconds
)

client.close() -> None              # close the underlying HTTP client
new CommClient({
  apiKey?: string,        // else COMM_API_KEY (env or ./.env)
  baseUrl?: string,       // else COMM_BASE_URL, default http://127.0.0.1:8000
  timeout?: number,       // per-request timeout, seconds (default 30)
  fetch?: typeof fetch,   // custom fetch (Node < 18)
})
agent.py
from caspian_sdk import CommClient

client = CommClient()                      # reads COMM_API_KEY / COMM_BASE_URL
# or explicitly:
client = CommClient(api_key="comm_...", base_url="https://api.trycaspianai.com")
agent.ts
import { CommClient } from "caspian-sdk";

const client = new CommClient();           // reads COMM_API_KEY / COMM_BASE_URL
// or explicitly:
const client = new CommClient({ apiKey: "comm_...", baseUrl: "https://api.trycaspianai.com" });

CommError

The single exception type raised by every gateway call. Carries the HTTP status and the gateway's error detail.

from caspian_sdk import CommError

try:
    client.connect_telegram(bot_token="bad")
except CommError as e:
    print(e.status_code)   # int, e.g. 400
    print(e.detail)        # str, gateway's message
import { CommError } from "caspian-sdk";

try {
  await client.connectTelegram({ botToken: "bad" });
} catch (e) {
  if (e instanceof CommError) {
    console.log(e.statusCode);  // number, e.g. 400
    console.log(e.detail);      // string
  }
}

The Message object

Your handler receives a Message, the same shape on every channel. Fields are read-only; reply() and typing() act back on the source channel.

FieldTypeDescription
idstrMessage id (reply/typing target).
conversation_idstrStable thread id; key your agent's memory on this.
connection_idstrThe connection (channel) it arrived on.
customer_idstrCustomer scope, if set.
agent_idstrAgent scope, if set.
channelstrSource channel, e.g. "email", "slack".
senderdict | NoneWho sent it (address/handle/user id, channel-specific).
subjectstr | NoneSubject line (email; usually None elsewhere).
textstr | NonePlain-text body.
htmlstr | NoneHTML body when the channel provides one.

In JavaScript the fields are camelCase (conversationId, connectionId, customerId, agentId) and all are readonly properties on the Message class.

message.reply(text: str | None = None, html: str | None = None) -> dict
# Reply on whichever channel this message arrived from (auto-threaded).

message.typing() -> None
# Show a 'thinking…' indicator on the channel (Discord/Telegram; no-op
# elsewhere). Fired automatically before your handler runs; call again
# during long work to keep it alive.
message.reply(text?: string | null, html?: string | null): Promise<Record<string, unknown>>
// Reply on whichever channel this message arrived from (auto-threaded).

message.typing(): Promise<Record<string, unknown>>
// Show a "thinking…" indicator on the channel (Discord/Telegram; no-op
// elsewhere). Fired automatically before your handler runs.

Connecting channels

Each connect_*() call returns a connection dict. Direct connects (email, Telegram, phone, WhatsApp) block until the connection is active, then return it. OAuth-style installs (install_*, connect_slack, connect_instagram, connect_facebook) return immediately with an authorize_url to open. All accept optional customer_id and agent_id to scope the connection, plus display_name / capabilities keyword args.

ChannelMethodCost
Emailconnect_emailfree
Telegramconnect_telegramfree
Discordinstall_discord / connect_discordfree
Slackinstall_slack / connect_slackfree
SMS / voiceconnect_phonefree
RCSconnect_rcsfree
Instagramconnect_instagramfree
Facebookconnect_facebookfree
WhatsAppconnect_whatsapp / start_whatsapp_onboarding
X (Twitter)install_x / connect_x
iMessageconnect_imessage

Email

connect_email(
    customer_id: str | None = None,
    agent_id: str | None = None,
    domain: str | None = None,     # verified custom domain
    username: str | None = None,   # exact local part (custom domains only)
    **kwargs,
) -> dict
# Connect an email inbox. Returns a connection with an "address".

inbox = client.connect_email()
print(inbox["address"])            # e.g. agent-xxxx@trycaspianai.com
connectEmail(opts?: {
  customerId?: string, agentId?: string,
  domain?: string, username?: string,
}): Promise<Connection>

const inbox = await client.connectEmail();
console.log(inbox.address);        // e.g. agent-xxxx@trycaspianai.com

Telegram

connect_telegram(
    bot_token: str,                # from @BotFather
    customer_id: str | None = None,
    agent_id: str | None = None,
    **kwargs,
) -> dict
connectTelegram(opts: {
  botToken: string,                // from @BotFather
  customerId?: string, agentId?: string,
}): Promise<Connection>

Phone (SMS / voice)

connect_phone(
    customer_id: str | None = None,
    agent_id: str | None = None,
    provider=None,                 # telnyx / twilio / modem / agentphone
    **kwargs,                      # provider credentials for BYO numbers
) -> dict
# Omit provider for the deployment default.
connectPhone(opts?: {
  customerId?: string, agentId?: string,
  provider?: string,               // telnyx / twilio / modem / agentphone
}): Promise<Connection>

WhatsApp

connect_whatsapp attaches a WhatsApp number directly. start_whatsapp_onboarding begins Meta Embedded Signup and returns a launcher_url the account owner clicks once (no tokens to copy).

connect_whatsapp(
    customer_id=None, agent_id=None,
    provider=None,                 # "twilio-whatsapp" or "meta-whatsapp"
    **kwargs,
) -> dict

start_whatsapp_onboarding(
    customer_id=None, agent_id=None,
    display_name=None, capabilities=None,
) -> dict
# Returns {"session", "launcher_url", "expires_in"}. Hand launcher_url
# to the WhatsApp Business account owner; poll get_connection() until active.
connectWhatsapp(opts?: {
  customerId?: string, agentId?: string,
  provider?: string,               // "twilio-whatsapp" or "meta-whatsapp"
}): Promise<Connection>

startWhatsappOnboarding(opts?: {
  customerId?: string, agentId?: string,
  displayName?: string, capabilities?: string[],
}): Promise<WhatsappOnboarding>
// Returns { session, launcher_url, expires_in }.

iMessage & RCS

connect_imessage(customer_id=None, agent_id=None, **kwargs) -> dict
# iMessage line (provider: agentphone-imessage).

connect_rcs(customer_id=None, agent_id=None, **kwargs) -> dict
# RCS Business Messaging sender (provider: twilio-rcs).
connectImessage(opts?: { customerId?: string, agentId?: string }): Promise<Connection>
// iMessage line (provider: agentphone-imessage).

connectRcs(opts?: { customerId?: string, agentId?: string }): Promise<Connection>
// RCS Business Messaging sender (provider: twilio-rcs).

Discord

install_discord is one-click on the shared bot (returns an authorize_url). connect_discord brings your own bot token, or a channel webhook for a per-agent identity.

install_discord(
    customer_id=None, agent_id=None,
    display_name=None,             # bot's name in that server
    **kwargs,
) -> dict
# Shared bot, no token. Returns a connection with an authorize_url.

connect_discord(
    bot_token: str | None = None,  # from discord.com/developers
    webhook_url: str | None = None,# OR a channel webhook (no bot)
    username: str | None = None,
    avatar_url: str | None = None,
    customer_id=None, agent_id=None, **kwargs,
) -> dict
installDiscord(opts?: {
  customerId?: string, agentId?: string, displayName?: string,
}): Promise<Connection>

connectDiscord(opts?: {
  botToken?: string, webhookUrl?: string,
  username?: string, avatarUrl?: string,
  customerId?: string, agentId?: string,
}): Promise<Connection>

Slack

install_slack is one-click on the shared app. connect_slack uses your own Slack app. Both return an authorize_url for the workspace owner to approve.

install_slack(
    customer_id=None, agent_id=None,
    display_name=None, icon_url=None,  # post under your own name + icon
    **kwargs,
) -> dict
# Shared app ("Add to Slack"). Returns a connection with an authorize_url.

connect_slack(
    slack_client_id: str | None = None,
    slack_client_secret: str | None = None,
    slack_signing_secret: str | None = None,
    customer_id=None, agent_id=None, **kwargs,
) -> dict
# Bring your own app (api.slack.com/apps). Returns an authorize_url.
installSlack(opts?: {
  customerId?: string, agentId?: string,
  displayName?: string, iconUrl?: string,
}): Promise<Connection>

connectSlack(opts?: {
  slackClientId?: string,
  slackClientSecret?: string,
  slackSigningSecret?: string,
  customerId?: string, agentId?: string,
}): Promise<Connection>

X (Twitter)

install_x is one-click ("Sign in with X"). connect_x brings your own account's OAuth tokens. Reactive DM bot only. It never cold-DMs.

install_x(customer_id=None, agent_id=None, **kwargs) -> dict
# Returns a connection with an authorize_url ("Sign in with X").

connect_x(
    access_token: str,
    user_id: str,                  # numeric id (before the dash in the token)
    access_secret: str | None = None,
    username: str | None = None,
    customer_id=None, agent_id=None, **kwargs,
) -> dict
# The account must be labelled "Automated" in X settings.
installX(opts?: { customerId?: string, agentId?: string }): Promise<Connection>

connectX(opts: {
  accessToken: string,
  userId: string,
  accessSecret?: string,
  username?: string,
  customerId?: string, agentId?: string,
}): Promise<Connection>

Instagram & Facebook

connect_instagram(customer_id=None, agent_id=None, **kwargs) -> dict
# Instagram DM install (OAuth). Returns an authorize_url.

connect_facebook(customer_id=None, agent_id=None, **kwargs) -> dict
# Facebook Messenger install (OAuth). Returns an authorize_url.
connectInstagram(opts?: { customerId?: string, agentId?: string }): Promise<Connection>
// Instagram DM install (OAuth). Returns an authorize_url.

connectFacebook(opts?: { customerId?: string, agentId?: string }): Promise<Connection>
// Facebook Messenger install (OAuth). Returns an authorize_url.

Branding

update_branding(
    connection_id: str,
    display_name=None, icon_url=None,
) -> dict
# Change the name/icon the agent posts under, after connecting. No re-install.
updateBranding(connectionId: string, opts?: {
  displayName?: string, iconUrl?: string,
}): Promise<Connection>

Per-channel setup (tokens, OAuth flow, what each provider needs) lives in the Channels pages. This reference just lists the calls.

Receiving & replying

Register one handler with on_message; it fires for every connection. listen() runs the poll loop forever and is resilient: a handler that raises is logged and skipped, and a failed poll retries with exponential backoff. Pass ack to send an instant acknowledgement (e.g. "On it, one moment…") the moment a message arrives, before your handler runs, so the human sees the agent is working while it thinks; the real answer follows. Handy on channels with no typing indicator (X, SMS, email).

agent.py
@client.on_message                          # register a handler (repeatable)
def handle(message):
    message.reply(f"You said: {message.text}")

client.listen(ack="On it, one moment…")     # instant ack, then your real reply
agent.ts
client.onMessage(async (message) => {      // register a handler (repeatable)
  await message.reply(`You said: ${message.text}`);
});

await client.listen({ ack: "On it, one moment…" });  // instant ack, then your real reply
on_message(handler) -> handler
# Register a message handler. The same handler answers every channel.

reply(message_id: str, text: str | None = None, html: str | None = None) -> dict
# Reply to a message by id (Message.reply wraps this).

typing(message_id: str) -> dict
# Show a 'thinking…' indicator on that message's channel. Best-effort.

listen(from_seq=None, poll_interval=1.0, max_backoff=30.0, ack=None) -> None
# Poll forever, dispatching inbound messages. Only SIGINT stops it.
# ack: instant acknowledgement reply sent before your handler runs
#   (e.g. "On it, one moment…"); the real answer follows. Best on
#   channels with no typing indicator (X, SMS, email).

dispatch_pending(after_seq: int = 0) -> int
# Drain all currently available events once; returns the last seen seq.

events(after_seq=0, limit=100, type: str | None = None) -> list[dict]
# Raw event feed. Cursor-paged by seq; filter by type.
onMessage(handler): handler
// Register a message handler. The same handler answers every channel.

reply(messageId, text?, html?): Promise<Record<string, unknown>>
typing(messageId): Promise<Record<string, unknown>>

listen(opts?: {
  fromSeq?: number, pollInterval?: number,
  maxBackoff?: number, signal?: AbortSignal,
  ack?: string,   // instant "On it…" reply before your handler runs
}): Promise<void>

dispatchPending(afterSeq = 0): Promise<number>

events(opts?: { afterSeq?: number, limit?: number, type?: string }): Promise<EventRecord[]>

Prefer push over polling? Register a webhook with set_webhook(url, secret) and receive events by HTTP. dispatch_pending() is handy inside a webhook handler or a serverless function where you don't want a long-running loop.

Proactive

Reply is reactive; these three send without an inbound message. Each needs the matching capability on the connection (SEND, INITIATE, BACKFILL).

send_message(conversation_id: str, text=None, html=None) -> dict
# Send into an existing conversation (needs Capability.SEND).

initiate(connection_id: str, recipient: str, text: str) -> dict
# Cold-start a new conversation (needs Capability.INITIATE, user account).

backfill(conversation_id: str, limit: int = 50) -> dict
# Pull history from before the connection (needs Capability.BACKFILL).
sendMessage(conversationId, text?, html?): Promise<Record<string, unknown>>
// Send into an existing conversation (needs Capability.SEND).

initiate(connectionId, recipient, text): Promise<Record<string, unknown>>
// Cold-start a new conversation (needs Capability.INITIATE).

backfill(conversationId, limit = 50): Promise<Record<string, unknown>>
// Pull history from before the connection (needs Capability.BACKFILL).

Behavior guides

Ready-to-inject system-prompt text telling your agent how to behave on each connected channel: Slack threading, WhatsApp's 24-hour window, SMS length, formatting. Append it to your agent's system prompt, or ignore it.

behavior_prompt() -> str
# Combined guide for every channel you've connected. Empty string if none.

channel_guide(channel: str) -> str
# Guide for a single channel, e.g. "slack", "discord".

system_prompt = "You are Acme's support agent.\n" + client.behavior_prompt()
behaviorPrompt(): Promise<string>
// Combined guide for every channel you've connected. Empty string if none.

channelGuide(channel: string): Promise<string>
// Guide for a single channel, e.g. "slack", "discord".

const systemPrompt = "You are Acme's support agent.\n" + (await client.behaviorPrompt());

See Behavior guides for the full walkthrough.

Resources

Scopes, lookups, webhooks, and email-domain management.

Scopes & lookups

create_customer(name: str) -> dict
# Create a customer scope. Pass its id as customer_id to connect_*().

create_agent(name: str) -> dict
# Create an agent scope. Pass its id as agent_id to connect_*().

get_connection(connection_id: str) -> dict
# Fetch one connection (poll this while an install goes active).

list_conversations(connection_id: str | None = None) -> list[dict]
# All conversations, or just one connection's.

list_messages(conversation_id: str) -> list[dict]
# Every message in a conversation.

channels() -> list[dict]
# Configured transports and their capabilities.
createCustomer(name): Promise<Customer>
createAgent(name): Promise<Agent>

getConnection(connectionId): Promise<Connection>
listConversations(connectionId?): Promise<Conversation[]>
listMessages(conversationId): Promise<Record<string, unknown>[]>

channels(): Promise<Record<string, unknown>[]>
// Configured transports and their capabilities.

Webhooks

set_webhook(url: str, secret: str | None = None) -> dict
# Receive events by push instead of (or alongside) polling.

get_webhook() -> dict
# The currently registered webhook.
setWebhook(url, secret?): Promise<Record<string, unknown>>
getWebhook(): Promise<Record<string, unknown>>

Email testing & domains

test_email(
    text="Hello from the comm test sender.",
    subject="Test email",
    connection_id: str | None = None,
) -> dict
# Inject a message into an email connection to watch your handler run.

add_domain(domain: str) -> dict
# Register a custom subdomain; returns DNS records to add at the registrar.

list_domains() -> list[dict]
get_domain(domain_id: str) -> dict
# Poll get_domain() until the domain is active, then use it in connect_email().
testEmail(opts?: {
  text?: string, subject?: string, connectionId?: string,
}): Promise<Record<string, unknown>>

addDomain(domain): Promise<Domain>
listDomains(): Promise<Domain[]>
getDomain(domainId): Promise<Domain>

Next steps

JavaScript SDK →
The same surface in camelCase, with types.
REST API →
The raw HTTP endpoints under the SDK.
Channels →
Per-channel setup for every connect call.
Behavior guides →
Make your agent reply the right way per platform.