> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runblob.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Veo3 webhooks for video events | Runblob API

> Receive instant HTTPS callbacks when Veo3 video generations complete, fail, or timeout. Skip polling, verify signatures, and ship faster pipelines.

<Info>
  Webhooks deliver completion, failure, and timeout events to your endpoint so you don't have to poll.
</Info>

## Setup Guide

<Steps>
  <Step title="Create Webhook Endpoint">
    Set up an HTTPS endpoint that can receive POST requests.

    ```python theme={null} theme={"system"}
    from flask import Flask, request, jsonify

    app = Flask(__name__)

    @app.route('/webhook', methods=['POST'])
    def handle_webhook():
        data = request.json
        
        if data['status'] == 'completed':
            print(f"Video ready: {data['video_url']}")
        else:
            error_msg = data.get('error', {}).get('message', 'Unknown error')
            print(f"Generation failed: {error_msg}")
            
        return jsonify({'status': 'received'}), 200
    ```
  </Step>

  <Step title="Add Webhook URL">
    Include your webhook URL when creating a generation task.

    ```bash theme={null} theme={"system"}
    curl -X POST https://platform.runblob.io/v1/veo/generate \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "prompt": "A sunset over mountains",
        "callback_url": "https://your-app.com/webhook"
      }'
    ```
  </Step>

  <Step title="Handle Notifications">
    Your endpoint will receive instant notifications when tasks complete.
  </Step>
</Steps>

## Payload Structure

<Tabs>
  <Tab title="Completed">
    Sent when video generation completes successfully.

    ```json theme={null} theme={"system"}
    {
      "generation_id": "ffd473d5-5bef-4e14-bf22-8d559be3c19f",
      "status": "completed",
      "prompt": "A cat playing with a ball in a sunny room",
      "video_url": "https://cdn.runblob.io/videos/your-video.mp4",
      "created_at": "2025-12-06T21:43:03.374000",
      "completed_at": "2025-12-06T21:44:06.143000",
      "seed": 319790131,
      "error": null
    }
    ```

    <ResponseField name="generation_id" type="string">
      UUID of the completed task
    </ResponseField>

    <ResponseField name="status" type="string">
      Always `"completed"` for successful generations
    </ResponseField>

    <ResponseField name="prompt" type="string">
      Original prompt used for generation
    </ResponseField>

    <ResponseField name="video_url" type="string">
      Direct download URL for the generated video
    </ResponseField>

    <ResponseField name="created_at" type="string">
      ISO 8601 timestamp when task was created
    </ResponseField>

    <ResponseField name="completed_at" type="string">
      ISO 8601 timestamp when task completed
    </ResponseField>

    <ResponseField name="seed" type="number">
      Random seed used for generation
    </ResponseField>

    <ResponseField name="error" type="null">
      Always `null` for successful generations
    </ResponseField>
  </Tab>

  <Tab title="Failed">
    Sent when video generation fails with specific error codes.

    ```json theme={null} theme={"system"}
    {
      "generation_id": "ffd473d5-5bef-4e14-bf22-8d559be3c19f",
      "status": "failed",
      "prompt": "A cat playing with a ball in a sunny room",
      "video_url": null,
      "created_at": "2025-12-06T21:43:03.374000",
      "completed_at": "2025-12-06T21:44:06.143000",
      "seed": 319790131,
      "error": {
        "message": "UNSAFE_CONTENT",
        "details": {}
      }
    }
    ```

    <ResponseField name="status" type="string">
      Always `"failed"` for failed generations
    </ResponseField>

    <ResponseField name="error" type="object">
      Error details with `message` (error code) and `details` fields (see [Error Codes](/api-reference/veo3/errors))
    </ResponseField>

    <ResponseField name="video_url" type="null">
      Always `null` for failed tasks
    </ResponseField>
  </Tab>

  <Tab title="Timeout">
    Sent when generation exceeds the 10-minute limit.

    ```json theme={null} theme={"system"}
    {
      "generation_id": "ffd473d5-5bef-4e14-bf22-8d559be3c19f",
      "status": "failed",
      "prompt": "A cat playing with a ball in a sunny room",
      "video_url": null,
      "created_at": "2025-12-06T21:43:03.374000",
      "completed_at": "2025-12-06T21:44:06.143000",
      "seed": 319790131,
      "error": {
        "message": "TIMEOUT",
        "details": {}
      }
    }
    ```

    <Note>
      Credits are automatically refunded for timed-out tasks.
    </Note>
  </Tab>
</Tabs>

## Reliability Features

<AccordionGroup>
  <Accordion icon="repeat" title="Automatic Retries">
    **Retry Policy:** Up to 5 attempts with 30-second intervals

    **Backoff Strategy:** Linear backoff between retries

    **Failure Handling:** After 5 failed attempts, the webhook is marked as failed
  </Accordion>

  <Accordion icon="clock" title="Timeout Handling">
    **Request Timeout:** 30 seconds per webhook request

    **Connection Timeout:** 10 seconds to establish connection

    **Best Practice:** Respond quickly with a 200 status code
  </Accordion>

  <Accordion icon="shield-check" title="Security">
    **HTTPS Required:** All webhook URLs must use HTTPS

    **IP Allowlist:** Consider restricting access to RunBlob's IP ranges

    **Validation:** Always validate the generation\_id in your webhook handler
  </Accordion>
</AccordionGroup>

## Example Implementations

<CodeGroup>
  ```python Flask theme={null} theme={"system"}
  from flask import Flask, request, jsonify
  import logging

  app = Flask(__name__)
  logging.basicConfig(level=logging.INFO)

  @app.route('/webhook', methods=['POST'])
  def runblob_webhook():
      try:
          data = request.json
          generation_id = data.get('generation_id')
          status = data.get('status')
          
          if status == 'completed':
              video_url = data.get('video_url')
              logging.info(f"Video ready for {generation_id}: {video_url}")
              # Process successful generation
              process_video(generation_id, video_url)
          else:
              error = data.get('error', {})
              error_code = error.get('message', 'Unknown error')
              logging.error(f"Generation failed for {generation_id}: {error_code}")
              # Handle failure
              handle_failure(generation_id, error_code)
          
          return jsonify({'status': 'received'}), 200
      except Exception as e:
          logging.error(f"Webhook error: {str(e)}")
          return jsonify({'error': 'Internal error'}), 500

  def process_video(generation_id, video_url):
      # Your video processing logic here
      pass

  def handle_failure(generation_id, error_code):
      # Your error handling logic here
      pass
  ```

  ```javascript Express.js theme={null} theme={"system"}
  const express = require('express');
  const app = express();

  app.use(express.json());

  app.post('/webhook', (req, res) => {
    try {
      const { status, generation_id, video_url, error } = req.body;
      
      if (status === 'completed') {
        console.log(`Video ready for ${generation_id}: ${video_url}`);
        // Process successful generation
        processVideo(generation_id, video_url);
      } else {
        const errorCode = error?.message || 'Unknown error';
        console.error(`Generation failed for ${generation_id}: ${errorCode}`);
        // Handle failure
        handleFailure(generation_id, errorCode);
      }
      
      res.status(200).json({ status: 'received' });
    } catch (error) {
      console.error('Webhook error:', error);
      res.status(500).json({ error: 'Internal error' });
    }
  });

  function processVideo(generationId, videoUrl) {
    // Your video processing logic here
  }

  function handleFailure(generationId, errorCode) {
    // Your error handling logic here
  }

  app.listen(3000, () => {
    console.log('Webhook server running on port 3000');
  });
  ```
</CodeGroup>

<Warning>
  **Important:** Always return a 200 status code quickly to acknowledge receipt. Perform heavy processing asynchronously to avoid timeouts.
</Warning>
