npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tello-ai/sdk

v0.1.3

Published

Tello SDK for Node.js - WebSocket realtime client

Readme

English | 한국어

@tello-ai/sdk

Node.js WebSocket SDK for the Tello /sdk protocol. The SDK is the "conversation brain": the gateway streams each caller turn from a live phone call, and your handler's reply is forwarded back into the call.

repo: tello-js · npm package: @tello-ai/sdk

Transport is WebSocket only. There is no REST or webhook surface.

1. Install

npm install @tello-ai/sdk

ESM and CJS builds ship together with type declarations. The only runtime dependency is ws.

2. API key

Authentication is handled internally by connect() — there is no separate step to call. After the WebSocket opens, the client sends an auth frame ({ event: "auth", data: { token } }, where token is your API key) as its first application frame, and connect() only resolves once the server replies with auth.ok. The API key is never placed in the WebSocket URL, an Authorization header, logs, or error messages. If the server rejects the key (an unauthenticated error or a 4401 close) or auth.ok does not arrive within openTimeoutMs, connect() rejects.

Omitting apiKey / url falls back to TELLO_API_KEY / TELLO_URL, and then to wss://api.telloai.io/sdk. The client appends sdk=js, version and protocol query parameters to the URL it opens, so the gateway can tell which SDK is connecting. The path and your other query parameters are kept as written; any sdk / version / protocol pairs you put in the URL are replaced. ws+unix: URLs are opened unchanged, without these parameters.

3. Connect + start a call

import { EventType, TelloClient } from "@tello-ai/sdk";

const client = await new TelloClient({
  apiKey: process.env.TELLO_API_KEY,
  url: process.env.TELLO_URL ?? "wss://api.telloai.io/sdk",
}).connect();

client.on(EventType.UserTurn, async (event) => {
  await client.answer(`heard: ${event.text ?? ""}`);
  await client.sendDtmf("1234#");
});

await client.createCall("+821012345678", "reservation check");
await client.waitClosed();

Outbound command frames use { event, data }. Inbound gateway frames are flat and dispatched by type.

4. Realtime turn events (pub/sub)

client.on(type, handler) subscribes per event type; handlers may be sync or async and are awaited in registration order. Every event arrives as one TelloEvent object whose populated fields depend on the type, with the decoded frame always available on event.raw.

Call-stream events carry type, version, sessionId, callId and timestamp. call.summary and error are command responses rather than stream events, so the gateway sends no sessionId or timestamp on them and those fields arrive empty.

| EventType | value | populated fields | | --- | --- | --- | | CallCreated | call.created | — (common fields only) | | UserTurn | user.turn | turnIndex, text | | AgentTurn | agent.turn | turnIndex, text | | AnswerAccepted | answer.accepted | requestId, messageId | | DtmfAccepted | dtmf.accepted | requestId, messageId, digits | | CallSummary | call.summary | requestId, status, durationSeconds, transcript, summary, creditCharged | | CallStatusChanged | call.statusChanged | status, previousStatus | | CallCompleted | call.completed | status | | CallNoAnswer | call.noAnswer | status, failureReason | | CallFailed | call.failed | status, failureReason | | Error | error | code, message, requestId, question | | Disconnected | disconnected | SDK-local; emitted when the WS closes |

auth.ok is consumed internally by connect() and never re-emitted. client.off(type, handler) removes a subscription.

5. Commands

await client.createCall(to, prompt?, metadata?, requestId?);
await client.answer(text?, messageId?, requestId?);
await client.sendDtmf(digits, messageId?, requestId?);
await client.cancel();
await client.getSummary(callId, requestId?);

requestId correlates a command with its response frame; it is not an idempotency key. createCall always carries one: when you omit it, the client generates a random UUID, so an error answering that createCall can be told apart from errors for other commands. Give each command its own requestId, and never reuse a createCall's on another command: an error echoing it ends the wait for the call.

await client.waitClosed() resolves when the call reaches a terminal state (call.completed / call.noAnswer / call.failed, or call.statusChanged with status cancelled) or the connection closes, and rejects with the error that ended the call (section 6). A wait in progress returns when its own call ends, even if a handler starts a follow-up call in the meantime; call waitClosed() again to wait for the follow-up. await client.aclose() closes the socket.

After cancel(), the gateway sends call.statusChanged with status cancelled as the call's terminal event, so the wait resolves.

6. Error handling

Gateway error frames map 1:1 to error classes (all extend TelloError). Every error carries the gateway code on .code — branch on that, never on .message, which is display text the gateway may reword.

| gateway code | error | | --- | --- | | unauthenticated | AuthenticationError (auth handshake; also close code 4401) | | toRequired | ValidationError | | callIdRequired | ValidationError | | dtmfDigitsRequired | ValidationError | | dtmfDigitsInvalid | ValidationError | | callNotFound | ValidationError | | callNotCompleted | ValidationError | | callAlreadyActive | CallAlreadyActiveError | | noActiveCall | NoActiveCallError | | callRejected | CallRejectedError (with .question) | | internalError | TelloServerError |

createCall can also be refused before any call exists — no call.created, no callId, no charge. The gateway never retries these; any retry policy is yours.

| gateway code | error | what to do | | --- | --- | --- | | insufficientCredit | CallRefusedError | tell the user to top up; do not resend | | concurrentLimitExceeded | CallRefusedError | wait for one of your own calls to end, then retry | | callerNotVerified | CallRefusedError | tell the user to verify the number; do not resend | | noRepresentativeNumber | CallRefusedError | tell the user to configure a caller number; do not resend | | callProviderUnauthorized | CallProviderError | service fault; report it, resending never helps | | callProviderDraining | CallProviderError | retry later at your own pace | | callProviderUnavailable | CallProviderError | retry later at your own pace | | callSetupFailed | CallProviderError | surface as a failure and report it |

Every command error is delivered to EventType.Error subscribers, and none of them closes the socket. waitClosed() re-throws only an error that ends the call, so a failed createCall (e.g. toRequired, callRejected) does not hang:

  • auth failure (unauthenticated frame, close 4401, or auth.ok timeout) → AuthenticationError, thrown from connect()
  • an error answering the call's createCall, matched by its requestId → its mapped error above. This covers a call-start rejection, and a call that fails after call.created (the gateway cancels it and sends only that error, with no terminal event). Two codes are exceptions: noActiveCall never ends the wait, and callAlreadyActive ends it only when it answers the createCall that opened the call (see below)
  • the connection dropping mid-call → ConnectionClosedError
  • the session being displaced (close 4429) → SessionReplacedError

callAlreadyActive answering the createCall that opened the call means the gateway is still finishing your previous call: it holds the session for a short cleanup window after that call's terminal event. The new call never started (no call.created), so waitClosed() rejects with CallAlreadyActiveError; send the createCall again shortly. A createCall sent while a call is in progress is refused with callAlreadyActive too, but that is only an EventType.Error event and the call in progress goes on.

Errors from any other command (answer, sendDtmf, getSummary, cancel) are delivered only as EventType.Error events. The call keeps running, and waitClosed() keeps waiting for a terminal event or the connection to close.

An EventType.Error handler receives the event, not an error object. exceptionFor(code, message, question?) turns it into the same typed error waitClosed() throws:

import { EventType, exceptionFor, ValidationError } from "@tello-ai/sdk";

client.on(EventType.Error, (event) => {
  const error = exceptionFor(event.code ?? "", event.message ?? "", event.question);
  if (error instanceof ValidationError) {
    console.warn(`command ${event.requestId} rejected: ${error.code}`);
  }
});

The gateway drives a WS-level ping heartbeat; ws answers pongs automatically. There is no reconnect/resume — treat an abnormal close as reconnect-worthy and restart the call.

7. Examples

Runnable programs live in examples/:

npm run build
node examples/basic-call.ts      # connect, one call, answer each turn
node examples/agent-callback.ts  # full lifecycle, history, cancel, typed errors
node examples/call-summary.ts    # gated live scenario ending in call.summary

They place real calls. Read examples/README.md first.

8. Version compatibility

@tello-ai/sdk 0.1.x implements Tello WS protocol 1.0 (PROTOCOL_VERSION).

The full frame contract is in docs/protocol/sdk-ws.v1.md, with docs/events/sdk-events.v1.schema.json and docs/errors/errors.v1.json. Those three files are generated copies of the canonical contract that lives beside the gateway implementation — read them here, edit them there.