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

# 인증

> 워크스페이스 사용자 키와 시스템 키를 구분하고, Tiro 외부 API에 필요한 Scope와 인증 방법을 알아보세요.

## API 키

Tiro API는 인증에 API 키를 사용해요. 모든 API 요청은 Authorization 헤더에 Bearer token 형식으로 유효한 API 키를 포함해야 해요.

각 키는 하나의 **워크스페이스**에 귀속돼요. 키는 그 워크스페이스의 리소스 — 노트, 전사, 요약, 폴더 — 에만 닿고, 그 밖으로는 닿지 않아요. 워크스페이스를 먼저 고른 다음 키를 발급하세요.

### 키 종류와 권한

Tiro API 키는 호출 주체에 따라 두 가지로 나뉘어요.

| 키 종류             | 식별 방식                          | 사용할 수 있는 작업                                                                 |
| ---------------- | ------------------------------ | --------------------------------------------------------------------------- |
| **사용자 키**        | 키가 특정 사용자 identity에 연결돼요.      | 읽기와 쓰기 작업에 사용할 수 있어요. 노트 문서 생성, 공유 링크 변경처럼 워크스페이스 데이터를 바꾸는 작업은 사용자 키가 필요해요. |
| **워크스페이스 시스템 키** | 사용자 없이 워크스페이스 컨테이너를 기준으로 동작해요. | 워크스페이스 전체에 공개된 리소스를 읽는 용도예요. Voice File Job 생명주기 API만 쓰기 작업을 지원해요.          |

워크스페이스 시스템 키는 워크스페이스 구성원 전체에게 공유된 폴더만 조회해요. 개인 폴더나 특정 사용자에게만 공유된 폴더까지 읽어야 하거나, Voice File Job 생명주기 외의 데이터를 생성·수정·삭제해야 한다면 사용자 키를 발급해 사용하세요.

<Note>
  워크스페이스 시스템 키가 아직 워크스페이스에 바인딩되지 않은 상태라면 인증 요청은 `401 Unauthorized`로 실패할 수 있어요. 키가 올바른 형식인데도 401이 계속된다면 키가 연결된 워크스페이스와 발급 화면을 먼저 확인하세요.
</Note>

<Warning>
  **레거시 개인 키는 곧 사용이 중단돼요.** 워크스페이스 도입 이전에 만든 개인 API 키는 **2026년 6월 30일**부로 작동을 멈춰요. 그 전에 워크스페이스 키를 발급해 교체하세요 — 아래 [레거시 개인 키](#레거시-개인-키-지원-중단)를 참고하세요.
</Warning>

## 권한 범위 (Scope)

Scope는 키가 **어떤 작업을 할 수 있는지**를 좁히는 최소 권한 설정이에요. 발급 시 선택하며, 발급 후에도 변경할 수 있어요.

* **Scope를 지정하지 않으면 모든 작업이 허용돼요.** Scope는 열어주는 게 아니라 **좁히는** 용도예요 — 지정한 scope에 해당하는 API만 호출할 수 있고, 나머지는 `403 insufficient_scope`로 거부돼요.
* **`write` scope는 같은 리소스의 `read`를 포함해요.** 예를 들어 `note:write`만 지정한 키로도 `note:read`가 필요한 조회 API를 호출할 수 있어요.
* Scope는 모든 키 종류(사용자·워크스페이스·조직)에 적용돼요.
* 선택할 수 있는 scope 목록은 `GET /v1/api-key-scopes`로 조회할 수 있어요.

### Scope 목록

| Scope                         | 설명                             |
| ----------------------------- | ------------------------------ |
| `note:read`                   | 노트 메타데이터·목록·전사 조회              |
| `note:write`                  | 노트 제목 등 노트 메타데이터 수정            |
| `note_summary:read`           | 노트 요약(한 페이지 문서) 조회             |
| `note_document:read`          | 생성된 문서 조회                      |
| `note_document:write`         | 커스텀 템플릿으로 문서 생성                |
| `note_document_template:read` | 노트 문서 템플릿 목록·조회                |
| `folder:read`                 | 폴더 조회                          |
| `folder:write`                | 폴더 생성·수정·삭제                    |
| `wiki:read`                   | 위키 조회·검색                       |
| `word_memory:read`            | 단어장 조회                         |
| `word_memory:write`           | 단어장 생성·수정·삭제                   |
| `workspace:read`              | 조직 워크스페이스 목록·조회                |
| `workspace:write`             | 조직 워크스페이스 생성·이름 변경·사용 한도 설정·삭제 |
| `voice_file_job:read`         | Voice File Job 상태·전사·번역·요약 조회  |
| `voice_file_job:write`        | Voice File Job 생성·처리 시작·삭제     |

### API별 필요 Scope

Scope를 지정한 키는 아래 표의 scope를 가진 API만 호출할 수 있어요. Scope를 지정하지 않은 키는 모든 API를 호출할 수 있어요.

| API                                                                                                                      | 필요 Scope                      |
| ------------------------------------------------------------------------------------------------------------------------ | ----------------------------- |
| `GET /v1/external/notes`, `POST /v1/external/notes/search`                                                               | `note:read`                   |
| `GET /v1/external/notes/{guid}`, `GET /v1/external/notes/{guid}/folders`                                                 | `note:read`                   |
| `GET /v1/external/notes/{guid}/paragraphs`                                                                               | `note:read`                   |
| `GET /v1/external/notes/{guid}/summaries`, `.../summaries/{id}`                                                          | `note_summary:read`           |
| `GET /v1/external/notes/{guid}/share-link`                                                                               | `note:read`                   |
| `PATCH /v1/external/notes/{guid}`, `PUT /v1/external/notes/{guid}/share-link`, `DELETE .../share-link`                   | `note:write`                  |
| `GET /v1/external/organizations/me/notes`                                                                                | `note:read`                   |
| `GET /v1/external/workspaces/{ws}/notes`, `POST .../workspaces/{ws}/notes/search`                                        | `note:read`                   |
| `GET /v1/external/notes/{guid}/documents`, `.../documents/{id}`                                                          | `note_document:read`          |
| `POST /v1/external/notes/{guid}/documents`                                                                               | `note_document:write`         |
| `GET /v1/external/note-document-templates`, `.../{id}`                                                                   | `note_document_template:read` |
| `GET .../folders` (폴더 조회)                                                                                                | `folder:read`                 |
| `POST/PATCH/PUT/DELETE .../folders` (폴더 변경)                                                                              | `folder:write`                |
| `GET /v1/external/workspaces/{ws}/wiki/**`                                                                               | `wiki:read`                   |
| `GET .../word-memories`, `.../word-memories/{id}` (users·teams·workspaces)                                               | `word_memory:read`            |
| `POST/PATCH/DELETE .../word-memories`                                                                                    | `word_memory:write`           |
| `GET /v1/external/organizations/me/workspaces`, `.../workspaces/{ws}`                                                    | `workspace:read`              |
| `POST /v1/external/organizations/me/workspaces`, `PATCH .../{ws}`, `PATCH .../{ws}/quota`, `DELETE .../{ws}`             | `workspace:write`             |
| `GET /v1/external/voice-file/jobs/{jobId}`, `GET .../transcript`, `GET .../translations/**`, `GET .../paragraph-summary` | `voice_file_job:read`         |
| `POST /v1/external/voice-file/jobs`, `PUT .../{jobId}/upload-complete`, `DELETE .../{jobId}`                             | `voice_file_job:write`        |
| `GET /v1/external/auth/me`, `.../organizations/me`, `.../workspaces`, `.../workspaces/me`                                | (scope 무관 — 모든 키 허용)          |

## API 키 발급

[Tiro Platform 대시보드](https://platform.tiro.ooo/dashboard/api-keys)에서 API 키를 발급받으세요.

<Steps>
  <Step title="로그인">
    [platform.tiro.ooo/dashboard/api-keys](https://platform.tiro.ooo/dashboard/api-keys)로 이동하세요.
  </Step>

  <Step title="워크스페이스 선택">
    사이드바의 워크스페이스 전환기에서 키가 접근할 데이터의 워크스페이스를 고르세요. 키는 이 워크스페이스에만 한정돼요.
  </Step>

  <Step title="키 생성">
    **Create New API Key**를 클릭하고 이름을 지정한 뒤, 점(.)을 포함한 **전체 키**를 복사하세요 — `abc123.xR7mK9pL2qW4...`.
  </Step>

  <Step title="저장">
    환경 변수로 저장하세요. 비밀 값은 한 번만 표시되며, 대화 상자를 닫으면 복구할 수 없어요.
  </Step>
</Steps>

<Warning>
  API 키는 안전하게 보관하고 클라이언트 측 코드에 절대 노출하지 마세요. API 키는
  서버 측 애플리케이션에서만 사용해야 해요.
</Warning>

## API 키 형식

Tiro API 키는 다음 형식을 따라요.

```
{id}.{secret}
```

예시: `abc123.xR7mK9pL2qW4...`

| 부분                      | 예시                       | 설명                                                  |
| ----------------------- | ------------------------ | --------------------------------------------------- |
| **Key ID** (`{id}`)     | `abc123`                 | Platform 대시보드에서 볼 수 있어요. 어떤 키가 요청을 보내는지 식별하는 데 쓰여요. |
| **Secret** (`{secret}`) | `xR7mK9pL2qW4...`        | 생성 시 한 번만 표시돼요. 서버는 해시만 저장하므로 복구할 수 없어요.            |
| **전체 API 키**            | `abc123.xR7mK9pL2qW4...` | **점을 포함한 전체 문자열**이에요. 이 값을 Bearer token으로 사용해요.     |

<Warning>
  **흔한 실수:** Key ID(`abc123`)만 Bearer token으로 쓰지 마세요. **전체 키**(`abc123.xR7mK9pL2qW4...`), 즉 키 생성 시 표시된 전체 문자열을 사용해야 해요.
</Warning>

## 인증된 요청 보내기

모든 요청의 `Authorization` 헤더에 API 키를 포함하세요.

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -H "Authorization: Bearer $TIRO_API_KEY" \
       -H "Content-Type: application/json" \
       https://api.tiro.ooo/v1/external/notes
  ```

  ```javascript Node.js theme={"system"}
  const response = await fetch("https://api.tiro.ooo/v1/external/notes", {
    method: "GET",
    headers: {
      "Authorization": `Bearer ${process.env.TIRO_API_KEY}`,
      "Content-Type": "application/json",
    },
  });

  const notes = await response.json();
  ```

  ```python Python theme={"system"}
  import os
  import requests

  headers = {
      'Authorization': f'Bearer {os.getenv("TIRO_API_KEY")}',
      'Content-Type': 'application/json'
  }

  response = requests.get('https://api.tiro.ooo/v1/external/notes', headers=headers)
  notes = response.json()
  ```

  ```go Go theme={"system"}
  import (
      "fmt"
      "net/http"
      "os"
  )

  func makeAuthenticatedRequest() (*http.Response, error) {
      client := &http.Client{}
      req, err := http.NewRequest("GET", "https://api.tiro.ooo/v1/external/notes", nil)
      if err != nil {
          return nil, err
      }
      
      apiKey := os.Getenv("TIRO_API_KEY")
      req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
      req.Header.Set("Content-Type", "application/json")
      
      return client.Do(req)
  }
  ```

  ```kotlin Kotlin + Spring theme={"system"}
  @Service
  class TiroApiService {
      
      @Value("\${tiro.api.key}")
      private lateinit var apiKey: String
      
      private val restTemplate = RestTemplate()
      
      fun getNotes(): ResponseEntity<String> {
          val headers = HttpHeaders()
          headers.set("Authorization", "Bearer $apiKey")
          headers.contentType = MediaType.APPLICATION_JSON
          
          val entity = HttpEntity<String>(headers)
          
          return restTemplate.exchange(
              "https://api.tiro.ooo/v1/external/notes",
              HttpMethod.GET,
              entity,
              String::class.java
          )
      }
  }
  ```
</CodeGroup>

## 인증 에러

인증에 실패하면 `401 Unauthorized` 응답을 받아요. 흔한 원인은 다음과 같아요.

* Authorization 헤더 누락
* 잘못된 형식의 키 (`{id}.{secret}` 형식이어야 해요)
* 알 수 없는 key id
* 비활성·만료·삭제된 키

```json theme={"system"}
{
  "error": {
    "code": "invalid_api_key",
    "message": "The API key provided is invalid",
    "type": "authentication_error"
  }
}
```

## 레거시 개인 키 (지원 중단)

워크스페이스 도입 이전에는 **개인** API 키가 워크스페이스가 아니라 계정에 묶여 있었어요. 이 개인 키는 이제 지원이 중단돼요. **팀** 키는 이제 워크스페이스 키로 들어와요 — 각 팀이 하나의 워크스페이스에 대응해요. 워크스페이스 키가 개인 키와 팀 키를 모두 대체해요.

<Warning>
  레거시 개인 키는 **2026년 6월 30일**부로 작동을 멈춰요. 그 이후 레거시 개인 키로 보낸 요청은 `401 Unauthorized`를 반환해요. 중단 없이 쓰려면 그 전에 마이그레이션하세요.
</Warning>

|           | 레거시 개인 키                 | 워크스페이스 키                                  |
| --------- | ------------------------ | ----------------------------------------- |
| **범위**    | 계정 전체                    | 워크스페이스 하나                                 |
| **신규 발급** | 비활성화됨                    | 대시보드 → 워크스페이스 선택 → **Create New API Key** |
| **기존 키**  | 2026년 6월 30일까지 조회·폐기만 가능 | 전체 수명 주기                                  |
| **형식**    | `{id}.{secret}`          | `{id}.{secret}` — 동일                      |

형식이 같으므로 마이그레이션은 한 줄 교체면 돼요 — 코드를 다시 쓸 필요 없어요.

### 3단계로 마이그레이션하기

<Steps>
  <Step title="워크스페이스 키 발급">
    [대시보드](https://platform.tiro.ooo/dashboard/api-keys)에서 통합이 사용하는 노트가 들어 있는 워크스페이스를 선택한 뒤 키를 발급하세요.
  </Step>

  <Step title="비밀 값 교체">
    `TIRO_API_KEY` 환경 변수 값을 새 키로 바꾸세요. 다른 코드 변경은 필요 없어요.
  </Step>

  <Step title="레거시 키 폐기">
    트래픽이 새 키로 도는 게 확인되면, 대시보드의 **Legacy personal keys** 섹션에서 레거시 키를 삭제하세요.
  </Step>
</Steps>

<Note>
  레거시 키는 계정의 모든 노트에 닿았지만, 워크스페이스 키는 하나의 워크스페이스에만 닿아요. 데이터가 여러 워크스페이스에 걸쳐 있다면 워크스페이스마다 키를 하나씩 발급하세요.
</Note>

## 보안 모범 사례

### 환경 변수

환경 변수를 사용해 API 키를 안전하게 보관하세요.

<CodeGroup>
  ```bash .env theme={"system"}
  # .env file (never commit this!)
  TIRO_API_KEY=abc123.XYZ...
  ```

  ```javascript Node.js theme={"system"}
  // Load from environment
  const apiKey = process.env.TIRO_API_KEY;
  if (!apiKey) {
    throw new Error('TIRO_API_KEY environment variable is required');
  }
  ```

  ```python Python theme={"system"}
  import os

  # Load from environment with validation
  api_key = os.getenv('TIRO_API_KEY')
  if not api_key:
      raise ValueError('TIRO_API_KEY environment variable is required')
  ```

  ```go Go theme={"system"}
  import (
      "fmt"
      "os"
  )

  func getAPIKey() (string, error) {
      apiKey := os.Getenv("TIRO_API_KEY")
      if apiKey == "" {
          return "", fmt.Errorf("TIRO_API_KEY environment variable is required")
      }
      return apiKey, nil
  }
  ```

  ```kotlin Kotlin + Spring theme={"system"}
  # application.properties
  tiro.api.key=${TIRO_API_KEY}

  # Or application.yml
  tiro:
    api:
      key: ${TIRO_API_KEY}
  ```
</CodeGroup>

### 추가 보안 가이드라인

* **키를 주기적으로 교체하세요**: 사용하지 않는 키는 삭제하고 새 키를 생성하세요
* **환경별로 키를 분리하세요**: 개발과 프로덕션에 서로 다른 키를 사용하세요
* **사용량을 모니터링하세요**: API 키 사용량을 추적하고 이상이 보이면 교체하세요
* **API 키를 절대 로그에 남기지 마세요**: 애플리케이션 로그에 키가 나타나지 않도록 하세요
* **HTTPS만 사용하세요**: 항상 보안 연결로 요청을 보내세요
