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

# Webhook 개요

> Webhook 엔드포인트는 노트·요약·문서가 바뀌는 순간 Tiro가 HTTP POST를 보내는 여러분 앱의 URL이에요. 폴링 없이 실시간으로 이벤트를 받으세요.

export const EventBaseStructure = {
  id: "evt_01J9ABCDEF",
  type: "note.created",
  createdAt: "2025-09-05T07:12:34Z",
  workspaceGuid: "ws_a1b2c3d4",
  data: {
    resourceType: "Note",
    resourceId: "note-guid-123",
    resource: {
      "title": "Meeting Note"
    }
  }
};

export const Json = ({data, lang = 'json', spaces = 2}) => <pre>
    <code className={`language-${lang}`}>{JSON.stringify(data, null, spaces)}</code>
  </pre>;

## Webhook 엔드포인트가 뭔가요?

Webhook 엔드포인트(endpoint)는 노트, 요약, 문서가 바뀌는 순간 Tiro가 HTTP POST 요청을 보내는 여러분 애플리케이션의 URL이에요. [Tiro Platform](https://platform.tiro.ooo)에 엔드포인트를 한 번 등록해 두면, Tiro가 모든 이벤트를 실시간으로 그 주소로 보내줘요. API를 폴링할 필요가 없어요.

## Webhook이란?

Webhook을 사용하면 워크스페이스에서 이벤트가 발생할 때 애플리케이션이 실시간 알림을 받을 수 있어요. API를 폴링하는 대신, 무언가 발생할 때마다 Tiro가 여러분의 endpoint로 HTTP POST 요청을 보내요.

## Webhook 동작 방식

1. **설정**: 애플리케이션에 webhook endpoint를 마련해요
2. **등록**: 받고 싶은 이벤트를 위해 endpoint를 추가해요
3. **수신**: 이벤트가 발생하면 즉시 알림을 받아요
4. **처리**: 애플리케이션에서 이벤트 데이터를 처리해요

## 이벤트 & 리소스 구조

Webhook은 **이벤트(Event)** 와 **리소스(Resource)** 를 중심으로 구성돼요.

### 이벤트(Events)

이벤트는 워크스페이스에서 발생하는 동작을 나타내요. 자세한 내용은 [Note Events](/ko/developers/webhooks/events/note-events), [Note Document Events](/ko/developers/webhooks/events/note-document-events), [Note Summary Events](/ko/developers/webhooks/events/note-summary-events), [FolderNote Events](/ko/developers/webhooks/events/folder-note-events)를 참고하세요.
Webhook 이벤트는 메타데이터만 포함하도록 설계되어 보통 수백 KB 이하로 유지돼요. transcript나 스크립트 같은 대용량 콘텐츠는 webhook의 안정성과 성능을 보장하기 위해 별도의 API를 통해 접근해요.

### 리소스(Resources)

리소스는 이벤트가 작용할 수 있는 주요 엔터티를 나타내요. 현재 지원되는 항목은 다음과 같아요.

* `Note`: 개별 노트 리소스
* `NoteDocument`: 노트에서 생성된 템플릿 기반 문서
* `NoteSummary`: 노트에 대한 AI 생성 요약
* `FolderNoteRelation`: 폴더와 노트 사이의 관계

## Webhook Payload 구조

모든 webhook 이벤트는 표준 [Event Structure](/ko/developers/webhooks/events/schema) 구조를 따라요.

<Json data={EventBaseStructure} />

## 보안

모든 webhook 요청에는 두 가지 검증 수단이 함께 실려요.

| 헤더                 | 내용                                                      |
| ------------------ | ------------------------------------------------------- |
| `Authorization`    | `Bearer {secret_key}` — endpoint 설정 시 발급된 secret        |
| `X-Tiro-Signature` | `sha256={HMAC-SHA256 서명}` — 요청 본문을 secret으로 서명한 값 (hex) |

가장 간단한 검증은 `Authorization` 헤더의 secret을 설정해 둔 값과 비교하는 것이에요. 더 강한 검증이 필요하면 `X-Tiro-Signature` 헤더의 HMAC-SHA256 서명을 확인하세요 — secret 자체를 비교하지 않고도 요청 진위와 본문 변조 여부를 함께 확인할 수 있어요.

### 검증 예시

<CodeGroup>
  ```javascript Node.js theme={"system"}
  function verifyWebhookAuth(authHeader, expectedSecret) {
    const token = authHeader.replace('Bearer ', '');
    return token === expectedSecret;
  }

  // Usage in your webhook endpoint
  app.post('/en/developers/webhooks/tiro', (req, res) => {
    const authHeader = req.headers.authorization;
    
    if (!verifyWebhookAuth(authHeader, process.env.WEBHOOK_SECRET)) {
      return res.status(401).send('Unauthorized');
    }
    
    // Process webhook event
    const event = req.body;
    handleWebhookEvent(event);
    
    res.status(200).send('OK');
  });
  ```

  ```python Python theme={"system"}
  import os
  from flask import Flask, request

  def verify_webhook_auth(auth_header, expected_secret):
      if not auth_header or not auth_header.startswith('Bearer '):
          return False
      token = auth_header.replace('Bearer ', '')
      return token == expected_secret

  # Usage in your webhook endpoint
  @app.route('/en/developers/webhooks/tiro', methods=['POST'])
  def handle_webhook():
      auth_header = request.headers.get('Authorization')
      
      if not verify_webhook_auth(auth_header, os.getenv('WEBHOOK_SECRET')):
          return 'Unauthorized', 401
      
      # Process webhook event
      event = request.get_json()
      handle_webhook_event(event)
      
      return 'OK', 200
  ```

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

  func verifyWebhookAuth(authHeader, expectedSecret string) bool {
      if !strings.HasPrefix(authHeader, "Bearer ") {
          return false
      }
      token := strings.TrimPrefix(authHeader, "Bearer ")
      return token == expectedSecret
  }

  // Usage in your webhook endpoint
  func handleWebhook(w http.ResponseWriter, r *http.Request) {
      authHeader := r.Header.Get("Authorization")
      
      if !verifyWebhookAuth(authHeader, os.Getenv("WEBHOOK_SECRET")) {
          http.Error(w, "Unauthorized", http.StatusUnauthorized)
          return
      }
      
      // Process webhook event
      var event WebhookEvent
      json.NewDecoder(r.Body).Decode(&event)
      handleWebhookEvent(event)
      
      w.WriteHeader(http.StatusOK)
  }
  ```

  ```kotlin Kotlin + Spring theme={"system"}
  @RestController
  class WebhookController {
      
      @Value("\${webhook.secret}")
      private lateinit var webhookSecret: String
      
      private fun verifyWebhookAuth(authHeader: String?, expectedSecret: String): Boolean {
          if (authHeader?.startsWith("Bearer ") != true) {
              return false
          }
          val token = authHeader.removePrefix("Bearer ")
          return token == expectedSecret
      }
      
      // Usage in your webhook endpoint
      @PostMapping("/en/developers/webhooks/tiro")
      fun handleWebhook(
          @RequestHeader("Authorization") authHeader: String?,
          @RequestBody event: WebhookEvent
      ): ResponseEntity<String> {
          if (!verifyWebhookAuth(authHeader, webhookSecret)) {
              return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Unauthorized")
          }
          
          // Process webhook event
          handleWebhookEvent(event)
          return ResponseEntity.ok("OK")
      }
  }
  ```
</CodeGroup>

## 전달 & 재시도

* **Method**: HTTP POST
* **Content-Type**: `application/json`
* **Timeout**: 60초
* **Retries**: exponential backoff로 최대 5회 재시도 (총 6회 시도)
* **Success**: 모든 2xx HTTP 상태 코드

### 재시도 스케줄

1. 15초
2. 30초
3. 5분
4. 30분
5. 2시간

## 시작하기

1. **endpoint 마련**: POST 요청을 받을 수 있는 HTTP endpoint를 만들어요
2. **webhook 설정**: [Tiro Platform](https://platform.tiro.ooo)에서 webhook endpoint를 등록해요
3. **이벤트 처리**: 애플리케이션에서 들어오는 webhook payload를 처리해요

[Tiro Platform](https://platform.tiro.ooo)에서 webhook endpoint를 설정하세요.

<Note>
  Webhook endpoint는 워크스페이스 단위로 등록/관리돼요. Platform에서 endpoint를 추가하면 현재 워크스페이스의 이벤트가 그 endpoint로 전달돼요. 여러 워크스페이스를 운영한다면 워크스페이스마다 endpoint를 등록하고, payload 최상위의 `workspaceGuid`로 이벤트 출처를 구분하세요.
</Note>

<Note>
  조직(Organization) 계약을 사용 중이라면 **조직 단위 webhook**도 등록할 수 있어요. 조직 endpoint는 조직 소속 워크스페이스 전체의 이벤트를 워크스페이스 구분 없이 받고, payload에 `organizationGuid`가 포함돼요. 자세한 내용은 [조직 단위로 연동하기](/ko/developers/organization/org-integration)를 참고하세요.
</Note>

endpoint를 등록했다면, 받게 될 [Event Structure](/ko/developers/webhooks/events/schema)를 먼저 익혀 보세요. 그런 다음 [Best Practices](/ko/developers/webhooks/best-practices)를 읽고, 운영에 들어가기 전에 재시도, idempotency, 서명 검증을 처리하세요.
