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 clientnew 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)
})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")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 messageimport { 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.
| Field | Type | Description |
|---|---|---|
id | str | Message id (reply/typing target). |
conversation_id | str | Stable thread id; key your agent's memory on this. |
connection_id | str | The connection (channel) it arrived on. |
customer_id | str | Customer scope, if set. |
agent_id | str | Agent scope, if set. |
channel | str | Source channel, e.g. "email", "slack". |
sender | dict | None | Who sent it (address/handle/user id, channel-specific). |
subject | str | None | Subject line (email; usually None elsewhere). |
text | str | None | Plain-text body. |
html | str | None | HTML 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.
| Channel | Method | Cost |
|---|---|---|
connect_email | free | |
| Telegram | connect_telegram | free |
| Discord | install_discord / connect_discord | free |
| Slack | install_slack / connect_slack | free |
| SMS / voice | connect_phone | free |
| RCS | connect_rcs | free |
connect_instagram | free | |
connect_facebook | free | |
connect_whatsapp / start_whatsapp_onboarding | paid | |
| X (Twitter) | install_x / connect_x | paid |
| iMessage | connect_imessage | paid |
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.comconnectEmail(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.comTelegram
connect_telegram(
bot_token: str, # from @BotFather
customer_id: str | None = None,
agent_id: str | None = None,
**kwargs,
) -> dictconnectTelegram(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>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,
) -> dictinstallDiscord(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).
@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 replyclient.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 replyon_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>