NovaKitv1.0

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

  1. You create a webhook endpoint in your application
  2. You register that URL in NovaKit Dashboard → Settings → Webhooks
  3. When an event occurs, NovaKit sends a POST request to your URL
  4. Your server processes the event and returns a 2xx response

Webhook Events

AI Agents

EventDescription
agent.run.startedAn agent run has started
agent.run.completedAn agent run completed successfully
agent.run.failedAn agent run failed with an error

Generations

EventDescription
generation.startedA generation job started (image, video, music, etc.)
generation.completedA generation job completed
generation.failedA generation job failed

Chat

EventDescription
chat.message.createdA new chat message was created
chat.conversation.createdA new conversation was started

Billing

EventDescription
subscription.createdA new subscription was created
subscription.updatedSubscription plan was changed
subscription.cancelledSubscription was cancelled
credits.lowCredits are below 10% remaining
credits.depletedCredits are fully exhausted

Team

EventDescription
team.member.addedA team member was added
team.member.removedA team member was removed
team.member.role_changedA 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:

HeaderDescription
x-novakit-signatureHMAC-SHA256 signature
x-novakit-timestampUnix timestamp of request
x-novakit-event-idUnique event ID (for deduplication)
x-novakit-event-typeEvent 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}), 200
package 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:

AttemptDelay
1Immediate
230 seconds
32 minutes
410 minutes
51 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

  1. Go to Dashboard → Settings → Integrations → Webhooks
  2. Click Create Webhook
  3. Enter your endpoint URL (must be HTTPS)
  4. Select the events you want to receive
  5. Optionally add custom headers
  6. 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

  1. Go to your webhook settings
  2. Click Send Test Event
  3. Select an event type
  4. 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

PlanMax WebhooksEventsCustom Headers
Free0--
Pro5AllYes
Business20AllYes
CLI Team20AllYes

On this page