SDK reference

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-sdk

Needs 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:

agent.ts
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)
});
OptionTypeDescription
apiKeystringBearer key. Falls back to COMM_API_KEY (env or ./.env).
baseUrlstringGateway URL. Falls back to COMM_BASE_URL, then http://127.0.0.1:8000.
timeoutnumberPer-request timeout in seconds. Default 30.
fetchtypeof fetchCustom fetch implementation. Defaults to global fetch.

A complete agent

The shape of nearly every program: construct, connect a channel, register a handler, listen.

agent.py
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 forever
agent.ts
import { 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 stop

Handlers 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.

MethodDescription
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.

MethodDescription
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

FieldTypeDescription
customerIdstringScope the connection to a customer.
agentIdstringAttach the connection to an agent.
displayNamestringName the agent posts under, where the channel supports it.
capabilitiesstring[]Requested capabilities for this connection.
waitbooleanWait for provisioning to finish. Default true; forced false for OAuth channels.
timeoutnumberProvisioning wait timeout in seconds. Default 60.
pollIntervalnumberPoll interval while provisioning, in seconds. Default 0.5.

Connect methods

MethodChannel-specific optionsDescription
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. 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?)noneiMessage line. Provider agentphone-imessage.
connectRcs(opts?)noneRCS 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.
installX(opts?)customerId?, agentId?One-click connect of an X account, no tokens. Returns an authorize_url ("Sign in with X").
connectInstagram(opts?)noneInstagram DM install (OAuth). free Returns an authorize_url.
connectFacebook(opts?)noneFacebook 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.

MethodDescription
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.

MethodDescription
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.

MethodDescription
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.

MethodDescription
onMessage(handler): MessageHandlerRegister 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

FieldTypeDescription
fromSeqnumberStart from this seq instead of "newest at startup".
pollIntervalnumberSeconds between polls when idle. Default 1.
maxBackoffnumberMax backoff in seconds after repeated poll failures. Default 30.
signalAbortSignalAbort to stop the loop gracefully.
ackstringSend 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:
    pass
const controller = new AbortController();
process.on("SIGINT", () => controller.abort());

await client.listen({ signal: controller.signal });   // resolves when aborted

The 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.

PropertyTypeDescription
idstringMessage id. Pass to reply() / typing().
conversationIdstringStable thread id; key your agent's memory on it.
connectionIdstringWhich connection (channel) delivered it.
customerIdstringCustomer scope, if set.
agentIdstringAgent scope, if set.
channelstringSource channel, e.g. "email", "slack".
senderRecord<string, unknown> | nullWho sent it (channel-specific shape).
subjectstring | nullSubject line, where the channel has one (email).
textstring | nullPlain-text body.
htmlstring | nullHTML body, where present.
MethodDescription
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.

PropertyTypeDescription
statusCodenumberHTTP status (e.g. 401, 408, 502). 0 for a transport/setup error.
detailstringThe 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.

Next steps

REST API →
The raw endpoints under every SDK method.
Python SDK →
The same reference, snake_case.
Behavior guides →
Make your agent reply the right way per channel.
Channels →
What each channel needs to connect.