Webhooks
ChimerAI hat drei verschiedene Webhook-Konzepte — alle heißen "Webhook", meinen aber grundlegend verschiedene Dinge:
| Konzept | Richtung | Feature | Sprache |
|---|---|---|---|
| Outbound Platform Webhooks | Deine App → Kunden-Endpunkte | chimerai add webhooks | TypeScript |
| AI Agent Webhook Tools | AI-Agent → Automation-Plattformen | chimerai add ai-tools webhook_tools | Python |
| Incoming Webhooks | Externe Services → Deine App | chimerai add billing / Custom | TypeScript |
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:
- Add the event string to
WEBHOOK_EVENTSinlib/webhook-events.ts - Call
fireWebhook(workspaceId, 'order.placed', { ... })at the relevant place in your code - 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 dieworkspaceId. Der Dispatcher sucht damit alle Webhooks in der DB:WHERE workspaceId = ?. Da Webhooks über das Dashboard registriert werden, ist dieworkspaceIdimmer 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:
| Route | Methode | Funktion |
|---|---|---|
/webhook/call | POST | Generischer HTTP-Call |
/webhook/n8n | POST | n8n Workflow triggern |
/webhook/zapier | POST | Zapier Zap triggern |
/webhook/make | POST | Make Scenario triggern |
/webhook/slack | POST | Slack Nachricht senden |
Unterschied zu Platform Webhooks
| Platform Webhooks | AI Agent Tools | |
|---|---|---|
| Auslöser | Business-Event im Code | Agent-Entscheidung während Inferenz |
| Empfänger | Kunden-registrierte URLs | Fest konfigurierte Automation-Plattformen |
| Signing | HMAC-SHA256 | Keins (optionaler Auth-Token) |
| Persistenz | Delivery-Log in DB | Kein 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:
| Event | Aktion |
|---|---|
checkout.session.completed | Subscription aktivieren |
customer.subscription.updated | Status + Ablaufdatum synchronisieren |
customer.subscription.deleted | Als gekündigt markieren |
invoice.paid | Credit-Guthaben aufladen |
invoice.payment_failed | Warning 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
- Billing Guide — Stripe-Integration im Detail
- AI Tools Guide — alle verfügbaren AI-Agent-Tools
- HMAC-Authentifizierung