> ## Documentation Index
> Fetch the complete documentation index at: https://docs.telloai.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 시작하기

> SDK를 설치하고 키를 발급받아 첫 통화를 걸어봐요.

## 할 수 있는 일

Tello SDK로 코드에서 통화를 만들고, 턴 이벤트에 응답해요. 게이트웨이가 진행 중인 통화에서
상대방이 말한 턴을 실시간으로 넘겨주고, 핸들러가 만든 답변이 다시 통화로 전달돼요. 대화의
두뇌는 여러분 코드가 맡아요.

```bash SDK Server URL theme={null}
wss://api.telloai.io/sdk
```

<Note>
  전송 계층은 WebSocket뿐이에요. REST와 webhook은 제공하지 않아요.
</Note>

## 시작하기 전에

포털 로그인과 SDK 설치 환경이 필요해요. 발신 조건은
[연동 준비](/developer/overview)에서 먼저 확인하세요.

<CodeGroup>
  ```bash Python theme={null}
  pip install tello-sdk    # Python 3.10+. import 이름은 tello
  ```

  ```bash Node theme={null}
  npm install @tello/sdk   # 런타임 의존성은 ws 하나
  ```

  ```bash Go theme={null}
  go get github.com/tello-tft/tello-go   # Go 1.22+
  ```

  ```kotlin Java theme={null}
  // build.gradle.kts (Java 17+)
  dependencies {
      implementation("ai.tello:tello-sdk:0.1.0")
  }
  ```
</CodeGroup>

## 사용 방법

### API Key 발급

포털 **SDK** 화면 위쪽의 **SDK Server URL**을 복사해 두어요(WebSocket 전용, SDK의 `TELLO_URL`
값). `+ 새 Key 발급`으로 키를 만들고, 코드에 키와 URL을 넣거나 환경변수로 지정해요.

```bash theme={null}
export TELLO_API_KEY="tello_live_xxx"
export TELLO_URL="wss://api.telloai.io/sdk"
```

인자 없이 클라이언트를 생성하면 위 두 변수를 읽어요. 둘 다 없으면
`ws://localhost:3000/sdk`로 폴백하니, 프로덕션에서는 반드시 값을 넣어요.

### 인증

키 인증은 **애플리케이션 레벨 핸드셰이크**로 이뤄져요. 소켓이 열리면 SDK가 `token` 필드에
키를 담은 `auth` 프레임을 보내고, 서버의 `auth.ok`를 받은 뒤에야 나머지 동작이 시작돼요.

```json theme={null}
{ "event": "auth", "data": { "token": "<API_KEY>" } }
```

키는 WS 업그레이드 요청에도, URL query에도 실리지 않아요. `Authorization` 헤더도 쓰지 않으므로
URL·로그·오류 메시지에 키가 남지 않아요. 전부 내부 처리라 `auth`를 직접 호출할 일은 없고,
인증이 끝나기 전에는 연결이 성공하지 않아요.

키가 거부되거나(`unauthenticated` 오류 프레임 또는 `4401` 종료), 서버 데드라인 10초 안에
`auth.ok`가 오지 않으면 연결이 실패하면서 인증 오류가 나요.

### 연결 + 첫 통화

핸들러를 등록하고, 연결하고, 통화를 만들고, 끝날 때까지 기다려요.

<CodeGroup>
  ```python Python theme={null}
  import asyncio
  from tello import TelloClient, EventType

  async def main():
      async with TelloClient(
          api_key="tello_live_xxx",
          url="wss://api.telloai.io/sdk",
      ) as client:
          @client.on(EventType.USER_TURN)
          async def on_user_turn(event):
              await client.answer(text="확인했습니다. 계속 말씀해주세요.")

          await client.create_call(to="+821012345678", prompt="예약 확인")
          await client.wait_closed()

  asyncio.run(main())
  ```

  ```ts Node theme={null}
  import { EventType, TelloClient } from "@tello/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.createCall("+821012345678", "예약 확인");
  await client.waitClosed();
  ```

  ```go Go theme={null}
  package main

  import (
  	"context"
  	"log"

  	"github.com/tello-tft/tello-go/tello"
  )

  func main() {
  	ctx := context.Background()
  	client, err := tello.NewClient("")
  	if err != nil {
  		log.Fatal(err)
  	}

  	client.On(tello.EventTypeUserTurn, func(ctx context.Context, event tello.Event) error {
  		return client.Answer(ctx, "heard: "+event.Text, "", "")
  	})

  	if err := client.Connect(ctx); err != nil {
  		log.Fatal(err)
  	}
  	defer client.Close()

  	if err := client.CreateCall(ctx, "+821012345678", "예약 확인", nil, ""); err != nil {
  		log.Fatal(err)
  	}
  	if err := client.WaitClosed(ctx); err != nil {
  		log.Fatal(err)
  	}
  }
  ```

  ```java Java theme={null}
  try (TelloClient client =
          new TelloClient("tello_live_xxx", "wss://api.telloai.io/sdk").connectBlocking()) {
      client.on(EventType.USER_TURN, e -> {
          TurnEvent turn = (TurnEvent) e;
          client.answer("확인했습니다. 계속 말씀해주세요.");
      });
      client.createCall("+821012345678", "예약 확인").join();
      client.waitClosed();
  }
  ```
</CodeGroup>

<Warning>
  핸들러는 **연결 전에** 등록하세요. 연결 후 등록하면 초기 프레임을 놓쳐요.
</Warning>

### 대화 주고받기

앞 예제는 상대가 무슨 말을 하든 같은 문장으로 답해요. 실제 연동에서는 `user.turn`의 텍스트를
읽어 답을 만들고, ARS처럼 번호를 눌러야 하면 `sendDtmf`로 키패드 입력을 보내요. 그동안 통화가
어떻게 흘러가는지는 이벤트로 따라가요.

<CodeGroup>
  ```python Python theme={null}
  @client.on(EventType.CALL_STATUS_CHANGED)
  async def on_status(event):
      print(f"[상태] {event.previous_status} -> {event.status}")

  @client.on(EventType.USER_TURN)
  async def on_user_turn(event):
      text = event.text or ""
      print(f"[상대 #{event.turn_index}] {text}")

      if "번호" in text:                       # ARS 안내를 만나면 키패드 입력
          await client.send_dtmf(digits="1234#")
          return
      if "감사합니다" in text:                  # 용건이 끝났으면 먼저 끊기
          await client.cancel()
          return

      await client.answer(text=my_agent.reply(text))

  @client.on(EventType.AGENT_TURN)
  async def on_agent_turn(event):
      print(f"[내 답변이 발화됨 #{event.turn_index}] {event.text}")

  @client.on(EventType.DTMF_ACCEPTED)
  async def on_dtmf(event):
      print(f"[DTMF 접수] {event.digits}")

  @client.on(EventType.CALL_FAILED)
  async def on_failed(event):
      print(f"[실패] {event.failure_reason}")

  @client.on(EventType.ERROR)
  async def on_error(event):
      print(f"[오류] {event.code}: {event.message}")
  ```

  ```ts Node theme={null}
  client.on(EventType.CallStatusChanged, (event) => {
    console.log(`[상태] ${event.previousStatus} -> ${event.status}`);
  });

  client.on(EventType.UserTurn, async (event) => {
    const text = event.text ?? "";
    console.log(`[상대 #${event.turnIndex}] ${text}`);

    if (text.includes("번호")) {          // ARS 안내를 만나면 키패드 입력
      await client.sendDtmf("1234#");
      return;
    }
    if (text.includes("감사합니다")) {     // 용건이 끝났으면 먼저 끊기
      await client.cancel();
      return;
    }

    await client.answer(myAgent.reply(text));
  });

  client.on(EventType.AgentTurn, (event) => {
    console.log(`[내 답변이 발화됨 #${event.turnIndex}] ${event.text}`);
  });

  client.on(EventType.DtmfAccepted, (event) => {
    console.log(`[DTMF 접수] ${event.digits}`);
  });

  client.on(EventType.CallFailed, (event) => {
    console.log(`[실패] ${event.failureReason}`);
  });

  client.on(EventType.Error, (event) => {
    console.log(`[오류] ${event.code}: ${event.message}`);
  });
  ```

  ```go Go theme={null}
  client.On(tello.EventTypeCallStatusChanged, func(ctx context.Context, event tello.Event) error {
  	log.Printf("[상태] %s -> %s", event.PreviousStatus, event.Status)
  	return nil
  })

  client.On(tello.EventTypeUserTurn, func(ctx context.Context, event tello.Event) error {
  	log.Printf("[상대 #%d] %s", event.TurnIndex, event.Text)

  	if strings.Contains(event.Text, "번호") { // ARS 안내를 만나면 키패드 입력
  		return client.SendDtmf(ctx, "1234#", "", "")
  	}
  	if strings.Contains(event.Text, "감사합니다") { // 용건이 끝났으면 먼저 끊기
  		return client.Cancel(ctx)
  	}

  	return client.Answer(ctx, myAgent.Reply(event.Text), "", "")
  })

  client.On(tello.EventTypeAgentTurn, func(ctx context.Context, event tello.Event) error {
  	log.Printf("[내 답변이 발화됨 #%d] %s", event.TurnIndex, event.Text)
  	return nil
  })

  client.On(tello.EventTypeDtmfAccepted, func(ctx context.Context, event tello.Event) error {
  	log.Printf("[DTMF 접수] %s", event.Digits)
  	return nil
  })

  client.On(tello.EventTypeCallFailed, func(ctx context.Context, event tello.Event) error {
  	log.Printf("[실패] %s", event.FailureReason)
  	return nil
  })

  client.On(tello.EventTypeError, func(ctx context.Context, event tello.Event) error {
  	log.Printf("[오류] %s: %s", event.Code, event.Message)
  	return nil
  })
  ```

  ```java Java theme={null}
  client.on(EventType.CALL_STATUS_CHANGED, e -> {
      StatusChangedEvent s = (StatusChangedEvent) e;
      System.out.printf("[상태] %s -> %s%n", s.previousStatus, s.status);
  });

  client.on(EventType.USER_TURN, e -> {
      TurnEvent turn = (TurnEvent) e;
      System.out.printf("[상대 #%d] %s%n", turn.turnIndex, turn.text);

      if (turn.text.contains("번호")) {          // ARS 안내를 만나면 키패드 입력
          client.sendDtmf("1234#");
      } else if (turn.text.contains("감사합니다")) { // 용건이 끝났으면 먼저 끊기
          client.cancel();
      } else {
          client.answer(myAgent.reply(turn.text));
      }
  });

  client.on(EventType.AGENT_TURN, e -> {
      TurnEvent turn = (TurnEvent) e;
      System.out.printf("[내 답변이 발화됨 #%d] %s%n", turn.turnIndex, turn.text);
  });

  client.on(EventType.DTMF_ACCEPTED, e -> {
      DtmfAcceptedEvent dtmf = (DtmfAcceptedEvent) e;
      System.out.printf("[DTMF 접수] %s%n", dtmf.digits);
  });

  client.on(EventType.CALL_FAILED, e -> {
      TerminalEvent terminal = (TerminalEvent) e;
      System.out.printf("[실패] %s%n", terminal.failureReason);
  });

  client.on(EventType.ERROR, e -> {
      ErrorEvent error = (ErrorEvent) e;
      System.out.printf("[오류] %s: %s%n", error.code, error.message);
  });
  ```
</CodeGroup>

알아 둘 것 몇 가지예요.

* **`sendDtmf`는 발화가 아니에요.** 톤을 보내는 것이라 `agent.turn`이 따라오지 않아요.
  접수 확인은 `dtmf.accepted`로 받아요. 허용 문자는 `0-9`, `*`, `#`예요.
* **`answer`도 즉시 발화가 아니에요.** `answer.accepted`는 명령이 접수됐다는 뜻이고, 실제로
  말해지면 그때 `agent.turn`이 와요. 어시스턴트 타임아웃으로 그 턴이 버려지면 `agent.turn`은
  오지 않아요.
* 핸들러는 등록 순서대로 처리돼요. 핸들러가 예외를 던져도 수신 루프는 죽지 않아요.
* `error` 이벤트로 오는 명령 오류는 소켓을 닫지 않아요. 통화를 끝낸 오류라면 `waitClosed`가
  다시 던져요.

## 확인 방법

통화가 정상적으로 진행되면 이벤트가 대략 이 순서로 도착해요.

```text theme={null}
call.created                       초기 상태 queued. callId 를 여기서 받아요
call.statusChanged  dialing        발신 시작
call.statusChanged  ringing        상대 단말이 울림
call.statusChanged  inProgress     상대가 받음
user.turn           #0             상대 발화. 답변할 차례
agent.turn          #0             내 답변이 실제로 발화됨
...                                user.turn 과 agent.turn 이 번갈아 반복
call.completed      completed      종단 이벤트. waitClosed 가 여기서 풀려요
```

`dialing`·`ringing`은 건너뛸 수 있어요. 이미 응답된 통화는 `inProgress`로 바로 가므로
**중간 상태가 반드시 온다고 가정하면 안 돼요.**

`answer`를 보내면 `answer.accepted`가 먼저 오는데, 이건 명령이 접수됐다는 뜻일 뿐 발화
보장이 아니에요. 실제로 말해진 것은 뒤따르는 `agent.turn`으로 확인해요.

통화가 끝난 뒤 [getSummary](/developer/sdk/commands/get-summary)로 전사·요약·차감 크레딧을
조회할 수 있어요. 이벤트 13종은 [이벤트](/developer/sdk/events)에, 명령별 상세는
[레퍼런스](/developer/sdk/reference)에 있어요.

## 주의사항

* 실제 통화가 발신되고 크레딧이 차감돼요. 통제된 테스트 수신번호로만 시험하세요.
* 한 연결에서 활성 통화는 하나예요. 통화 중에 `createCall`을 또 보내면 거부돼요.
* **재연결·세션 재개 프로토콜이 없어요.** 비정상 종료가 나면 통화를 처음부터 다시 시작해요.
* 하트비트는 게이트웨이가 주도하고 표준 WS 라이브러리가 pong을 자동으로 보내요. 직접 구현할
  일은 없어요.
* 전체 키는 발급 직후 한 번만 볼 수 있어요. 분실하면 폐기 후 재발급해요.

## 관련 문서

* [레퍼런스](/developer/sdk/reference)
* [이벤트](/developer/sdk/events)
* [오류](/developer/sdk/errors)
* [연동 준비](/developer/overview)
* [MCP](/developer/mcp)
* [A2A](/developer/a2a/inbound)
