Webhooks

Beta

Receive real-time notifications for events in your account—usage alerts, payments, and more.

Overview

Webhooks allow your application to receive HTTP callbacks when specific events occur in your LLM Hub account. Instead of polling the API for changes, you'll receive instant notifications.

Note: Webhooks are currently in beta. Event types and payload formats may change.

Available Events

EventDescription
usage.thresholdUsage reaches configured threshold (e.g., 80%)
credits.lowCredit balance drops below threshold
credits.depletedCredit balance reaches zero
payment.completedPayment processed and credits added
payment.failedPayment attempt failed
api_key.createdNew API key created
api_key.revokedAPI key revoked or deleted
rate_limit.exceededRate limit exceeded (aggregated, not per-request)

Creating a Webhook

Register a webhook endpoint to start receiving events:

TypeScript
// Configure your webhook endpoint
// POST https://api.llmhub.dev/v1/webhooks

const webhook = await fetch('https://api.llmhub.dev/v1/webhooks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer your-api-key',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    url: 'https://your-app.com/api/llmhub-webhook',
    events: ['usage.threshold', 'payment.completed', 'api_key.created'],
    secret: 'your-webhook-secret'  // Used to sign payloads
  })
});

console.log(await webhook.json());
// {
//   "id": "wh_abc123",
//   "url": "https://your-app.com/api/llmhub-webhook",
//   "events": ["usage.threshold", "payment.completed", "api_key.created"],
//   "active": true,
//   "created_at": "2024-01-15T10:30:00Z"
// }

Verifying Signatures

Important: Always verify the webhook signature to ensure requests are from LLM Hub. We sign all webhook payloads with your secret using HMAC-SHA256.

TypeScript
import crypto from 'crypto';

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  
  const expected = Buffer.from(`sha256=${expectedSignature}`);
  const received = Buffer.from(signature);
  
  return crypto.timingSafeEqual(expected, received);
}

// In your webhook handler (Express example)
app.post('/api/llmhub-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-llmhub-signature'] as string;
  const payload = req.body.toString();
  
  if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  const event = JSON.parse(payload);
  
  // Handle the event
  switch (event.type) {
    case 'usage.threshold':
      console.log('Usage threshold reached:', event.data);
      break;
    case 'payment.completed':
      console.log('Payment received:', event.data);
      break;
    default:
      console.log('Unhandled event:', event.type);
  }
  
  res.status(200).json({ received: true });
});

Next.js Handler Example

Complete webhook handler for Next.js App Router:

TypeScript
// app/api/webhooks/llmhub/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';

function verifySignature(payload: string, signature: string, secret: string) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return signature === `sha256=${expected}`;
}

export async function POST(request: NextRequest) {
  const payload = await request.text();
  const signature = request.headers.get('x-llmhub-signature') || '';
  
  if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }
  
  const event = JSON.parse(payload);
  
  try {
    switch (event.type) {
      case 'usage.threshold':
        await handleUsageThreshold(event.data);
        break;
      case 'payment.completed':
        await handlePaymentCompleted(event.data);
        break;
      case 'credits.low':
        await handleLowCredits(event.data);
        break;
    }
    
    return NextResponse.json({ received: true });
  } catch (error) {
    console.error('Webhook error:', error);
    return NextResponse.json({ error: 'Handler failed' }, { status: 500 });
  }
}

async function handleUsageThreshold(data: { user_id: string; usage: number }) {
  // Send notification to user
  console.log(`User ${data.user_id} reached ${data.usage}% usage`);
}

async function handlePaymentCompleted(data: { amount: number; credits: number }) {
  // Update database, send receipt
  console.log(`Payment: ${data.amount} → ${data.credits} credits`);
}

async function handleLowCredits(data: { credits_remaining: number }) {
  // Send low balance alert
  console.log(`Low credits: ${data.credits_remaining} remaining`);
}

Event Payloads

All webhook events follow this structure:

json
// Example: usage.threshold event
{
  "id": "evt_abc123",
  "type": "usage.threshold",
  "created": "2024-01-15T10:30:00Z",
  "data": {
    "user_id": "user_xyz",
    "threshold": 80,
    "current_usage": 82,
    "period": "monthly"
  }
}

// Example: payment.completed event
{
  "id": "evt_def456",
  "type": "payment.completed",
  "created": "2024-01-15T11:00:00Z",
  "data": {
    "payment_id": "pay_123",
    "amount": 5000,
    "currency": "eur",
    "credits_added": 500000,
    "user_id": "user_xyz"
  }
}

// Example: credits.low event
{
  "id": "evt_ghi789",
  "type": "credits.low",
  "created": "2024-01-15T12:00:00Z",
  "data": {
    "user_id": "user_xyz",
    "credits_remaining": 10000,
    "threshold": 50000
  }
}

Managing Webhooks

List, update, or delete your webhooks:

TypeScript
// List all webhooks
const response = await fetch('https://api.llmhub.dev/v1/webhooks', {
  headers: { 'Authorization': 'Bearer your-api-key' }
});

const webhooks = await response.json();
// {
//   "data": [
//     {
//       "id": "wh_abc123",
//       "url": "https://your-app.com/api/webhook",
//       "events": ["usage.threshold", "payment.completed"],
//       "active": true
//     }
//   ]
// }

// Delete a webhook
await fetch('https://api.llmhub.dev/v1/webhooks/wh_abc123', {
  method: 'DELETE',
  headers: { 'Authorization': 'Bearer your-api-key' }
});

// Update a webhook
await fetch('https://api.llmhub.dev/v1/webhooks/wh_abc123', {
  method: 'PATCH',
  headers: {
    'Authorization': 'Bearer your-api-key',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    events: ['usage.threshold'],  // Update events
    active: false                  // Disable webhook
  })
});

Retry Policy

If your endpoint doesn't return a 2xx response, we'll retry the webhook:

  • Attempt 1: Immediate
  • Attempt 2: After 1 minute
  • Attempt 3: After 5 minutes
  • Attempt 4: After 30 minutes
  • Attempt 5: After 2 hours
  • Attempt 6: After 8 hours (final)

After all retries fail, the event is marked as failed. You can view failed events in the dashboard.

Best Practices

Always Verify Signatures

Never trust webhook payloads without verifying the HMAC signature. This prevents spoofed requests.

Respond Quickly

Return a 200 response within 30 seconds. Process events asynchronously if needed to avoid timeouts.

Handle Duplicates

Use the event id to deduplicate. The same event may be delivered multiple times during retries.

Use HTTPS

Webhook endpoints must use HTTPS in production to ensure payload confidentiality.

Monitor Your Endpoints

Set up alerts for webhook failures. Check the dashboard for failed deliveries.

Testing Webhooks

Use a Tunnel for Local Dev

Tools like ngrok or localtunnel expose your localhost to receive webhooks during development.

Test from Dashboard

Use the "Send Test Event" button in the webhook settings to trigger sample events.

Next Steps