Skip to main content

Webhooks Configuration

Setup guide, signature verification, and best practices for Trix webhooks.

Setting Up Your Endpoint

Before creating a webhook, you need an endpoint to receive events. Here are example implementations:

Express.js (Node.js)

server.js
const express = require('express');
const crypto = require('crypto');

const app = express();
// IMPORTANT: verify against the raw bytes, not a parsed object.
// Mount a raw body parser on just the webhook route:
app.use('/webhooks/trix', express.raw({ type: 'application/json' }));

const WEBHOOK_SECRET = process.env.TRIX_WEBHOOK_SECRET;

app.post('/webhooks/trix', (req, res) => {
  // 1. Verify signature (req.body is a Buffer because of express.raw)
  const signature = req.headers['x-webhook-signature'];
  const timestamp = req.headers['x-webhook-timestamp'];

  if (!verifySignature(req.body, signature, timestamp)) {
    return res.status(401).send('Invalid signature');
  }

  // 2. Process the event
  const event = JSON.parse(req.body.toString('utf8'));
  console.log(`Received: ${event.event}`);

  // Handle specific event types
  switch (event.event) {
    case 'memory.created':
      handleMemoryCreated(event.data);
      break;
    case 'memory.deleted':
      handleMemoryDeleted(event.data);
      break;
    default:
      console.log(`Unhandled event: ${event.event}`);
  }

  // 3. Respond quickly with 200
  res.status(200).send('OK');
});

// NOTE: Mount this route with `express.raw({type: 'application/json'})` - the
// signature must be verified against the raw request bytes, not a re-serialized object.
function verifySignature(rawBody, signatureHeader, timestamp) {
  // Header shape: "t=<ts>,v1=<hex>"
  const parts = Object.fromEntries(
    (signatureHeader || '').split(',').map(s => s.split('=', 2))
  );
  const providedHex = parts.v1 || '';

  // Check timestamp freshness (5 minute window)
  const age = Date.now() / 1000 - parseInt(timestamp);
  if (age > 300) return false;

  const signedPayload = `${timestamp}.${rawBody.toString('utf8')}`;
  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(signedPayload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(providedHex),
    Buffer.from(expected)
  );
}

app.listen(3000);

Flask (Python)

app.py
from flask import Flask, request, abort
import hmac
import hashlib
import time
import os

app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('TRIX_WEBHOOK_SECRET')

@app.route('/webhooks/trix', methods=['POST'])
def handle_webhook():
    # 1. Verify signature
    signature = request.headers.get('X-Webhook-Signature')
    timestamp = request.headers.get('X-Webhook-Timestamp')

    if not verify_signature(request.data, signature, timestamp):
        abort(401)

    # 2. Process the event
    event = request.get_json()
    print(f"Received: {event['event']}")

    # Handle specific event types
    if event['event'] == 'memory.created':
        handle_memory_created(event['data'])
    elif event['event'] == 'memory.deleted':
        handle_memory_deleted(event['data'])

    # 3. Respond quickly with 200
    return 'OK', 200

def verify_signature(payload, signature_header, timestamp):
    # Signature header is "t=<ts>,v1=<hex>"
    parts = dict(p.split('=', 1) for p in (signature_header or '').split(','))
    provided_hex = parts.get('v1', '')

    # Check timestamp freshness (5 minute window)
    age = time.time() - int(timestamp)
    if age > 300:
        return False

    signed_payload = f"{timestamp}.{payload.decode('utf-8')}"
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(provided_hex, expected)

if __name__ == '__main__':
    app.run(port=3000)

FastAPI (Python)

main.py
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import time
import os

app = FastAPI()
WEBHOOK_SECRET = os.environ.get('TRIX_WEBHOOK_SECRET')

@app.post('/webhooks/trix')
async def handle_webhook(request: Request):
    signature = request.headers.get('X-Webhook-Signature')
    timestamp = request.headers.get('X-Webhook-Timestamp')
    body = await request.body()

    if not verify_signature(body, signature, timestamp):
        raise HTTPException(status_code=401, detail='Invalid signature')

    event = await request.json()
    print(f"Received: {event['event']}")

    # Process asynchronously for long-running tasks
    # background_tasks.add_task(process_event, event)

    return {'status': 'ok'}

def verify_signature(payload: bytes, signature_header: str, timestamp: str) -> bool:
    parts = dict(p.split('=', 1) for p in (signature_header or '').split(','))
    provided_hex = parts.get('v1', '')

    age = time.time() - int(timestamp)
    if age > 300:
        return False

    signed_payload = f"{timestamp}.{payload.decode('utf-8')}"
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(provided_hex, expected)

ASP.NET Core (C#)

WebhookController.cs
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("webhooks")]
public class WebhookController : ControllerBase
{
    private static readonly string WebhookSecret =
        Environment.GetEnvironmentVariable("TRIX_WEBHOOK_SECRET")!;

    [HttpPost("trix")]
    public async Task<IActionResult> HandleWebhook()
    {
        // 1. Read raw body for signature verification
        using var reader = new StreamReader(Request.Body);
        var body = await reader.ReadToEndAsync();

        var signature = Request.Headers["X-Webhook-Signature"].ToString();
        var timestamp = Request.Headers["X-Webhook-Timestamp"].ToString();

        if (!VerifySignature(body, signature, timestamp))
        {
            return Unauthorized("Invalid signature");
        }

        // 2. Process the event
        var payload = System.Text.Json.JsonDocument.Parse(body);
        var eventType = payload.RootElement.GetProperty("event").GetString();
        Console.WriteLine($"Received: {eventType}");

        // Handle specific event types
        switch (eventType)
        {
            case "memory.created":
                // Handle memory created
                break;
            case "memory.deleted":
                // Handle memory deleted
                break;
        }

        // 3. Respond quickly with 200
        return Ok("OK");
    }

    private static bool VerifySignature(string payload, string signatureHeader, string timestamp)
    {
        // Signature header is "t=<ts>,v1=<hex>"
        var parts = (signatureHeader ?? "").Split(',')
            .Select(p => p.Split('=', 2))
            .Where(p => p.Length == 2)
            .ToDictionary(p => p[0], p => p[1]);
        if (!parts.TryGetValue("v1", out var providedHex)) return false;

        // Check timestamp freshness (5 minute window)
        var age = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(timestamp);
        if (age > 300) return false;

        var signedPayload = $"{timestamp}.{payload}";
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(WebhookSecret));
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signedPayload));
        var expected = Convert.ToHexString(hash).ToLowerInvariant();

        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(providedHex),
            Encoding.UTF8.GetBytes(expected)
        );
    }
}

Signature Verification

Always verify webhook signatures to ensure requests are authentic and haven't been tampered with.

How Signatures Work

  1. Trix creates a signed payload: timestamp.JSON_payload
  2. Computes HMAC-SHA256 using your webhook secret
  3. Includes the signature and timestamp in request headers

Headers Sent

HeaderExample ValueDescription
X-Webhook-Signaturea1b2c3d4e5...HMAC-SHA256 hex digest
X-Webhook-Timestamp1705315800Unix timestamp (seconds)
X-Webhook-Delivery-IDdel_abc123Unique delivery ID
X-Webhook-Eventmemory.createdEvent type
X-Webhook-IDwhk_xyz789Webhook ID

Verification Steps

verification-steps.js
// 1. Extract headers
const signature = headers['x-webhook-signature'];
const timestamp = headers['x-webhook-timestamp'];

// 2. Check timestamp freshness (prevent replay attacks)
const age = Date.now() / 1000 - parseInt(timestamp);
if (age > 300) {
  throw new Error('Timestamp too old');
}

// 3. Create signed payload
const signedPayload = `${timestamp}.${JSON.stringify(body)}`;

// 4. Compute expected signature
const expected = crypto
  .createHmac('sha256', WEBHOOK_SECRET)
  .update(signedPayload)
  .digest('hex');

// 5. Use constant-time comparison
const valid = crypto.timingSafeEqual(
  Buffer.from(signature),
  Buffer.from(expected)
);

Security: Always use constant-time comparison (like timingSafeEqual or hmac.compare_digest) to prevent timing attacks.

Handling Duplicates

Webhooks may be delivered more than once due to retries. Use the X-Webhook-Delivery-ID header to deduplicate events.

In-Memory Deduplication

in-memory.js
const processedDeliveries = new Set();

function handleWebhook(req, res) {
  const deliveryId = req.headers['x-webhook-delivery-id'];

  // Skip if already processed
  if (processedDeliveries.has(deliveryId)) {
    console.log(`Duplicate: ${deliveryId}`);
    return res.status(200).send('OK');
  }

  // Process event
  processEvent(req.body);

  // Mark as processed
  processedDeliveries.add(deliveryId);

  res.status(200).send('OK');
}

Redis Deduplication

redis-dedup.js
const Redis = require('ioredis');
const redis = new Redis();

async function handleWebhook(req, res) {
  const deliveryId = req.headers['x-webhook-delivery-id'];

  // Try to set key (NX = only if not exists)
  const isNew = await redis.set(
    `webhook:${deliveryId}`,
    '1',
    'EX', 86400,  // Expire after 24 hours
    'NX'          // Only set if not exists
  );

  if (!isNew) {
    console.log(`Duplicate: ${deliveryId}`);
    return res.status(200).send('OK');
  }

  // Process event
  await processEvent(req.body);

  res.status(200).send('OK');
}

Database Deduplication

db-dedup.js
async function handleWebhook(req, res) {
  const deliveryId = req.headers['x-webhook-delivery-id'];

  try {
    // Insert with unique constraint on delivery_id
    await db.query(`
      INSERT INTO webhook_deliveries (delivery_id, processed_at)
      VALUES ($1, NOW())
    `, [deliveryId]);
  } catch (err) {
    if (err.code === '23505') { // Unique violation
      console.log(`Duplicate: ${deliveryId}`);
      return res.status(200).send('OK');
    }
    throw err;
  }

  await processEvent(req.body);
  res.status(200).send('OK');
}

Async Processing

For reliable webhook handling, respond immediately with 200 and process events asynchronously. This prevents timeouts and ensures no events are lost.

Using a Job Queue (BullMQ)

bullmq-worker.js
const { Queue, Worker } = require('bullmq');

const webhookQueue = new Queue('webhooks');

// Endpoint: Queue the event and respond immediately
app.post('/webhooks/trix', async (req, res) => {
  if (!verifySignature(req)) {
    return res.status(401).send('Invalid');
  }

  // Add to queue
  await webhookQueue.add('process', {
    event: req.body,
    deliveryId: req.headers['x-webhook-delivery-id']
  });

  // Respond immediately
  res.status(200).send('OK');
});

// Worker: Process events from queue
const worker = new Worker('webhooks', async (job) => {
  const { event, deliveryId } = job.data;

  // Deduplicate
  if (await isProcessed(deliveryId)) {
    return;
  }

  // Process based on event type
  switch (event.event) {
    case 'memory.created':
      await syncToDatabase(event.data);
      break;
    case 'memory.deleted':
      await removeFromDatabase(event.data.id);
      break;
  }

  await markProcessed(deliveryId);
});

Using a queue provides automatic retries, dead letter handling, and visibility into processing status.

Configuring Retries

Customize retry behavior when creating or updating webhooks:

retry-config.json
{
  "name": "My Webhook",
  "url": "https://example.com/webhook",
  "events": ["memory.created"],
  "max_retries": 5,
  "retry_delay_seconds": 120
}
SettingDefaultRangeDescription
max_retries30-10Number of retry attempts after initial failure
retry_delay_seconds6010-3600Base delay between retries (exponential backoff)

Retry Behavior

  • Initial attempt fails → wait retry_delay_seconds
  • First retry fails → wait retry_delay_seconds * 2
  • Second retry fails → wait retry_delay_seconds * 4
  • And so on until max_retries is reached

Space-Scoped Webhooks

Limit events to a specific space by setting space_id:

space-scoped.json
{
  "name": "Project Alpha Webhook",
  "url": "https://example.com/webhook",
  "events": ["memory.created", "memory.updated"],
  "space_id": "spc_project_alpha"
}

This webhook will only receive events for memories in the "Project Alpha" space.

If space_id is not set, the webhook receives events from all spaces in your account.

Custom Headers

Add custom headers to webhook requests for authentication with your endpoint:

custom-headers.json
{
  "name": "Authenticated Webhook",
  "url": "https://api.example.com/webhook",
  "events": ["memory.created"],
  "headers": {
    "Authorization": "Bearer your-api-token",
    "X-Custom-Header": "custom-value"
  }
}

Testing Webhooks

Local Development

Use ngrok or similar tools to expose your local server:

Terminal
# Start your local server
node server.js

# In another terminal, expose it
ngrok http 3000

# Use the ngrok URL when creating your webhook
# https://abc123.ngrok.io/webhooks/trix

Test Endpoint

Use the test endpoint to verify your setup:

test-webhook.sh
curl -X POST https://api.trixdb.com/v1/webhooks/YOUR_WEBHOOK_ID/test \
  -H "Authorization: Bearer YOUR_API_KEY"

Manual Testing

Simulate a webhook delivery locally:

simulate.js
const crypto = require('crypto');

const secret = 'your_webhook_secret';
const timestamp = Math.floor(Date.now() / 1000).toString();
const payload = JSON.stringify({
  id: 'evt_test',
  event: 'memory.created',
  data: { id: 'mem_123', content: 'Test memory' }
});

const signature = crypto
  .createHmac('sha256', secret)
  .update(`${timestamp}.${payload}`)
  .digest('hex');

// Make request to your local endpoint
fetch('http://localhost:3000/webhooks/trix', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Webhook-Signature': signature,
    'X-Webhook-Timestamp': timestamp,
    'X-Webhook-Delivery-ID': 'test_123',
    'X-Webhook-Event': 'memory.created'
  },
  body: payload
});

Best Practices

  • Respond quickly (under 30 seconds) - Return 200 immediately, then process asynchronously. Long-running handlers may timeout.
  • Always verify signatures - Never trust unverified payloads. Check both signature and timestamp.
  • Handle duplicates - Use the delivery ID to deduplicate. Events may be delivered multiple times.
  • Use a queue for processing - For high-volume webhooks, queue events and process with workers.
  • Monitor failures - Check delivery history regularly. Set up alerts for repeated failures.
  • Rotate secrets periodically - Update webhook secrets and deploy new verification code before removing the old secret.
  • Use HTTPS only - Webhook URLs must use HTTPS for security.
  • Log everything - Log incoming webhooks (with sensitive data redacted) for debugging.

Troubleshooting

Common Issues

ProblemCauseSolution
Signature verification failsWrong secret or payload mismatchCheck secret matches, ensure raw body is used
Timestamp validation failsServer clock drift or old requestsSync server time with NTP
TimeoutsHandler takes too longProcess asynchronously, return 200 quickly
Missing eventsWrong event types subscribedCheck webhook configuration for event types
Duplicate processingRetries without deduplicationImplement idempotency using delivery ID

Debugging

debug.sh
# Check webhook configuration
curl https://api.trixdb.com/v1/webhooks/YOUR_WEBHOOK_ID \
  -H "Authorization: Bearer YOUR_API_KEY"

# View recent deliveries
curl "https://api.trixdb.com/v1/webhooks/YOUR_WEBHOOK_ID/deliveries?limit=10" \
  -H "Authorization: Bearer YOUR_API_KEY"

# Check for failed deliveries
curl "https://api.trixdb.com/v1/webhooks/YOUR_WEBHOOK_ID/deliveries?status=failed" \
  -H "Authorization: Bearer YOUR_API_KEY"