JavaScript / TypeScript SDK
Complete reference for caspian-sdk on npm. One CommClient, one onMessage handler, and a connect* call per channel. The package ships full TypeScript types, so every method, option, and return shape is checked at build time. This mirrors the Python reference: same methods, camelCase names, options-object arguments.
Install
npm install caspian-sdkNeeds Node 18+ (the SDK uses the global fetch and AbortSignal.timeout). TypeScript types are bundled: no @types package to add. Import the class as an ES module:
import { CommClient, CommError } from "caspian-sdk";Constructor
Everything is optional. With no arguments the client reads COMM_API_KEY and COMM_BASE_URL from the environment or a ./.env file next to your code. It throws CommError(401) if no key is found.
from caspian_sdk import CommClient
client = CommClient() # env / .env
client = CommClient(api_key="comm_...", base_url="https://api.trycaspianai.com", timeout=30.0)import { CommClient } from "caspian-sdk";
const client = new CommClient(); // env / .env
const client = new CommClient({
apiKey: "comm_...", // falls back to COMM_API_KEY
baseUrl: "https://api.trycaspianai.com", // falls back to COMM_BASE_URL
timeout: 30, // per-request timeout, seconds (default 30)
// fetch: myFetch, // inject a custom fetch (testing)
});| Option | Type | Description |
|---|---|---|
apiKey | string | Bearer key. Falls back to COMM_API_KEY (env or ./.env). |
baseUrl | string | Gateway URL. Falls back to COMM_BASE_URL, then http://127.0.0.1:8000. |
timeout | number | Per-request timeout in seconds. Default 30. |
fetch | typeof fetch | Custom fetch implementation. Defaults to global fetch. |
A complete agent
The shape of nearly every program: construct, connect a channel, register a handler, listen.
from caspian_sdk import CommClient
client = CommClient()
inbox = client.connect_email()
print("Agent address:", inbox["address"])
@client.on_message
def handle(message):
message.reply(f"You said: {message.text}")
client.listen() # blocks foreverimport { CommClient } from "caspian-sdk";
const client = new CommClient();
const inbox = await client.connectEmail();
console.log("Agent address:", inbox.address);
client.onMessage(async (message) => {
await message.reply(`You said: ${message.text}`);
});
await client.listen(); // runs forever; pass a signal to stopHandlers can be sync or async; the SDK awaits whatever you return. Every method that hits the gateway returns a Promise, so await them.
Behavior guides
Opt-in text you can inject into your agent's system prompt so it follows each channel's etiquette. Both return a plain string.
| Method | Description |
|---|---|
behaviorPrompt(): Promise<string> | Combined system-prompt block covering every channel you've connected. Empty string if nothing is connected yet. |
channelGuide(channel): Promise<string> | The guide for a single channel, e.g. "slack" or "discord". |
system_prompt += "\n\n" + client.behavior_prompt()
slack_rules = client.channel_guide("slack")systemPrompt += "\n\n" + await client.behaviorPrompt();
const slackRules = await client.channelGuide("slack");Resources
Optional scoping objects. Most single-agent programs never need them; pass the ids they return as customerId / agentId on any connect* call to group connections.
| Method | Description |
|---|---|
createCustomer(name): Promise<Customer> | Create a customer (an end user or tenant) to scope connections under. |
createAgent(name): Promise<Agent> | Create an agent identity to attach connections to. |
Connect a channel
Every connect* and install* method returns a Connection. They all accept the shared ConnectOptions below, plus their own channel-specific fields. Provisioned channels (email, Telegram, phone, WhatsApp) resolve once active; OAuth channels resolve immediately with an authorize_url on the returned connection; hand that link to the user to finish the install.
Shared ConnectOptions
| Field | Type | Description |
|---|---|---|
customerId | string | Scope the connection to a customer. |
agentId | string | Attach the connection to an agent. |
displayName | string | Name the agent posts under, where the channel supports it. |
capabilities | string[] | Requested capabilities for this connection. |
wait | boolean | Wait for provisioning to finish. Default true; forced false for OAuth channels. |
timeout | number | Provisioning wait timeout in seconds. Default 60. |
pollInterval | number | Poll interval while provisioning, in seconds. Default 0.5. |
Connect methods
| Method | Channel-specific options | Description |
|---|---|---|
connectEmail(opts?) | domain?, username? | Email inbox. free Pass domain for a verified custom domain and username to pick the local part. |
connectTelegram(opts) | botToken (required) | Telegram bot. free Get a token from @BotFather. |
connectPhone(opts?) | provider? | SMS/voice phone line. free provider picks the backend (telnyx / twilio / modem / agentphone). |
connectWhatsapp(opts?) | provider? | WhatsApp number. paid provider is twilio-whatsapp or meta-whatsapp. |
startWhatsappOnboarding(opts?) | customerId?, agentId?, displayName?, capabilities? | Begin Meta Embedded Signup. Returns { session, launcher_url, expires_in }; hand launcher_url to the WhatsApp Business account owner. |
connectImessage(opts?) | none | iMessage line. paid Provider agentphone-imessage. |
connectRcs(opts?) | none | RCS Business Messaging sender. Provider twilio-rcs. |
connectDiscord(opts?) | botToken?, webhookUrl?, username?, avatarUrl? | Discord identity. free A bot (botToken) or a channel webhookUrl with a custom username/avatarUrl. |
installDiscord(opts?) | customerId?, agentId?, displayName? | One-click install of the shared Discord bot. Returns a connection with an authorize_url. |
connectSlack(opts?) | slackClientId?, slackClientSecret?, slackSigningSecret? | Slack install with your own Slack app. free Returns an authorize_url for the workspace owner. |
installSlack(opts?) | customerId?, agentId?, displayName?, iconUrl? | One-click install of the shared Slack app. Returns an authorize_url ("Add to Slack"); displayName/iconUrl post under your own brand. |
connectX(opts) | accessToken, userId (required), accessSecret?, username? | X (Twitter) reactive DM bot with your own OAuth tokens. paid |
installX(opts?) | customerId?, agentId? | One-click connect of an X account, no tokens. Returns an authorize_url ("Sign in with X"). |
connectInstagram(opts?) | none | Instagram DM install (OAuth). free Returns an authorize_url. |
connectFacebook(opts?) | none | Facebook Messenger install (OAuth). free Returns an authorize_url. |
updateBranding(connectionId, opts?) | displayName?, iconUrl? | Change the name/icon the agent posts under after connecting, no re-install. |
client.connect_email() # free, no args
client.connect_telegram(bot_token="123:abc") # from @BotFather
client.connect_phone(provider="twilio") # your own number
client.install_slack(display_name="Acme Bot") # returns authorize_url
x = client.connect_x(access_token="...", user_id="123", access_secret="...")
inst = client.install_x()
print("Sign in with X:", inst["authorize_url"])await client.connectEmail(); // free, no args
await client.connectTelegram({ botToken: "123:abc" }); // from @BotFather
await client.connectPhone({ provider: "twilio" }); // your own number
await client.installSlack({ displayName: "Acme Bot" }); // returns authorize_url
await client.connectX({ accessToken: "...", userId: "123", accessSecret: "..." });
const inst = await client.installX();
console.log("Sign in with X:", inst.authorize_url);OAuth channels (Slack, Discord install, X install, Instagram, Facebook) return immediately with authorize_url on the connection. Poll getConnection(id) until status === "active", or watch for a connection.active event.
Custom domains
Bring your own subdomain for email addresses. Register it, add the DNS records it returns, then poll until active.
| Method | Description |
|---|---|
addDomain(domain): Promise<Domain> | Register a subdomain (e.g. agents.example.com). Returns the DNS records to add. |
listDomains(): Promise<Domain[]> | List registered domains and their status. |
getDomain(domainId): Promise<Domain> | Fetch one domain; poll it until status is active. |
Conversations and messages
Read connection state and history, and reply to or send messages. Replies route back on the source channel automatically and stay in the right thread.
| Method | Description |
|---|---|
getConnection(connectionId): Promise<Connection> | Fetch a connection's current status and details. |
listConversations(connectionId?): Promise<Conversation[]> | List conversations, optionally filtered to one connection. |
listMessages(conversationId): Promise<Record<string, unknown>[]> | List messages in a conversation, oldest first. |
reply(messageId, text?, html?): Promise<Record<string, unknown>> | Reply to a specific message, auto-threaded on its channel. |
typing(messageId): Promise<Record<string, unknown>> | Show a "thinking…" indicator on the message's channel (Discord/Telegram; no-op elsewhere). |
sendMessage(conversationId, text?, html?): Promise<Record<string, unknown>> | Proactively send into an existing conversation (needs the SEND capability). |
initiate(connectionId, recipient, text): Promise<Record<string, unknown>> | Cold-start a new conversation (needs the INITIATE capability: user account). |
backfill(conversationId, limit?): Promise<Record<string, unknown>> | Pull history from before the connection existed (needs BACKFILL). limit defaults to 50. |
testEmail(opts?): Promise<Record<string, unknown>> | Inject a test email so your handler fires. Options: text?, subject?, connectionId?. |
convos = client.list_conversations()
msgs = client.list_messages(convos[0]["id"])
# proactive send into an existing thread
client.send_message(convos[0]["id"], text="Following up on your ticket.")
# trigger your handler without a mail client
client.test_email(text="ping", subject="hello")const convos = await client.listConversations();
const msgs = await client.listMessages(convos[0].id);
// proactive send into an existing thread
await client.sendMessage(convos[0].id, "Following up on your ticket.");
// trigger your handler without a mail client
await client.testEmail({ text: "ping", subject: "hello" });Webhooks and channels
Receive events by push instead of (or alongside) the polling loop, and inspect what transports the gateway has configured.
| Method | Description |
|---|---|
setWebhook(url, secret?): Promise<Record<string, unknown>> | Register a URL to receive events by push. Optional secret for signing. |
getWebhook(): Promise<Record<string, unknown>> | Fetch the currently configured webhook. |
channels(): Promise<Record<string, unknown>[]> | List configured transports and their capabilities. |
Events and listening
The event stream is the source of truth for inbound messages. listen() polls it and dispatches to your handlers; the lower-level methods let you drive the loop yourself. 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.
| Method | Description |
|---|---|
onMessage(handler): MessageHandler | Register a handler; the same one answers every channel. May be sync or async. Register multiple; all run per message. |
events(opts?): Promise<EventRecord[]> | Read raw events. Options: afterSeq? (default 0), limit? (default 100), type?. |
dispatchPending(afterSeq?): Promise<number> | Drain all currently available events once and return the last seen seq. Handler exceptions are caught per message. |
listen(opts?): Promise<void> | Poll forever and dispatch inbound messages. Resilient: throwing handlers are logged and skipped, failed polls retried with backoff. |
ListenOptions
| Field | Type | Description |
|---|---|---|
fromSeq | number | Start from this seq instead of "newest at startup". |
pollInterval | number | Seconds between polls when idle. Default 1. |
maxBackoff | number | Max backoff in seconds after repeated poll failures. Default 30. |
signal | AbortSignal | Abort to stop the loop gracefully. |
ack | string | Send an instant acknowledgement reply (e.g. "On it, one moment…") the moment a message arrives, before your handler runs. Best on channels with no typing indicator (X, SMS, email); the real answer follows. |
Stop the loop cleanly with an AbortController, the JS equivalent of catching KeyboardInterrupt in Python:
try:
client.listen() # blocks until Ctrl-C
except KeyboardInterrupt:
passconst controller = new AbortController();
process.on("SIGINT", () => controller.abort());
await client.listen({ signal: controller.signal }); // resolves when abortedThe Message object
What your handler receives. Fields are identical across every channel, so one handler works everywhere. Note the camelCase (conversationId, connectionId) versus Python's snake_case.
| Property | Type | Description |
|---|---|---|
id | string | Message id. Pass to reply() / typing(). |
conversationId | string | Stable thread id; key your agent's memory on it. |
connectionId | string | Which connection (channel) delivered it. |
customerId | string | Customer scope, if set. |
agentId | string | Agent scope, if set. |
channel | string | Source channel, e.g. "email", "slack". |
sender | Record<string, unknown> | null | Who sent it (channel-specific shape). |
subject | string | null | Subject line, where the channel has one (email). |
text | string | null | Plain-text body. |
html | string | null | HTML body, where present. |
| Method | Description |
|---|---|
message.reply(text?, html?): Promise<Record<string, unknown>> | Reply on whichever channel this message arrived from, auto-threaded. |
message.typing(): Promise<Record<string, unknown>> | Keep the "thinking…" indicator alive during long work. |
@client.on_message
def handle(message):
message.typing() # extend the indicator for slow work
answer = my_agent(message.text)
message.reply(answer)client.onMessage(async (message) => {
await message.typing(); // extend the indicator for slow work
const answer = await myAgent(message.text);
await message.reply(answer);
});Errors
Any non-2xx gateway response (or a transport failure) throws CommError. It carries the HTTP status and the gateway's detail string.
| Property | Type | Description |
|---|---|---|
statusCode | number | HTTP status (e.g. 401, 408, 502). 0 for a transport/setup error. |
detail | string | The gateway's error detail (or validation payload, stringified). |
from caspian_sdk import CommError
try:
client.connect_telegram(bot_token="bad")
except CommError as e:
print(e.status_code, e.detail)import { CommError } from "caspian-sdk";
try {
await client.connectTelegram({ botToken: "bad" });
} catch (e) {
if (e instanceof CommError) console.error(e.statusCode, e.detail);
}listen() never throws on poll failures (it retries with backoff), but a connect* that times out while provisioning throws CommError(408), and a failed provision throws CommError(502). Handle those where you connect.