@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/sdkTransport is WebSocket only. There is no REST or webhook surface.
1. Install
npm install @tello-ai/sdkESM 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 (
unauthenticatedframe, close 4401, orauth.oktimeout) →AuthenticationError, thrown fromconnect() - an error answering the call's
createCall, matched by itsrequestId→ its mapped error above. This covers a call-start rejection, and a call that fails aftercall.created(the gateway cancels it and sends only that error, with no terminal event). Two codes are exceptions:noActiveCallnever ends the wait, andcallAlreadyActiveends it only when it answers thecreateCallthat 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.summaryThey 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.
