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

# Create Land Transformation Video

> Create an AI-powered cinematic video transforming an empty plot of land into proposed constructions

Creates a new land transformation video from a single photo of an empty plot. The system progressively builds the land through chained transitions: raw terrain → marked buildable area → proposed constructions. Each transition is rendered as a cinematic morph, resulting in a fluid visualization of what the land could become.

This endpoint is **progressive** — only `source_image_url` and `format` are required. Everything else is optional and resolved server-side if omitted:

* **No mask?** The server auto-detects the buildable area via Gemini.
* **No scenes?** The server generates construction proposals based on terrain analysis.
* **No script?** The server auto-generates a narration script.

When you provide pre-generated assets (`scenes[]`, `script`, `mask_image_url`), the server skips the corresponding generation steps. This avoids double-charging for assets you already created.

## Request Body

<ParamField body="source_image_url" type="string" required>
  Publicly accessible URL of a single photo of an empty plot of land.

  * Must be a valid HTTP(S) URL
  * Must be an image file (JPEG, PNG, or WebP)
  * Only **one** image is accepted

  <Warning>
    Every image URL in the request is fetched and validated before the task is created. Unreachable images fail with `400 INVALID_IMAGE_URL`, and images below the minimum resolution fail with `400 IMAGE_RESOLUTION_TOO_LOW`. VibePeak-hosted assets belonging to another account are rejected with `403 ASSET_ACCESS_DENIED`.
  </Warning>
</ParamField>

<ParamField body="format" type="string" required>
  Target video format / aspect ratio.

  | Value  | Description                                                          |
  | ------ | -------------------------------------------------------------------- |
  | `9:16` | Vertical video (portrait) — ideal for social media stories and reels |
  | `16:9` | Horizontal video (landscape) — ideal for websites and presentations  |
</ParamField>

### Mask & Area Overrides

<ParamField body="mask_image_url" type="string">
  Optional binary mask image (PNG) marking the buildable area with a neon outline and semi-transparent fill.

  When provided, the server uses this mask directly and skips auto-detection. Takes precedence over `area_polygon`.

  Alias: `mask_url` is accepted and normalized to `mask_image_url`. If both are sent, `mask_image_url` wins.
</ParamField>

<ParamField body="marked_image_url" type="string">
  Optional pre-rendered **marked terrain** image (the `img1` frame — the source photo with the buildable area already highlighted).

  When provided, the server skips the marking render entirely and uses this frame directly. Like `mask_image_url` and `area_polygon`, supplying it waives the 5-credit mask charge, and the task reports `marking: "mask"`.

  It takes precedence over both `mask_image_url` and `area_polygon`.

  <Note>
    `marked_image_url` also changes how the frame count is calculated when you supply `scenes[]` — see [Frame Limits](#frame-limits).
  </Note>
</ParamField>

<ParamField body="area_polygon" type="object[]">
  Optional polygon coordinates marking the buildable area. Minimum 3 points required.

  Each point is an object with normalized coordinates:

  <Expandable title="Point properties">
    <ParamField body="x" type="number" required>
      Normalized X coordinate between 0 and 1.
    </ParamField>

    <ParamField body="y" type="number" required>
      Normalized Y coordinate between 0 and 1.
    </ParamField>
  </Expandable>

  Alias: `polygon` is accepted and normalized to `area_polygon`.

  If none of `mask_image_url`, `area_polygon` or `marked_image_url` is provided, the server auto-detects the buildable area using Gemini vision analysis.
</ParamField>

### Terrain & Construction Overrides

<ParamField body="terrain_type" type="string">
  Optional terrain type hint, e.g. `"urban"`, `"rural"` or `"industrial"`.

  Any non-empty string is accepted — the value is a free-form hint passed to the proposal generator, not a closed enum. If omitted, the server analyzes the image and auto-detects the terrain type.
</ParamField>

<ParamField body="construction_prompts" type="string[]">
  Optional array of construction prompts (1-8). One image is generated per prompt in the order provided.

  When provided, the server skips proposal auto-generation and renders exactly these constructions. Each prompt is still billed as a generated scene.

  Alias: `prompts` is accepted and normalized to `construction_prompts`.
</ParamField>

<ParamField body="num_constructions" type="integer" default="3">
  Optional number of constructions to auto-generate (2-8). Default is 3.

  Ignored if `scenes` or `construction_prompts` are provided.
</ParamField>

<ParamField body="style" type="string">
  Optional style hint that biases auto-generated proposals (e.g., `"modern"`, `"rustic"`, `"minimalist"`).
</ParamField>

<ParamField body="exclude" type="string">
  Optional exclusion hint for auto-generated proposals (e.g., `"no swimming pools"`, `"no glass facades"`).
</ParamField>

### Scene Chain (Fully Resolved)

<ParamField body="scenes" type="object[]">
  Optional fully-resolved scene chain (1-8 items). When provided, the server skips scene generation and renders these frames directly.

  Each scene is an object with:

  <Expandable title="Scene properties">
    <ParamField body="image_url" type="string">
      Publicly accessible URL of the scene image. Optional at the schema level, but a scene without an image cannot be rendered — always send it when supplying a resolved chain.
    </ParamField>

    <ParamField body="prompt" type="string">
      Optional prompt that generated this scene.
    </ParamField>

    <ParamField body="order" type="integer">
      Optional display order within the chain. **Must be 1 or greater** — `0` is rejected with `400 VALIDATION_ERROR`.
    </ParamField>

    <ParamField body="camera_movement" type="string">
      Optional per-scene camera movement applied by the WAN renderer.

      One of `traveling_left`, `traveling_right`, `traveling_up`, `traveling_down`, `dolly_in`, `dolly_out`, `arc_left`, `arc_right`. Omitted means the renderer picks the movement.
    </ParamField>
  </Expandable>

  <Tip>
    Providing `scenes[]` is the recommended path for first-party callers (like the VibePeak web app) that have already generated images interactively. This ensures you are only charged the base render cost (15 credits) and avoids double-charging for generated assets.
  </Tip>
</ParamField>

### Script, Voice & Music Overrides

<ParamField body="script" type="string">
  Optional narration script for text-to-speech. Any non-empty string is accepted.

  <Note>
    Unlike the [narrated slideshow](/api-reference/videos/create-narrated-slideshow) and real-estate endpoints, `/v1/land` does **not** enforce a character-count range or reject digits and symbols. For natural pacing aim for roughly 65-85 characters per scene, but this is guidance, not validation.
  </Note>
</ParamField>

<ParamField body="language" type="string">
  Language code for the auto-generated script and TTS pronunciation (e.g., `en`, `es`, `de`, `fr`, `pt`). Minimum 2 characters.
</ParamField>

<ParamField body="voice_id" type="string">
  Voice ID for narration. You can find available voices using the [List Voices](/api-reference/voices/list-voices) endpoint.

  Defaults to a platform voice if omitted.
</ParamField>

<ParamField body="voiceover_enabled" type="boolean">
  Set to `false` to render the video without narration. When `false`, the server skips text-to-speech entirely and `script` / `voice_id` may be omitted.

  Omitting this field leaves narration **on**.
</ParamField>

<ParamField body="background_music" type="boolean">
  Set to `true` to add a background music bed. Omitting this field, or sending `false`, renders the video without music.
</ParamField>

<ParamField body="music_id" type="string">
  Optional Music Library track to pin, as a **UUID**. Without it, a `background_music: true` job uses a randomly selected bed.

  <Warning>
    `music_id` and `music_kind` must be sent **together** — sending one without the other fails with `400 VALIDATION_ERROR` (`music_id and music_kind must be provided together`). A non-UUID value fails with `music_id must be a valid UUID`.

    Both fields are only honored when `background_music` is `true`; with music off the video stays silent regardless.
  </Warning>
</ParamField>

<ParamField body="music_kind" type="string">
  Which catalogue `music_id` refers to: `stock` (Music Library track) or `ai` (AI-generated track). Required whenever `music_id` is present.
</ParamField>

### Subtitle & Elements Configuration

<ParamField body="subtitle_config" type="object">
  Subtitle configuration for word-level animated overlays. Same structure as other video endpoints — see [Create Narrated Slideshow](/api-reference/videos/create-narrated-slideshow) for the full field list.

  On this endpoint the object is passed through to the renderer without field-level validation, so a malformed key is accepted at request time and simply ignored downstream.
</ParamField>

<ParamField body="elements_config" type="object">
  Configuration for visual elements and branding overlays (watermark, text, intro/outro). Same structure as other video endpoints — see [Create Narrated Slideshow](/api-reference/videos/create-narrated-slideshow).

  The `intro_outro` block is fully validated; the rest is passed through to the renderer. Browse the designs in the [template gallery](/api-reference/videos/intro-outro-templates), or read the catalogue from [`GET /v1/intro-outro/templates`](/api-reference/videos/list-intro-outro-templates).

  As on [Create Narrated Slideshow](/api-reference/videos/create-narrated-slideshow), the overlays are applied **after** the video renders. The task passes through the transitional `intro_outro` status while that runs, and if the overlays cannot be applied the task still completes: the video is delivered without them and the result carries an `intro_outro_warning`.

  <Note>
    Unlike the real-estate and narrated-slideshow endpoints, `/v1/land` does **not** pre-validate the watermark URL at request time, so it never returns `INVALID_WATERMARK_URL`. An unreachable watermark surfaces later as a render failure. Verify the URL before submitting.
  </Note>
</ParamField>

### Webhook

<ParamField body="webhook_url" type="string">
  HTTPS URL to receive a webhook notification when the task completes or fails.

  See [Webhooks](/concepts/webhooks) for payload format and verification details.

  <Warning>
    Webhook URLs must use HTTPS and cannot point to private/internal networks (SSRF protection). A rejected URL fails with `400 INVALID_WEBHOOK_URL`.
  </Warning>
</ParamField>

## Response

Returns a `202 Accepted` response. Unlike other video endpoints, land tasks start in the `preparing` state while the server resolves inputs (auto-detecting the mask, generating scenes, creating the script), and they report credits under `charged_credits` with a `resolution` object.

<Warning>
  **Land responses are deliberately shaped differently from other video endpoints.** Land tasks use `charged_credits` and a `resolution` object; every other task type uses `credits_charged` and has no `resolution`. If you share response-parsing code across endpoints, handle both key names.
</Warning>

<ResponseField name="task_id" type="string" required>
  Unique identifier for the task, in the form `task_<id>`. Use this to poll for status via [Get Task](/api-reference/videos/get-task).

  <Note>
    The `task_id` sent in the **webhook** payload is a different identifier (`vp_land_<id>`, the task's external id). Correlate webhooks by storing both, or by matching on your own reference — do not assume the webhook `task_id` can be passed to `GET /v1/tasks/{id}`.
  </Note>
</ResponseField>

<ResponseField name="status" type="string" required>
  Initial task status. Always `preparing` for land tasks.
</ResponseField>

<ResponseField name="message" type="string" required>
  Human-readable acknowledgement — `"Land transformation request accepted"`.
</ResponseField>

<ResponseField name="created_at" type="string" required>
  ISO 8601 timestamp of task creation.
</ResponseField>

<ResponseField name="livemode" type="boolean" required>
  `true` for live keys. Requests made with a `vpk_test_` key return `false` and are not charged — see [Test Mode](/concepts/test-mode).
</ResponseField>

<ResponseField name="request_id" type="string" required>
  Request identifier, useful when contacting support.
</ResponseField>

<ResponseField name="charged_credits" type="integer" required>
  Credits actually deducted for this request. See [Credit Model](#credit-model) for the breakdown.
</ResponseField>

<ResponseField name="resolution" type="object">
  Preparation metadata describing what the server will compute.

  <Expandable title="Resolution properties">
    <ResponseField name="stage" type="string">
      Current preparation stage — always `"preparing"` at creation.
    </ResponseField>

    <ResponseField name="progress" type="integer">
      Preparation progress percentage (0-100). Starts at 10.
    </ResponseField>

    <ResponseField name="used_overrides" type="string[]">
      The request fields you supplied that overrode server-side generation, in canonical order. An explicit `background_music: false` counts as an override; an omitted field does not.
    </ResponseField>

    <ResponseField name="credits_breakdown" type="object">
      Detailed credit cost breakdown.

      <Expandable title="Credits breakdown properties">
        <ResponseField name="base_render" type="integer">
          Base render cost (15 credits).
        </ResponseField>

        <ResponseField name="generated_mask" type="integer">
          Cost for a server-generated mask (5 credits) — 0 if `mask_image_url`, `area_polygon` or `marked_image_url` is provided.
        </ResponseField>

        <ResponseField name="generated_scene_count" type="integer">
          Number of scenes the server will generate.
        </ResponseField>

        <ResponseField name="generated_scenes" type="integer">
          Total cost for generated scenes (5 credits each).
        </ResponseField>

        <ResponseField name="generated_script" type="integer">
          Cost for a server-generated script (5 credits) — 0 if `script` is provided.
        </ResponseField>

        <ResponseField name="total" type="integer">
          Total credits charged.
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="_links" type="object">
  Convenience links: `self` (the poll URL) and `poll_interval_seconds`.
</ResponseField>

### Response Headers

| Header        | Description                                         |
| ------------- | --------------------------------------------------- |
| `Location`    | URL to poll for task status (`/v1/tasks/{task_id}`) |
| `Retry-After` | Recommended polling interval in seconds (30)        |

<RequestExample>
  ```bash cURL (Minimal — 1 image) theme={null}
  curl -X POST https://api.vibepeak.ai/v1/land \
    -H "Authorization: Bearer vpk_live_xxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "source_image_url": "https://example.com/land/empty-plot.jpg",
      "format": "9:16"
    }'
  ```

  ```bash cURL (With mask and more constructions) theme={null}
  curl -X POST https://api.vibepeak.ai/v1/land \
    -H "Authorization: Bearer vpk_live_xxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "source_image_url": "https://example.com/land/empty-plot.jpg",
      "format": "9:16",
      "mask_image_url": "https://example.com/land/mask.png",
      "num_constructions": 5
    }'
  ```

  ```bash cURL (Fully resolved — short-circuit) theme={null}
  curl -X POST https://api.vibepeak.ai/v1/land \
    -H "Authorization: Bearer vpk_live_xxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "source_image_url": "https://example.com/land/original.jpg",
      "format": "16:9",
      "marked_image_url": "https://example.com/land/marked.jpg",
      "scenes": [
        { "image_url": "https://example.com/land/building1.jpg", "order": 1, "camera_movement": "dolly_in" },
        { "image_url": "https://example.com/land/building2.jpg", "order": 2 },
        { "image_url": "https://example.com/land/building3.jpg", "order": 3 }
      ],
      "script": "Discover the potential of this stunning plot. From raw land to a modern family home, the transformation is breathtaking.",
      "voice_id": "EXAVITQu4vr4xnSDxMaL",
      "language": "en",
      "webhook_url": "https://yourserver.com/webhooks/vibepeak"
    }'
  ```

  ```bash cURL (No narration, with pinned music) theme={null}
  curl -X POST https://api.vibepeak.ai/v1/land \
    -H "Authorization: Bearer vpk_live_xxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "source_image_url": "https://example.com/land/empty-plot.jpg",
      "format": "9:16",
      "voiceover_enabled": false,
      "background_music": true,
      "music_id": "3f9a1c62-5b4e-4a77-9d21-0c8b6e4f1a35",
      "music_kind": "stock"
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch('https://api.vibepeak.ai/v1/land', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer vpk_live_xxxxx',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      source_image_url: 'https://example.com/land/empty-plot.jpg',
      format: '9:16'
    })
  });

  const task = await response.json();
  console.log(`Task created: ${task.task_id}`);
  console.log(`Status: ${task.status}`);
  console.log(`Credits: ${task.charged_credits}`);
  console.log(`Breakdown:`, task.resolution.credits_breakdown);
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.vibepeak.ai/v1/land',
      headers={
          'Authorization': 'Bearer vpk_live_xxxxx',
          'Content-Type': 'application/json'
      },
      json={
          'source_image_url': 'https://example.com/land/empty-plot.jpg',
          'format': '9:16'
      }
  )

  task = response.json()
  print(f"Task created: {task['task_id']}")
  print(f"Status: {task['status']}")
  print(f"Credits: {task['charged_credits']}")
  print(f"Breakdown: {task['resolution']['credits_breakdown']}")
  ```
</RequestExample>

<ResponseExample>
  ```json 202 Accepted (Minimal) theme={null}
  {
    "task_id": "task_abc123xyz",
    "status": "preparing",
    "message": "Land transformation request accepted",
    "created_at": "2026-08-31T12:15:00.000Z",
    "livemode": true,
    "request_id": "req_xyz123",
    "charged_credits": 40,
    "resolution": {
      "stage": "preparing",
      "progress": 10,
      "credits_breakdown": {
        "base_render": 15,
        "generated_mask": 5,
        "generated_scene_count": 3,
        "generated_scenes": 15,
        "generated_script": 5,
        "total": 40
      },
      "used_overrides": []
    },
    "_links": {
      "self": "/v1/tasks/task_abc123xyz",
      "poll_interval_seconds": 30
    }
  }
  ```

  ```json 202 Accepted (Fully Resolved) theme={null}
  {
    "task_id": "task_abc123xyz",
    "status": "preparing",
    "message": "Land transformation request accepted",
    "created_at": "2026-08-31T12:15:00.000Z",
    "livemode": true,
    "request_id": "req_xyz123",
    "charged_credits": 15,
    "resolution": {
      "stage": "preparing",
      "progress": 10,
      "credits_breakdown": {
        "base_render": 15,
        "generated_mask": 0,
        "generated_scene_count": 0,
        "generated_scenes": 0,
        "generated_script": 0,
        "total": 15
      },
      "used_overrides": ["marked_image_url", "scenes", "script", "language", "voice_id"]
    },
    "_links": {
      "self": "/v1/tasks/task_abc123xyz",
      "poll_interval_seconds": 30
    }
  }
  ```

  ```json 400 Bad Request (Validation Error) theme={null}
  {
    "error": {
      "code": "VALIDATION_ERROR",
      "message": "Land chains must contain 3 to 10 total frames including img0 and img1",
      "request_id": "req_xyz123",
      "details": {
        "field": "scenes",
        "issues": [
          {
            "path": "scenes",
            "message": "Land chains must contain 3 to 10 total frames including img0 and img1"
          }
        ]
      }
    }
  }
  ```

  ```json 400 Bad Request (Invalid Image URL) theme={null}
  {
    "error": {
      "code": "INVALID_IMAGE_URL",
      "message": "Failed to access image: fetch failed (ENOTFOUND)",
      "request_id": "req_xyz123",
      "details": {
        "invalid_images": ["https://example.com/land/empty-plot.jpg"]
      }
    }
  }
  ```

  ```json 402 Payment Required theme={null}
  {
    "error": {
      "code": "INSUFFICIENT_CREDITS",
      "message": "Insufficient credits",
      "request_id": "req_xyz123"
    }
  }
  ```

  ```json 403 Forbidden (Asset Access) theme={null}
  {
    "error": {
      "code": "ASSET_ACCESS_DENIED",
      "message": "Asset is not accessible",
      "request_id": "req_xyz123"
    }
  }
  ```

  ```json 403 Forbidden (Plan) theme={null}
  {
    "error": {
      "code": "PLAN_REQUIRED",
      "message": "API access requires a Plus, Pro, Max or Enterprise plan. Please upgrade your subscription.",
      "request_id": "req_xyz123",
      "details": {
        "current_plan": "Free",
        "required_plans": ["Plus", "Pro", "Max", "Enterprise"]
      }
    }
  }
  ```

  ```json 409 Conflict theme={null}
  {
    "error": {
      "code": "CONCURRENCY_LIMIT_EXCEEDED",
      "message": "You have reached your concurrent task limit of 1. Please wait for existing tasks to complete.",
      "request_id": "req_xyz123",
      "details": {
        "limit": 1,
        "in_flight": 1,
        "plan": "Plus"
      }
    }
  }
  ```

  ```json 503 Service Unavailable theme={null}
  {
    "error": {
      "code": "SERVICE_UNAVAILABLE",
      "message": "Land transformation service is temporarily unavailable. Please try again.",
      "request_id": "req_xyz123"
    }
  }
  ```
</ResponseExample>

## Credit Model

Land transformation videos use a **differentiated credit model** based on the work the server performs:

| Component        | Credits     | Condition                                                                                |
| ---------------- | ----------- | ---------------------------------------------------------------------------------------- |
| Base render      | 15          | Always charged                                                                           |
| Generated mask   | 5           | Charged if you do **not** provide `mask_image_url`, `area_polygon` or `marked_image_url` |
| Generated scenes | 5 per scene | Charged for each scene the server generates (not for pre-provided `scenes[]`)            |
| Generated script | 5           | Charged if you do **not** provide `script`                                               |

When `scenes[]` is absent, the number of generated scenes is `construction_prompts.length`, else `num_constructions`, else 3.

**Example costs:**

| Scenario                                              | Calculation     | Total  |
| ----------------------------------------------------- | --------------- | ------ |
| Minimal (1 image, auto everything)                    | 15 + 5 + 15 + 5 | **40** |
| With mask, auto scenes + script                       | 15 + 0 + 15 + 5 | **35** |
| With mask + 5 construction prompts                    | 15 + 0 + 25 + 5 | **45** |
| Fully resolved (`scenes[]` + `script` + marked frame) | 15 + 0 + 0 + 0  | **15** |

<Info>
  Assets you pre-provide (`scenes[]`, `script`, `mask_image_url`, `marked_image_url`) are **never regenerated** and **never re-charged**. This prevents double-charging for first-party callers that already generated images interactively.
</Info>

Credits are deducted at creation time. If the job later fails, the credits are **refunded automatically** — the webhook's `charged_credits` reflects the post-refund amount (typically `0`).

## Plan Access

| Plan       | API Access | Concurrent Tasks |
| ---------- | ---------- | ---------------- |
| Free       | No         | —                |
| Starter    | No         | —                |
| Plus       | Yes        | 1                |
| Pro        | Yes        | 3                |
| Max        | Yes        | 5                |
| Enterprise | Yes        | Custom           |

## Frame Limits

The video chain must contain **3 to 10 total frames**. How the count is derived depends on whether you supply `scenes[]`:

* **Without `scenes[]`** — the count is `2 + n`, where `n` is `construction_prompts.length`, else `num_constructions`, else 3. The two implied frames are `img0` (original terrain) and `img1` (marked terrain).
* **With `scenes[]`** — the count is `1 + (marked_image_url ? 1 : 0) + scenes.length`. `img1` is only counted when you actually supply `marked_image_url`.

<Warning>
  A resolved chain of 3 scenes with **no** `marked_image_url` counts as 4 frames, not 5. If you are supplying a chain that already includes the marked frame as its first scene, do not also send `marked_image_url` — you would count it twice.
</Warning>

Requests outside the 3-10 range fail with `400 VALIDATION_ERROR` on the `scenes` field.

## Polling

Land tasks flow through `preparing` → `queued` → `processing` → `completed` or `failed`.

If the request asked for post-processing, the task also reports a transitional status between `processing` and its final state, in this order: `watermarking`, `intro_outro`, `subtitling`. Each appears only when the corresponding feature was requested. Treat all three like `processing` and keep polling — the task always ends in `completed` or `failed`. A step that cannot finish does not fail the task: the video is delivered without that refinement and `result` carries a matching `watermark_warning`, `intro_outro_warning` or `subtitle_warning`. See [Asynchronous Processing](/concepts/async-processing).

While in `preparing`, `GET /v1/tasks/{task_id}` returns a `resolution` object that advances through these stages:

| Stage              | Progress |
| ------------------ | -------- |
| `preparing`        | 10       |
| `marking_resolved` | 35       |
| `terrain_resolved` | 50       |
| `scenes_resolved`  | 70       |
| `script_resolved`  | 85       |
| `resolved`         | 100      |

Not every task passes through every stage. Supplying `marked_image_url` or `mask_image_url` resolves marking synchronously, so a fully-resolved request skips the intermediate stages and reports `resolved` / 100 on the first poll. Treat the ladder as monotonic but sparse — do not wait for a specific stage to appear.

Once marking is decided, the `resolution` object also carries `marking`, `generated_image_count` and `used_overrides`. It also carries `resolved_inputs` — a whitelisted view of what the server actually resolved (`source_image_url`, `format`, `mask_image_url`, `area_polygon`, `terrain_type`, `scenes`, `construction_prompts`, `script`), which is how you retrieve the generated scene image URLs and the auto-written script. Your `webhook_url` and other request fields are deliberately not echoed back. `generated_image_count` counts server-generated scene images plus one for an auto-generated mask, and stays `0` for a fully-resolved request. On land tasks the GET response uses `charged_credits`; other task types use `credits_charged`.

<Note>
  Tasks expire **24 hours** after creation. After that, `GET /v1/tasks/{task_id}` returns `404 TASK_EXPIRED` and the video URL is no longer retrievable — download the result before then.
</Note>

## Webhook Payload

When the task completes or fails, a POST request is sent to your `webhook_url`. See [Webhooks](/concepts/webhooks) for verification.

### Webhook Headers

| Header                 | Value                             |
| ---------------------- | --------------------------------- |
| `X-VibePeak-Event`     | `task.completed` or `task.failed` |
| `X-VibePeak-Signature` | HMAC-SHA256, hex encoded          |
| `X-VibePeak-Timestamp` | Unix timestamp in seconds         |

<Warning>
  The signed message is **`{timestamp}.{raw body}`**, not the body alone. Concatenate the `X-VibePeak-Timestamp` value, a literal `.`, and the exact raw request body before computing the HMAC, or verification will fail.
</Warning>

<Note>
  The event name lives in the `X-VibePeak-Event` **header**, not in the body. There is no `event` field inside the JSON.
</Note>

### Payload

```json theme={null}
{
  "task_id": "vp_land_gXATHu1cGcD9Ht9s5-19I",
  "status": "completed",
  "result": {
    "video_url": "https://<project>.supabase.co/storage/v1/object/public/vibepeak/<user-id>/projects/land/<project-id>/final-1788179592692.mp4",
    "cover_image_url": "https://storage.googleapis.com/vibepeak-api-images/covers/task_yo9RC7ZrE9YuFx4yXbg4N.jpg"
  },
  "charged_credits": 15,
  "resolution": {
    "marking": "mask",
    "used_overrides": ["marked_image_url", "scenes", "script", "language"],
    "generated_image_count": 0
  }
}
```

For failed tasks:

```json theme={null}
{
  "task_id": "vp_land_gXATHu1cGcD9Ht9s5-19I",
  "status": "failed",
  "error": {
    "code": "GENERATION_FAILED",
    "message": "Land generation failed"
  },
  "charged_credits": 0,
  "resolution": {
    "marking": "auto",
    "generated_image_count": 4,
    "used_overrides": []
  }
}
```

<Warning>
  The webhook `task_id` is the task's **external id** (`vp_land_<id>`), which is **not** the `task_id` returned by `POST /v1/land` (`task_<id>`) and cannot be passed to `GET /v1/tasks/{id}`. Store the value returned at creation and correlate on your side.
</Warning>

Notes on the payload:

* `resolution` is the **typed** shape only — `marking`, `generated_image_count`, `used_overrides`. It does **not** include `stage`, `progress` or `credits_breakdown`; those appear only on the polling response.
* `resolution` is omitted entirely if marking was never resolved (for example, a failure during preparation).
* `result` carries **only** `video_url` and `cover_image_url`. There is no `duration_seconds`, `resolution` or `expires_at` inside `result`.
* `cover_image_url` is present only if cover generation succeeded. Note that it is keyed by the internal `task_<id>`, while the payload's `task_id` is the `vp_land_<id>` external id.
* There is no `event`, `type` or `timestamp` field in the body. Distinguish outcomes with `status`, or read the `X-VibePeak-Event` header.

The `marking` field indicates which method was used to identify the buildable area:

| Value       | Meaning                                                                                                                     |
| ----------- | --------------------------------------------------------------------------------------------------------------------------- |
| `mask`      | A pre-provided `marked_image_url` **or** `mask_image_url` was used. `marked_image_url` takes precedence when both are sent. |
| `polygon`   | A pre-provided `area_polygon` was rasterized into a mask                                                                    |
| `auto`      | Gemini auto-detected the buildable area                                                                                     |
| `fullimage` | Reserved. Present in the type contract but not currently emitted by any code path — do not branch on it.                    |

## Error Codes

| Code                         | Status | Description                                                                                                         |
| ---------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------- |
| `VALIDATION_ERROR`           | 400    | Invalid request parameters, frame count outside 3-10, `order` below 1, or `music_id`/`music_kind` not sent together |
| `INVALID_IMAGE_URL`          | 400    | An image URL is unreachable, not an image, or too large                                                             |
| `IMAGE_RESOLUTION_TOO_LOW`   | 400    | An image is below the minimum resolution                                                                            |
| `INVALID_WEBHOOK_URL`        | 400    | Webhook URL failed SSRF validation                                                                                  |
| `INVALID_API_KEY`            | 401    | Invalid or missing API key                                                                                          |
| `INSUFFICIENT_CREDITS`       | 402    | Not enough credits to process the request                                                                           |
| `ASSET_ACCESS_DENIED`        | 403    | A VibePeak-hosted image URL belongs to another account                                                              |
| `PLAN_REQUIRED`              | 403    | Plan doesn't include API access                                                                                     |
| `CONCURRENCY_LIMIT_EXCEEDED` | 409    | Concurrent task limit reached                                                                                       |
| `SERVICE_UNAVAILABLE`        | 503    | The job could not be enqueued; credits are refunded automatically                                                   |

Asynchronous failures are reported through the task's `error` object and the failure webhook:

| Code                 | Description                             |
| -------------------- | --------------------------------------- |
| `GENERATION_FAILED`  | The land render failed downstream       |
| `GENERATION_TIMEOUT` | Generation exceeded the 15-minute limit |

See [Error Handling](/concepts/error-handling) for more details.

## Next Steps

After creating a task:

1. **Poll for status**: Use [Get Task](/api-reference/videos/get-task) with the `task_id` from the 202 response, every 30 seconds.
2. **Wait for webhook**: If configured, receive notification when complete — remembering the id difference noted above.
3. **Download video**: Access `result.video_url` within 24 hours of creation.
