Idempotency
Use the eventid and resource resourceId for idempotent processing:
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)
}
}
Error Handling
Always return appropriate HTTP status codes: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")
}
}
}
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: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")
}
}
Resource Routing
UseresourceType and resourceId for efficient routing:
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);
}
}