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

# Best Practices

> Best practices for handling webhook events

## Idempotency

Use the event `id` and resource `resourceId` for idempotent processing:

<CodeGroup>
  ```javascript Node.js theme={"system"}
  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);
  }
  ```

  ```python Python theme={"system"}
  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'])
  ```

  ```go Go theme={"system"}
  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
  }
  ```

  ```kotlin Kotlin + Spring theme={"system"}
  @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)
      }
  }
  ```
</CodeGroup>

## Error Handling

Always return appropriate HTTP status codes:

<CodeGroup>
  ```javascript Node.js theme={"system"}
  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');
    }
  });
  ```

  ```python Python theme={"system"}
  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
  ```

  ```go Go theme={"system"}
  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)
  }
  ```

  ```kotlin Kotlin + Spring theme={"system"}
  @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")
          }
      }
  }
  ```
</CodeGroup>

## Retry Handling

Our webhook system will retry failed deliveries. Handle retries gracefully:

* **Return 2xx status codes** for successful processing
* **Return 4xx status codes** for permanent failures (we won't retry)
* **Return 5xx status codes** for temporary failures (we will retry)

## Security

Always verify webhook authenticity:

<CodeGroup>
  ```javascript Node.js theme={"system"}
  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');
  });
  ```

  ```python Python theme={"system"}
  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
  ```

  ```go Go theme={"system"}
  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)
  }
  ```

  ```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
      }
      
      @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")
      }
  }
  ```
</CodeGroup>

### Resource Routing

Use `resourceType` and `resourceId` for efficient routing:

```javascript theme={"system"}
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);
  }
}
```
