Webhooks
Receive real-time notifications when events occur in your NovaKit account
Webhooks
Webhooks allow you to receive real-time HTTP notifications when events occur in your NovaKit account. Instead of polling for changes, webhooks push data to your server automatically.
Webhooks are available on Pro and higher plans. Check your plan limits for webhook quotas.
How Webhooks Work
- You create a webhook endpoint in your application
- You register that URL in NovaKit Dashboard → Settings → Webhooks
- When an event occurs, NovaKit sends a POST request to your URL
- Your server processes the event and returns a 2xx response
Webhook Events
AI Agents
| Event | Description |
|---|---|
agent.run.started | An agent run has started |
agent.run.completed | An agent run completed successfully |
agent.run.failed | An agent run failed with an error |
Generations
| Event | Description |
|---|---|
generation.started | A generation job started (image, video, music, etc.) |
generation.completed | A generation job completed |
generation.failed | A generation job failed |
Chat
| Event | Description |
|---|---|
chat.message.created | A new chat message was created |
chat.conversation.created | A new conversation was started |
Billing
| Event | Description |
|---|---|
subscription.created | A new subscription was created |
subscription.updated | Subscription plan was changed |
subscription.cancelled | Subscription was cancelled |
credits.low | Credits are below 10% remaining |
credits.depleted | Credits are fully exhausted |
Team
| Event | Description |
|---|---|
team.member.added | A team member was added |
team.member.removed | A team member was removed |
team.member.role_changed | A team member's role was changed |
Webhook Payload
All webhook payloads follow this structure:
{
"id": "evt_abc123xyz",
"type": "generation.completed",
"timestamp": "2025-01-15T10:30:00Z",
"data": {
"jobId": "job_abc123",
"tool": "image",
"model": "fal-ai/flux/dev",
"outputUrl": "https://fal.media/files/...",
"durationMs": 3200,
"unitsUsed": 1,
"unitType": "image_generations"
},
"orgId": "org_xyz789"
}Event-Specific Payloads
agent.run.started
{
"runId": "run_abc123",
"agentId": "agent_xyz",
"agentName": "Customer Support Bot",
"input": "Help me reset my password"
}agent.run.completed
{
"runId": "run_abc123",
"agentId": "agent_xyz",
"agentName": "Customer Support Bot",
"input": "Help me reset my password",
"output": "I can help you reset your password...",
"totalSteps": 3,
"durationMs": 4500,
"unitsUsed": 1,
"unitType": "agent_runs"
}agent.run.failed
{
"runId": "run_abc123",
"agentId": "agent_xyz",
"agentName": "Customer Support Bot",
"input": "Help me reset my password",
"errorMessage": "Rate limit exceeded"
}generation.started
{
"jobId": "job_abc123",
"tool": "video",
"model": "fal-ai/minimax/video-01"
}generation.completed
{
"jobId": "job_abc123",
"tool": "video",
"model": "fal-ai/minimax/video-01",
"outputUrl": "https://fal.media/files/video.mp4",
"durationMs": 45000,
"unitsUsed": 5,
"unitType": "video_seconds"
}generation.failed
{
"jobId": "job_abc123",
"tool": "video",
"model": "fal-ai/minimax/video-01",
"errorMessage": "Content policy violation"
}credits.low
{
"bucketType": "chat_tokens",
"remaining": 8500,
"limit": 100000,
"percentUsed": 91.5
}subscription.updated
{
"subscriptionId": "sub_abc123",
"planCode": "pro_monthly",
"planName": "Pro Monthly",
"status": "active"
}Security
Signature Verification
All webhook requests are signed using HMAC-SHA256. Always verify the signature to ensure requests are from NovaKit.
Signature Headers:
| Header | Description |
|---|---|
x-novakit-signature | HMAC-SHA256 signature |
x-novakit-timestamp | Unix timestamp of request |
x-novakit-event-id | Unique event ID (for deduplication) |
x-novakit-event-type | Event type (e.g., generation.completed) |
Verifying Signatures
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
timestamp: string,
secret: string
): boolean {
const signedPayload = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-novakit-signature'] as string;
const timestamp = req.headers['x-novakit-timestamp'] as string;
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, timestamp, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(payload);
// Process event...
res.status(200).json({ received: true });
});import hmac
import hashlib
from flask import Flask, request, jsonify
def verify_webhook_signature(payload: bytes, signature: str, timestamp: str, secret: str) -> bool:
signed_payload = f"{timestamp}:{payload.decode()}"
expected = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('x-novakit-signature')
timestamp = request.headers.get('x-novakit-timestamp')
payload = request.get_data()
if not verify_webhook_signature(payload, signature, timestamp, WEBHOOK_SECRET):
return jsonify({'error': 'Invalid signature'}), 401
event = request.get_json()
# Process event...
return jsonify({'received': True}), 200package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
)
func verifySignature(payload, signature, timestamp, secret string) bool {
signedPayload := fmt.Sprintf("%s:%s", timestamp, payload)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signedPayload))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("x-novakit-signature")
timestamp := r.Header.Get("x-novakit-timestamp")
payload, _ := io.ReadAll(r.Body)
if !verifySignature(string(payload), signature, timestamp, webhookSecret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process event...
w.WriteHeader(http.StatusOK)
}Timestamp Validation
Reject requests with timestamps older than 5 minutes to prevent replay attacks:
const timestamp = parseInt(headers['x-novakit-timestamp']);
const now = Math.floor(Date.now() / 1000);
const FIVE_MINUTES = 300;
if (Math.abs(now - timestamp) > FIVE_MINUTES) {
return res.status(401).json({ error: 'Request too old' });
}Retry Policy
If your endpoint doesn't return a 2xx status code, NovaKit will retry the webhook:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 1 hour |
After 5 failed attempts, the webhook delivery is marked as failed.
Auto-Disable: If a webhook fails consistently (10+ consecutive failures), it will be automatically disabled. Re-enable it in the dashboard after fixing your endpoint.
Event Deduplication
Use the x-novakit-event-id header to deduplicate events. Store processed event IDs and skip duplicates:
const eventId = req.headers['x-novakit-event-id'];
// Check if already processed
const alreadyProcessed = await redis.get(`webhook:${eventId}`);
if (alreadyProcessed) {
return res.status(200).json({ received: true, duplicate: true });
}
// Process event
await processEvent(event);
// Mark as processed (expire after 24 hours)
await redis.set(`webhook:${eventId}`, '1', 'EX', 86400);Creating Webhooks
Via Dashboard
- Go to Dashboard → Settings → Integrations → Webhooks
- Click Create Webhook
- Enter your endpoint URL (must be HTTPS)
- Select the events you want to receive
- Optionally add custom headers
- Copy the signing secret and store it securely
Via API
curl -X POST https://www.novakit.ai/api/v1/webhooks \
-H "Authorization: Bearer sk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Production Webhook",
"url": "https://api.yourapp.com/novakit-webhook",
"events": ["generation.completed", "generation.failed"],
"enabled": true
}'Best Practices
1. Respond Quickly
Return a 200 response immediately and process asynchronously:
app.post('/webhook', async (req, res) => {
// Verify signature first
if (!verifySignature(req)) {
return res.status(401).send();
}
// Respond immediately
res.status(200).json({ received: true });
// Process asynchronously
await queue.add('process-webhook', req.body);
});2. Use Idempotent Processing
Handle the same event being delivered multiple times gracefully.
3. Log Everything
Keep detailed logs of webhook deliveries for debugging:
console.log({
eventId: req.headers['x-novakit-event-id'],
eventType: req.headers['x-novakit-event-type'],
timestamp: new Date().toISOString(),
payload: req.body
});4. Monitor Failures
Set up alerts for webhook failures in your monitoring system.
5. Use HTTPS
Always use HTTPS endpoints. HTTP endpoints are not supported.
Testing Webhooks
Using the Dashboard
- Go to your webhook settings
- Click Send Test Event
- Select an event type
- Review the request/response in the delivery logs
Using cURL
Test your endpoint locally with a sample payload:
curl -X POST http://localhost:3000/webhook \
-H "Content-Type: application/json" \
-H "x-novakit-signature: test" \
-H "x-novakit-timestamp: $(date +%s)" \
-H "x-novakit-event-id: test_123" \
-H "x-novakit-event-type: generation.completed" \
-d '{
"id": "evt_test123",
"type": "generation.completed",
"timestamp": "2025-01-15T10:30:00Z",
"data": {
"jobId": "job_abc123",
"tool": "image",
"model": "fal-ai/flux/dev"
}
}'Webhook Limits by Plan
| Plan | Max Webhooks | Events | Custom Headers |
|---|---|---|---|
| Free | 0 | - | - |
| Pro | 5 | All | Yes |
| Business | 20 | All | Yes |
| CLI Team | 20 | All | Yes |