Skip to main content
Webhooks let you receive HTTP notifications when video generation jobs reach a terminal state, eliminating the need to poll the jobs endpoint. When a job completes, fails, or partially completes, Nouvel sends a POST request to your registered URL with the job details.

How It Works

1

Register a Webhook URL

Include a webhookUrl in your POST /api/v1/generate request body. The URL is stored with the job and will receive a notification when the job finishes.
2

Job Runs

Video generation proceeds as normal. You can still poll the jobs endpoint if desired — webhooks and polling are not mutually exclusive.
3

Receive Notification

When all projects in the job reach a terminal state (completed, partial, or failed), Nouvel sends a POST to your webhook URL with the full job details.

Registration

Register a webhook by including webhookUrl in your generate request:
curl -X POST https://app.nouvel.ai/api/v1/generate \
  -H "Authorization: Bearer nvl_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": ["https://example.com/products/protein-powder"],
    "variantCount": 2,
    "webhookUrl": "https://your-server.com/webhooks/nouvel"
  }'

URL Requirements

Your webhook URL must meet the following requirements:
RequirementDetails
ProtocolHTTPS only (HTTP is rejected)
No localhostlocalhost, 127.0.0.1, 0.0.0.0, ::1 are rejected
No private IPs10.*, 192.168.*, 172.16-31.* ranges are rejected
Publicly accessibleYour server must be reachable from the public internet
If the webhook URL fails validation, the generate request returns a 400 error and no job is created.

Webhook Payload

When a job reaches a terminal state, Nouvel sends the following POST request to your URL:
POST https://your-server.com/webhooks/nouvel
Content-Type: application/json
X-Nouvel-Signature: a1b2c3d4e5f6...

{
  "event": "job.completed",
  "jobId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "completed",
  "totalProjects": 2,
  "completedProjects": 2,
  "failedProjects": 0,
  "projects": [
    {
      "projectId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "title": "Protein Powder Ad - Variant 1",
      "status": "completed",
      "videoUrl": "https://storage.supabase.co/final-xxx.mp4"
    },
    {
      "projectId": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
      "title": "Protein Powder Ad - Variant 2",
      "status": "completed",
      "videoUrl": "https://storage.supabase.co/final-yyy.mp4"
    }
  ]
}

Payload Fields

event
string
required
Event type. Currently always job.completed (covers completed, partial, and failed terminal states).
jobId
string
required
UUID of the generation job.
status
string
required
Terminal job status. One of:
  • completed — all projects finished successfully
  • partial — some projects completed, some failed
  • failed — all projects failed
totalProjects
integer
required
Total number of projects in the job.
completedProjects
integer
required
Number of projects that completed successfully.
failedProjects
integer
required
Number of projects that failed.
projects
array
required
Details for each project in the job.

Signature Verification

Every webhook request includes an X-Nouvel-Signature header containing an HMAC-SHA256 signature of the request body. Always verify this signature to ensure the request is genuinely from Nouvel.
import crypto from 'crypto';

function verifyWebhookSignature(
  body: string,
  signature: string,
  secret: string
): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// In your webhook handler:
app.post('/webhooks/nouvel', (req, res) => {
  const signature = req.headers['x-nouvel-signature'] as string;
  const rawBody = JSON.stringify(req.body);

  if (!verifyWebhookSignature(rawBody, signature, process.env.WEBHOOK_SECRET!)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Process the webhook...
  const { event, jobId, status, projects } = req.body;
  console.log(`Job ${jobId}: ${status} (${projects.length} projects)`);

  res.status(200).json({ received: true });
});
The signing secret is the WEBHOOK_SIGNING_SECRET environment variable configured on the Nouvel server. Contact support@nouvel.ai to obtain your webhook signing secret for signature verification.

Delivery Behavior

PropertyDetails
MethodPOST
Content-Typeapplication/json
Timeout5 seconds — your server must respond within 5 seconds
RetriesNone (v1) — if delivery fails, the webhook is not retried
Rate limitsWebhook deliveries do not count toward your API rate limit
Since webhooks are not retried in v1, we recommend using both webhooks and periodic polling as a fallback. Poll the jobs endpoint every few minutes to catch any missed webhook deliveries.

Timing

The webhook fires when the job status endpoint detects that a job has transitioned to a terminal state. This happens during a status poll — not at the exact moment of completion. In practice, this means:
  • If you’re polling the job status endpoint, the webhook fires during one of your polls
  • If you’re not polling, the webhook fires when any poll (including internal monitoring) checks the job
For the fastest notification, combine webhooks with periodic polling. The webhook will typically arrive within seconds of job completion if you’re actively polling.

Best Practices

Your webhook endpoint should return a 200 status code within 5 seconds. If you need to do heavy processing, accept the webhook immediately and process asynchronously.
app.post('/webhooks/nouvel', async (req, res) => {
  // Respond immediately
  res.status(200).json({ received: true });

  // Process asynchronously
  processWebhook(req.body).catch(console.error);
});
Never trust webhook payloads without verifying the X-Nouvel-Signature header. This prevents attackers from sending fake webhook events to your endpoint.
Design your webhook handler to be idempotent. While webhooks are not retried in v1, future versions may add retry logic. Use the jobId as a deduplication key.
Since v1 webhooks have no retry mechanism, implement periodic polling of the jobs endpoint as a safety net. Check for completed jobs that may have been missed due to webhook delivery failures.

Example: Full Integration

Here’s a complete example combining webhook registration with a handler:
// 1. Submit generation with webhook
const job = await fetch('https://app.nouvel.ai/api/v1/generate', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.NOUVEL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    urls: ['https://example.com/products/protein-powder'],
    variantCount: 2,
    webhookUrl: 'https://your-server.com/webhooks/nouvel',
  }),
}).then(r => r.json());

console.log(`Job ${job.jobId} started. Waiting for webhook...`);

// 2. Handle webhook (Express server)
import express from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.json());

app.post('/webhooks/nouvel', (req, res) => {
  // Verify signature
  const sig = req.headers['x-nouvel-signature'] as string;
  const expected = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(JSON.stringify(req.body)).digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).send('Invalid signature');
  }

  const { jobId, status, projects } = req.body;

  // Process completed videos
  for (const project of projects) {
    if (project.status === 'completed') {
      console.log(`Video ready: ${project.videoUrl}`);
      // Download, publish, notify your users, etc.
    }
  }

  res.status(200).json({ received: true });
});

app.listen(3000);