메인 콘텐츠로 건너뛰기

Idempotency

멱등성 있는 처리를 위해 이벤트의 id와 리소스의 resourceId를 사용하세요.
const processedEvents = new Set();

function processEvent(event) {
  // Deduplicate by event ID
  if (processedEvents.has(event.id)) {
    return;
  }

  // Process the event
  handleEvent(event);
  processedEvents.add(event.id);
}
processed_events = set()

def process_event(event):
    # Deduplicate by event ID
    if event['id'] in processed_events:
        return

    # Process the event
    handle_event(event)
    processed_events.add(event['id'])
var processedEvents = make(map[string]bool)

func processEvent(event WebhookEvent) {
    // Deduplicate by event ID
    if processedEvents[event.ID] {
        return
    }

    // Process the event
    handleEvent(event)
    processedEvents[event.ID] = true
}
@Service
class WebhookService {
    private val processedEvents = mutableSetOf<String>()

    fun processEvent(event: WebhookEvent) {
        // Deduplicate by event ID
        if (event.id in processedEvents) {
            return
        }

        // Process the event
        handleEvent(event)
        processedEvents.add(event.id)
    }
}

에러 처리

항상 적절한 HTTP 상태 코드를 반환하세요.
app.post('/en/developers/webhooks/tiro', (req, res) => {
  try {
    const event = req.body;
    
    // Process the event
    processEvent(event);
    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook processing error:', error);
    res.status(500).send('Internal server error');
  }
});
from flask import Flask, request, jsonify

@app.route('/en/developers/webhooks/tiro', methods=['POST'])
def handle_webhook():
    try:
        event = request.get_json()
        
        # Process the event
        process_event(event)
        return '', 200
    except Exception as error:
        print(f'Webhook processing error: {error}')
        return 'Internal server error', 500
func handleWebhook(w http.ResponseWriter, r *http.Request) {
    var event WebhookEvent
    
    if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }
    
    // Process the event
    if err := processEvent(event); err != nil {
        log.Printf("Webhook processing error: %v", err)
        http.Error(w, "Internal server error", http.StatusInternalServerError)
        return
    }
    
    w.WriteHeader(http.StatusOK)
}
@RestController
class WebhookController {
    
    @PostMapping("/en/developers/webhooks/tiro")
    fun handleWebhook(@RequestBody event: WebhookEvent): ResponseEntity<String> {
        return try {
            // Process the event
            processEvent(event)
            ResponseEntity.ok("OK")
        } catch (error: Exception) {
            logger.error("Webhook processing error", error)
            ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("Internal server error")
        }
    }
}

재시도 처리

Tiro의 webhook 시스템은 전달에 실패한 요청을 재시도해요. 재시도를 자연스럽게 처리하세요.
  • 2xx 상태 코드 반환: 처리에 성공한 경우
  • 4xx 상태 코드 반환: 영구적인 실패인 경우 (재시도하지 않아요)
  • 5xx 상태 코드 반환: 일시적인 실패인 경우 (재시도해요)

보안

항상 webhook의 진위 여부를 검증하세요.
function verifyWebhookAuth(authHeader, expectedSecret) {
  const token = authHeader.replace('Bearer ', '');
  return token === expectedSecret;
}

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
  processEvent(req.body);
  res.status(200).send('OK');
});
import os

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

@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()
    process_event(event)
    return 'OK', 200
import (
    "os"
    "strings"
)

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

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)
    processEvent(event)
    w.WriteHeader(http.StatusOK)
}
@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
    }
    
    @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
        processEvent(event)
        return ResponseEntity.ok("OK")
    }
}

리소스 라우팅

효율적인 라우팅을 위해 resourceTyperesourceId를 사용하세요.
function routeEvent(event) {
  const { resourceType, resourceId } = event.data;

  switch (resourceType) {
    case 'Note':
      return handleNoteEvent(event.type, resourceId, event.data.resource);
    case 'NoteDocument':
      return handleNoteDocumentEvent(event.type, resourceId, event.data.resource);
    case 'NoteSummary':
      return handleNoteSummaryEvent(event.type, resourceId, event.data.resource);
    case 'FolderNoteRelation':
      return handleFolderNoteEvent(event.type, resourceId, event.data.resource);
    default:
      console.log('Unknown resource type:', resourceType);
  }
}