⚡ You're viewing a live demo of ChimerAI. Data resets daily at midnight UTC.Get the CLI →

Webhooks

ChimerAI hat drei verschiedene Webhook-Konzepte — alle heißen "Webhook", meinen aber grundlegend verschiedene Dinge:

KonzeptRichtungFeatureSprache
Outbound Platform WebhooksDeine App → Kunden-Endpunktechimerai add webhooksTypeScript
AI Agent Webhook ToolsAI-Agent → Automation-Plattformenchimerai add ai-tools webhook_toolsPython
Incoming WebhooksExterne Services → Deine Appchimerai add billing / CustomTypeScript

1. Outbound Platform Webhooks (Enterprise Pro)

Das klassische Webhook-System — deine App benachrichtigt externe Endpunkte wenn Events passieren. Analog zu Stripe- oder GitHub-Webhooks.

Installation

chimerai add webhooks

Generierte Dateien:

lib/webhook-events.ts                      ← Event-Liste (hier eigene Events ergänzen)
lib/webhook-dispatcher.ts                  ← HMAC-Dispatch, Delivery-Log, Auto-Disable
lib/webhooks.ts                            ← fireWebhook() Helper
app/api/webhooks/route.ts                  ← GET Liste + POST Endpoint registrieren
app/api/webhooks/[id]/route.ts             ← PATCH Update + DELETE
app/api/webhooks/[id]/deliveries/route.ts  ← GET Delivery-History eines Endpunkts
app/api/webhooks/events/route.ts           ← SSE-Backend (für eigene UI-Erweiterungen)
app/dashboard/webhooks/page.tsx            ← Management UI mit Delivery-History

Custom Events

All available event types are defined in a single file — lib/webhook-events.ts. The dashboard event picker and the dispatcher filter both read from this list.

// lib/webhook-events.ts
export const WEBHOOK_EVENTS = [
  'user.created',
  'user.updated',
  'user.deleted',
  'billing.subscription_updated',
  'billing.payment_succeeded',
  // Add your own events here:
  // 'order.placed',
  // 'report.generated',
] as const;

export type WebhookEvent = (typeof WEBHOOK_EVENTS)[number] | '*';

To add a custom event:

  1. Add the event string to WEBHOOK_EVENTS in lib/webhook-events.ts
  2. Call fireWebhook(workspaceId, 'order.placed', { ... }) at the relevant place in your code
  3. Users can then subscribe to the new event via the dashboard at /dashboard/webhooks

The wildcard * is always available and matches every event regardless of the list.

Prisma Schema

model Webhook {
  id              String    @id @default(cuid())
  workspaceId     String
  url             String
  secret          String
  events          String              // JSON-Array als String (SQLite-kompatibel)
  active          Boolean   @default(true)
  description     String?
  failCount       Int       @default(0)
  lastTriggeredAt DateTime?
  createdAt       DateTime  @default(now())
  updatedAt       DateTime  @updatedAt
  deliveries      WebhookDelivery[]

  @@index([workspaceId])
  @@index([active])
}

model WebhookDelivery {
  id         String   @id @default(cuid())
  webhookId  String
  event      String
  payload    String
  statusCode Int
  success    Boolean
  response   String?
  createdAt  DateTime @default(now())
  webhook    Webhook  @relation(fields: [webhookId], references: [id], onDelete: Cascade)

  @@index([webhookId])
  @@index([success])
  @@index([createdAt])
}

Webhook-Event feuern

import { fireWebhook } from '@/lib/webhooks';

// session = the currently logged-in admin/user who registered the webhook
const session = await getServerSession(authOptions);
if (session?.user?.id) {
  await fireWebhook(session.user.id, 'user.created', {
    userId: newUser.id,   // the affected entity — goes into the payload
    email: newUser.email,
  });
}

workspaceId = wer den Webhook registriert hat, nicht wer betroffen ist.

Der erste Parameter von fireWebhook() ist die workspaceId. Der Dispatcher sucht damit alle Webhooks in der DB: WHERE workspaceId = ?. Da Webhooks über das Dashboard registriert werden, ist die workspaceId immer die ID des eingeloggten Admins (session.user.id) — nicht die ID des neu erstellten Users oder eines anderen Entities.

Wird die falsche ID übergeben, findet der Dispatcher keine Endpunkte und es passiert nichts — ohne Fehlermeldung.

Eingebaut: Event-Filter

Beim Registrieren eines Webhooks wählt der Nutzer, welche Events empfangen werden sollen:

user.created
user.updated
user.deleted
billing.subscription_updated
billing.payment_succeeded
*   ← alle Events

Der Dispatcher prüft automatisch: nur Endpunkte mit passendem Event oder * erhalten den Call.

Webhook Secret

Every registered endpoint gets a unique secret, generated automatically on creation:

whsec_<64 random hex characters>

The secret is returned only once — in the POST /api/webhooks response body. It is never exposed again via the list or detail endpoints. The receiver must store it securely (e.g. as an environment variable) immediately after creation.

The secret is never sent directly in the request. Instead it is used to compute an HMAC-SHA256 signature that travels in the X-ChimerAI-Signature header. Verifying this signature on the receiver side is strongly recommended — without it anyone could send fake events to the endpoint.

Signatur-Verifikation auf der Empfänger-Seite

Jeder ausgehende Request ist HMAC-SHA256-signiert:

X-ChimerAI-Signature: {timestamp}.{hmac}
X-ChimerAI-Event: user.created
X-ChimerAI-Timestamp: 1718000000

Verifikation im empfangenden Service:

import crypto from 'crypto';

export async function POST(req: Request) {
  const rawBody = await req.text();
  const sigHeader = req.headers.get('x-chimerai-signature') ?? '';
  const [tsStr, sig] = sigHeader.split('.');

  // Replay-Schutz: nicht älter als 5 Minuten
  if (Math.abs(Date.now() / 1000 - Number(tsStr)) > 300) {
    return new Response('Request too old', { status: 401 });
  }

  const expected = await signPayload(`${tsStr}.${rawBody}`, process.env.WEBHOOK_SECRET!);
  if (expected !== sig) {
    return new Response('Invalid signature', { status: 401 });
  }

  const event = JSON.parse(rawBody);
  console.log('Received:', event.event, event.data);
  return new Response('ok');
}

async function signPayload(payload: string, secret: string): Promise<string> {
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    'raw', encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
  );
  const mac = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));
  return Array.from(new Uint8Array(mac)).map(b => b.toString(16).padStart(2, '0')).join('');
}

Auto-Disable

Nach 10 aufeinanderfolgenden Fehlern (failCount >= MAX_FAILURES) wird der Endpunkt automatisch deaktiviert (active = false). Im Dashboard oder per PATCH /api/webhooks/[id] mit { "active": true } wieder aktivieren — failCount wird dabei auf 0 zurückgesetzt.

Typische Integration-Punkte

// app/api/admin/users/route.ts — neuer User
const session = await getServerSession(authOptions);
if (session?.user?.id) {
  await fireWebhook(session.user.id, 'user.created', { userId: user.id, email: user.email });
}

// app/api/billing/webhook/route.ts — Abo-Update
await fireWebhook(userId, 'billing.subscription_updated', { plan: newPlan, previousPlan });

// app/api/v1/chat/stream/route.ts — Conversation gestartet
await fireWebhook(session.user.id, 'conversation.started', { conversationId, model });

2. AI Agent Webhook Tools

Der AI-Agent kann externe Automation-Plattformen als Tool aufrufen — n8n, Zapier, Make.com, Slack. Das ist kein Notification-System, sondern ein Agent-Tool: der Agent entscheidet während der Inferenz, wann und welchen Endpunkt er aufruft.

Installation

chimerai add ai-tools webhook_tools

Generierte Datei:

services/ai/services/tools/webhook_tools.py

Keine zusätzlichen Python-Dependencies — nutzt httpx, das bereits im AI-Service-Core enthalten ist.

Verfügbare Methoden

call_webhook — universeller HTTP-Caller

result = await webhook_tools.call_webhook(
    url="https://myapp.com/api/notify",
    payload={"event": "order_placed", "orderId": "123"},
    method="POST",
    auth_token="my-secret-token"
)
# { "success": True, "status_code": 200, "response": {...}, "url": "...", "method": "POST" }

Unterstützt GET, POST, PUT, DELETE, PATCH. Timeout: 30 Sekunden. Gibt immer ein Dict zurück — keine Exceptions bei HTTP-Fehlern.

call_n8n_webhook

result = await webhook_tools.call_n8n_webhook(
    webhook_id="my-workflow-id",
    payload={"user": "Alice", "action": "signup"}
)

Base-URL aus Env-Variable N8N_WEBHOOK_URL:

N8N_WEBHOOK_URL=https://n8n.mycompany.com

call_zapier_webhook

result = await webhook_tools.call_zapier_webhook(
    hook_id="abc123/xyz456",
    payload={"name": "Alice", "email": "alice@example.com"}
)

call_make_webhook

result = await webhook_tools.call_make_webhook(
    webhook_path="abc123def456",
    payload={"trigger": "new_user"},
    region="eu1"   # default: eu1
)

notify_slack_via_webhook

result = await webhook_tools.notify_slack_via_webhook(
    webhook_url="https://hooks.slack.com/services/T.../B.../xxx",
    message="New signup: Alice (alice@example.com)",
    channel="#alerts",
    username="ChimerAI Bot",
    icon_emoji=":robot_face:"
)

Slack Incoming Webhook erstellen: https://api.slack.com/apps → deine App → "Incoming Webhooks".

AI-Service-Routen

Der AI-Service exponiert diese Routen für den Agent:

RouteMethodeFunktion
/webhook/callPOSTGenerischer HTTP-Call
/webhook/n8nPOSTn8n Workflow triggern
/webhook/zapierPOSTZapier Zap triggern
/webhook/makePOSTMake Scenario triggern
/webhook/slackPOSTSlack Nachricht senden

Unterschied zu Platform Webhooks

Platform WebhooksAI Agent Tools
AuslöserBusiness-Event im CodeAgent-Entscheidung während Inferenz
EmpfängerKunden-registrierte URLsFest konfigurierte Automation-Plattformen
SigningHMAC-SHA256Keins (optionaler Auth-Token)
PersistenzDelivery-Log in DBKein Log

3. Incoming Webhooks

Externe Services rufen deine App auf — deine App empfängt und verarbeitet die Events.

Stripe (via chimerai add billing)

Installiert unter app/api/billing/webhook/route.ts. Stripe-Signatur-Verifikation ist eingebaut.

Registrierung im Stripe Dashboard:

  • URL: https://yourdomain.com/api/billing/webhook
  • Events: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.paid, invoice.payment_failed
STRIPE_WEBHOOK_SECRET=whsec_...

Lokal mit Stripe CLI:

stripe listen --forward-to localhost:3000/api/billing/webhook

Verarbeitete Events:

EventAktion
checkout.session.completedSubscription aktivieren
customer.subscription.updatedStatus + Ablaufdatum synchronisieren
customer.subscription.deletedAls gekündigt markieren
invoice.paidCredit-Guthaben aufladen
invoice.payment_failedWarning loggen

Custom Incoming Webhook

Für andere externe Services (GitHub, Shopify, etc.):

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

export async function POST(request: NextRequest) {
  const rawBody = await request.text();
  const signature = request.headers.get('x-hub-signature-256') ?? '';

  // Signature verifizieren
  const expected = `sha256=${crypto
    .createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET!)
    .update(rawBody)
    .digest('hex')}`;

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  const event = JSON.parse(rawBody);
  const eventType = request.headers.get('x-github-event');

  if (eventType === 'pull_request' && event.action === 'opened') {
    // PR geöffnet — z.B. Notification feuern
  }

  return NextResponse.json({ received: true });
}

Weiterführend

ChimerAI Docs · Back to Demo