Skip to main content

Overview

Raily webhooks notify your application in real-time when events occur, such as content access, policy violations, or analytics thresholds. Use webhooks to build reactive integrations, logging systems, and automated workflows.

Setting Up Webhooks

Create a Webhook

const webhook = await raily.webhooks.create({
  url: "https://api.example.com/raily/events",
  events: [
    "access.granted",
    "access.denied",
    "content.created",
    "content.updated"
  ],
  secret: "whsec_your_secret_key",  // For signature verification
  metadata: {
    environment: "production"
  }
});

console.log(`Webhook created: ${webhook.id}`);

Via Dashboard

1

Navigate to Webhooks

Go to Settings > Webhooks in your dashboard.
2

Add Endpoint

Click Add Endpoint and enter your URL.
3

Select Events

Choose which events to receive.
4

Copy Secret

Save the webhook secret for signature verification.

Event Types

Access Events

EventDescription
access.requestedAccess check initiated
access.grantedAccess approved by policy
access.deniedAccess blocked by policy
access.rate_limitedRequest exceeded rate limit

Content Events

EventDescription
content.createdNew content registered
content.updatedContent metadata updated
content.archivedContent archived
content.deletedContent permanently deleted
content.accessedContent successfully retrieved

Policy Events

EventDescription
policy.createdNew policy created
policy.updatedPolicy rules modified
policy.deletedPolicy removed

Analytics Events

EventDescription
analytics.thresholdCustom threshold exceeded
analytics.daily_summaryDaily analytics digest
analytics.weekly_summaryWeekly analytics digest

Webhook Payload

All webhooks follow a consistent structure:
{
  "id": "evt_abc123xyz",
  "type": "access.granted",
  "created": "2024-01-15T10:30:00Z",
  "data": {
    "contentId": "cnt_xyz789",
    "requesterId": "partner_openai",
    "permissions": ["full_access", "inference"],
    "rateLimit": {
      "remaining": 999,
      "resetAt": "2024-01-15T11:00:00Z"
    }
  },
  "metadata": {
    "environment": "production"
  }
}

Event-Specific Payloads

{
  "id": "evt_abc123",
  "type": "access.granted",
  "created": "2024-01-15T10:30:00Z",
  "data": {
    "contentId": "cnt_xyz789",
    "contentTitle": "AI Report 2024",
    "requesterId": "partner_openai",
    "requesterName": "OpenAI",
    "permissions": ["full_access", "inference"],
    "policyId": "pol_enterprise",
    "policyName": "Enterprise Access",
    "matchedRule": {
      "priority": 1,
      "action": "allow"
    },
    "context": {
      "purpose": "rag",
      "model": "gpt-4"
    },
    "rateLimit": {
      "remaining": 999,
      "limit": 1000,
      "resetAt": "2024-01-15T11:00:00Z"
    }
  }
}

Handling Webhooks

Express.js Example

import express from 'express';
import crypto from 'crypto';

const app = express();

// Use raw body for signature verification
app.use('/webhooks/raily', express.raw({ type: 'application/json' }));

app.post('/webhooks/raily', (req, res) => {
  // Verify signature
  const signature = req.headers['x-raily-signature'];
  const timestamp = req.headers['x-raily-timestamp'];

  const payload = `${timestamp}.${req.body.toString()}`;
  const expectedSignature = crypto
    .createHmac('sha256', process.env.RAILY_WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');

  if (signature !== `sha256=${expectedSignature}`) {
    console.error('Invalid webhook signature');
    return res.status(401).send('Invalid signature');
  }

  // Check timestamp to prevent replay attacks (5 minute tolerance)
  const eventTime = parseInt(timestamp) * 1000;
  if (Math.abs(Date.now() - eventTime) > 300000) {
    return res.status(401).send('Timestamp too old');
  }

  // Parse and handle event
  const event = JSON.parse(req.body.toString());

  switch (event.type) {
    case 'access.granted':
      handleAccessGranted(event.data);
      break;
    case 'access.denied':
      handleAccessDenied(event.data);
      break;
    case 'content.created':
      handleContentCreated(event.data);
      break;
    default:
      console.log(`Unhandled event: ${event.type}`);
  }

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

function handleAccessGranted(data) {
  console.log(`Access granted: ${data.contentId} to ${data.requesterId}`);
  // Log to analytics, update dashboards, etc.
}

function handleAccessDenied(data) {
  console.log(`Access denied: ${data.contentId} - ${data.reason}`);
  // Alert security team if suspicious
  if (data.reason === 'policy_blocked') {
    alertSecurityTeam(data);
  }
}

function handleContentCreated(data) {
  console.log(`New content: ${data.title}`);
  // Update search index, notify team, etc.
}

Python Flask Example

from flask import Flask, request, abort
import hmac
import hashlib
import time
import json

app = Flask(__name__)

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

    payload = f"{timestamp}.{request.data.decode()}"
    expected = hmac.new(
        os.environ['RAILY_WEBHOOK_SECRET'].encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()

    if signature != f"sha256={expected}":
        abort(401, 'Invalid signature')

    # Check timestamp
    event_time = int(timestamp)
    if abs(time.time() - event_time) > 300:
        abort(401, 'Timestamp too old')

    # Handle event
    event = json.loads(request.data)

    handlers = {
        'access.granted': handle_access_granted,
        'access.denied': handle_access_denied,
        'content.created': handle_content_created,
    }

    handler = handlers.get(event['type'])
    if handler:
        handler(event['data'])

    return 'OK', 200

def handle_access_granted(data):
    print(f"Access granted: {data['contentId']}")

def handle_access_denied(data):
    print(f"Access denied: {data['reason']}")

def handle_content_created(data):
    print(f"New content: {data['title']}")

Using the SDK Webhook Helper

import { verifyWebhook } from '@raily/sdk';

app.post('/webhooks/raily', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const event = verifyWebhook(
      req.body,
      req.headers['x-raily-signature'],
      req.headers['x-raily-timestamp'],
      process.env.RAILY_WEBHOOK_SECRET
    );

    // Event is verified, handle it
    handleEvent(event);
    res.status(200).send('OK');

  } catch (error) {
    console.error('Webhook verification failed:', error.message);
    res.status(401).send('Invalid webhook');
  }
});

Webhook Management

List Webhooks

const webhooks = await raily.webhooks.list();

webhooks.forEach(webhook => {
  console.log(`${webhook.id}: ${webhook.url}`);
  console.log(`  Events: ${webhook.events.join(', ')}`);
  console.log(`  Status: ${webhook.status}`);
});

Update Webhook

await raily.webhooks.update('wh_abc123', {
  events: ['access.granted', 'access.denied', 'analytics.threshold'],
  enabled: true
});

Delete Webhook

await raily.webhooks.delete('wh_abc123');

View Delivery Logs

const logs = await raily.webhooks.logs('wh_abc123', {
  limit: 100,
  status: 'failed'  // or 'success', 'all'
});

logs.forEach(log => {
  console.log(`${log.eventId}: ${log.status} - ${log.responseCode}`);
  if (log.error) {
    console.log(`  Error: ${log.error}`);
  }
});

Retry Failed Deliveries

// Retry a specific delivery
await raily.webhooks.retry('wh_abc123', 'delivery_xyz789');

// Retry all failed deliveries in last 24h
await raily.webhooks.retryFailed('wh_abc123', {
  since: '24h'
});

Best Practices

Verify Signatures

Always verify webhook signatures to ensure requests come from Raily.

Respond Quickly

Return 200 status immediately. Process events asynchronously.

Handle Retries

Implement idempotency. Raily retries failed deliveries.

Monitor Failures

Set up alerts for webhook delivery failures.

Async Processing Pattern

import Queue from 'bull';

const eventQueue = new Queue('raily-events', process.env.REDIS_URL);

// Webhook endpoint - responds immediately
app.post('/webhooks/raily', express.raw({ type: 'application/json' }), async (req, res) => {
  const event = verifyWebhook(req.body, req.headers, process.env.WEBHOOK_SECRET);

  // Queue for async processing
  await eventQueue.add(event, {
    attempts: 3,
    backoff: { type: 'exponential', delay: 1000 }
  });

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

// Process events asynchronously
eventQueue.process(async (job) => {
  const event = job.data;

  switch (event.type) {
    case 'access.granted':
      await logToAnalytics(event.data);
      await updateDashboard(event.data);
      break;
    case 'access.denied':
      await logSecurityEvent(event.data);
      await checkForSuspiciousActivity(event.data);
      break;
  }
});

Idempotency

const processedEvents = new Set();  // Use Redis in production

app.post('/webhooks/raily', async (req, res) => {
  const event = verifyWebhook(req.body, req.headers, process.env.WEBHOOK_SECRET);

  // Check if already processed
  if (processedEvents.has(event.id)) {
    console.log(`Duplicate event: ${event.id}`);
    return res.status(200).send('OK');
  }

  // Process event
  await handleEvent(event);

  // Mark as processed
  processedEvents.add(event.id);

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

Retry Policy

Raily automatically retries failed webhook deliveries:
AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry24 hours
After 5 failed attempts, the webhook is marked as failed and you’ll receive an email notification.

Testing Webhooks

Send Test Event

// Send a test event to your endpoint
await raily.webhooks.test('wh_abc123', {
  eventType: 'access.granted'
});

Local Development

Use a tunneling service for local development:
# Using ngrok
ngrok http 3000

# Use the ngrok URL for your webhook
# https://abc123.ngrok.io/webhooks/raily

Webhook Simulator

// Simulate webhook locally for testing
import { simulateWebhook } from '@raily/sdk/testing';

const mockEvent = simulateWebhook({
  type: 'access.granted',
  data: {
    contentId: 'cnt_test',
    requesterId: 'test_partner'
  },
  secret: 'test_secret'
});

// mockEvent includes headers and body for testing
// {
//   headers: { 'x-raily-signature': '...', 'x-raily-timestamp': '...' },
//   body: '{"id":"evt_...","type":"access.granted",...}'
// }

Next Steps