> ## 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 Property Showcase

> Generate a branded real estate property showcase image from a template

Creates a property showcase image from a single property photo and a selected template. Generation runs asynchronously — the request returns a job ID immediately and the finished image is delivered to your `webhook_url`.

<Warning>
  Set a `webhook_url`. Showcase jobs are **not** pollable: [Get Task](/api-reference/videos/get-task) covers video tasks only and returns `404 TASK_NOT_FOUND` for a showcase `job_id`. The webhook is the only way to receive `output_image_url`.
</Warning>

`price` and `size` are required for **every** template. All other property `fields` are optional, but each template only **accepts** the subset it renders — sending a field a template doesn't support returns a `VALIDATION_ERROR`.

## Request Body

<ParamField body="template_id" type="string" required>
  The showcase template to render. Determines the layout, the output **aspect ratio**, which optional `fields` it accepts, and exactly how many supporting images it requires. `price` and `size` are always required; the remaining fields are optional and only accepted when the template renders them. Each template requires an **exact** number of `supporting_image_urls` (see the table).

  | Value                | Aspect ratio | Always required | Optional accepted                           | Supporting images |
  | -------------------- | ------------ | --------------- | ------------------------------------------- | ----------------- |
  | `hero_impact`        | 9:16         | `price`, `size` | `bedrooms`                                  | 0                 |
  | `info_story`         | 9:16         | `price`, `size` | `bedrooms`, `bathrooms`, `parking`          | 0                 |
  | `luxury_vertical`    | 9:16         | `price`, `size` | `label`, `bedrooms`, `bathrooms`            | 3                 |
  | `dynamic_collage`    | 9:16         | `price`, `size` | `label`                                     | 3                 |
  | `ad_performance`     | 9:16         | `price`, `size` | `label`                                     | 3                 |
  | `cover_blurs`        | 9:16         | `price`, `size` | `label`                                     | 3                 |
  | `minimalist`         | 16:9         | `price`, `size` | `label`, `bedrooms`, `bathrooms`, `parking` | 3                 |
  | `magazine`           | 16:9         | `price`, `size` | `label`, `bedrooms`, `bathrooms`, `parking` | 3                 |
  | `dark_minimalist`    | 16:9         | `price`, `size` | `label`, `bedrooms`, `bathrooms`, `parking` | 3                 |
  | `tech`               | 16:9         | `price`, `size` | `label`, `bedrooms`, `bathrooms`, `parking` | 3                 |
  | `luxury_real_estate` | 16:9         | `price`, `size` | `label`, `bedrooms`, `bathrooms`, `parking` | 3                 |
  | `modern`             | 16:9         | `price`, `size` | `label`, `bedrooms`, `bathrooms`, `parking` | 4                 |

  Templates with a **9:16** aspect ratio render a vertical (portrait) image; **16:9** templates render a horizontal (landscape) image.

  See the [**Showcase Templates**](/api-reference/images/property-showcase-templates) page for a visual preview of every template.
</ParamField>

<ParamField body="fields" type="object" required>
  Property data rendered onto the showcase. `price` and `size` are **always required** (non-empty strings) for every template. The remaining fields are optional and only **accepted** when the selected `template_id` renders them (see the table above). Omitting a required field — or sending an empty string — returns a `VALIDATION_ERROR` with a `fields.<name>` path, and sending a field the template doesn't accept returns a `VALIDATION_ERROR` with the message `"<field> is not a valid field for the <template> template"`.

  <Expandable title="Fields properties">
    <ParamField body="price" type="string" required>
      Display price, e.g. `"$450,000"`. Sent as a string (non-empty). Required by every template.
    </ParamField>

    <ParamField body="size" type="string" required>
      Display size, e.g. `"120 m²"` or `"1,300 sqft"`. Sent as a string (non-empty). Required by every template.
    </ParamField>

    <ParamField body="label" type="string">
      Short headline or tag shown on the showcase, e.g. `"For Sale"` or `"New Listing"`. Sent as a string (non-empty).
    </ParamField>

    <ParamField body="bedrooms" type="integer">
      Number of bedrooms. Non-negative integer (`>= 0`).
    </ParamField>

    <ParamField body="bathrooms" type="integer">
      Number of bathrooms. Non-negative integer (`>= 0`).
    </ParamField>

    <ParamField body="parking" type="integer">
      Number of parking spots. Non-negative integer (`>= 0`).
    </ParamField>
  </Expandable>

  <Note>
    `price` and `size` are required for every template. String fields (`price`, `size`, `label`) must not be empty or whitespace-only. Numeric fields (`bedrooms`, `bathrooms`, `parking`) must be non-negative integers. Sending a field the selected template doesn't accept is rejected.
  </Note>
</ParamField>

<ParamField body="language" type="string" default="en">
  Language used to render the showcase copy. One of `en`, `es`, `de`, `fr`, `it`, `pt`. Defaults to `en` when omitted. Forwarded to the generation service so the rendered text matches the requested locale.
</ParamField>

<ParamField body="main_image_url" type="string" required>
  URL of the primary property photo used as the showcase hero image. Must be a valid, publicly accessible URL.
</ParamField>

<ParamField body="supporting_image_urls" type="string[]" default="[]">
  Additional property photos. Each template requires an **exact** number of supporting images (see the `template_id` table): `0` for `hero_impact` and `info_story`, `3` for most templates, and `4` for `modern`. Sending the wrong number returns a `VALIDATION_ERROR` on the `supporting_image_urls` path. Each must be a valid URL. For a template that requires `0`, omit the field or pass an empty array.
</ParamField>

<ParamField body="logo_image_url" type="string">
  Optional URL of a brand/agency logo to overlay on the showcase. Must be a valid URL. May be `null`.
</ParamField>

<Note>
  **Accepted image formats & size.** Every image URL (`main_image_url`, `supporting_image_urls`, `logo_image_url`) must point to a publicly reachable image in one of these formats: **PNG, JPG/JPEG, GIF, WebP, BMP, TIFF, AVIF, HEIC, ICO, SVG**, and must be **≤ 8 MB** each. Before the job is accepted, every URL is validated for reachability, content type, actual image content (magic bytes), and SSRF safety; a failure returns one of the image error codes listed below. These image errors carry **no** `details` object — the offending field name is prefixed onto the `message` instead, e.g. `"main_image_url: Image exceeds the 8 MB limit"`.
</Note>

<ParamField body="webhook_url" type="string">
  URL to receive a webhook notification when the job completes. Must be a valid HTTPS URL, at most 2048 characters. May be `null`. See [Webhooks](/concepts/webhooks) for details.

  <Warning>
    **The webhook is the only way to receive the finished image.** Showcase jobs are not exposed through [Get Task](/api-reference/videos/get-task) — that endpoint covers video tasks only and returns `404 TASK_NOT_FOUND` for a showcase `job_id`. Without a `webhook_url` you cannot retrieve `output_image_url`.
  </Warning>

  <Warning>
    Webhook URLs must use HTTPS and cannot point to private/internal networks (SSRF protection). A non-HTTPS or private URL is rejected with `VALIDATION_ERROR` and the message `Webhook URL must be HTTPS and cannot point to private networks`.
  </Warning>
</ParamField>

## Response

<ResponseField name="job_id" type="string" required>
  Unique identifier for the showcase job. Use this to check status or correlate the webhook.
</ResponseField>

<ResponseField name="status" type="string" required>
  Initial job status. Always `pending` for new jobs.
</ResponseField>

<ResponseField name="message" type="string" required>
  Human-readable confirmation that the request was accepted for async processing.
</ResponseField>

<ResponseField name="request_id" type="string" required>
  Unique identifier for this request, useful for support and debugging.
</ResponseField>

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://api.vibepeak.ai/v1/real-estate/property-showcase \
    -H "Authorization: Bearer vpk_live_xxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "template_id": "luxury_vertical",
      "language": "es",
      "fields": {
        "price": "$450,000",
        "size": "120 m²",
        "bedrooms": 3
      },
      "main_image_url": "https://example.com/property/exterior.jpg",
      "supporting_image_urls": [
        "https://example.com/property/living-room.jpg",
        "https://example.com/property/kitchen.jpg",
        "https://example.com/property/bedroom.jpg"
      ],
      "logo_image_url": "https://example.com/agency-logo.png",
      "webhook_url": "https://yourserver.com/webhooks/vibepeak"
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch('https://api.vibepeak.ai/v1/real-estate/property-showcase', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer vpk_live_xxxxx',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      template_id: 'luxury_vertical',
      language: 'es',
      fields: {
        price: '$450,000',
        size: '120 m²',
        bedrooms: 3
      },
      main_image_url: 'https://example.com/property/exterior.jpg',
      supporting_image_urls: [
        'https://example.com/property/living-room.jpg',
        'https://example.com/property/kitchen.jpg',
        'https://example.com/property/bedroom.jpg'
      ],
      logo_image_url: 'https://example.com/agency-logo.png',
      webhook_url: 'https://yourserver.com/webhooks/vibepeak'
    })
  });

  const job = await response.json();
  console.log(`Job created: ${job.job_id}`);
  ```

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

  response = requests.post(
      'https://api.vibepeak.ai/v1/real-estate/property-showcase',
      headers={
          'Authorization': 'Bearer vpk_live_xxxxx',
          'Content-Type': 'application/json'
      },
      json={
          'template_id': 'luxury_vertical',
          'language': 'es',
          'fields': {
              'price': '$450,000',
              'size': '120 m²',
              'bedrooms': 3
          },
          'main_image_url': 'https://example.com/property/exterior.jpg',
          'supporting_image_urls': [
              'https://example.com/property/living-room.jpg',
              'https://example.com/property/kitchen.jpg',
              'https://example.com/property/bedroom.jpg'
          ],
          'logo_image_url': 'https://example.com/agency-logo.png',
          'webhook_url': 'https://yourserver.com/webhooks/vibepeak'
      }
  )

  job = response.json()
  print(f"Job created: {job['job_id']}")
  ```
</RequestExample>

<ResponseExample>
  ```json 202 Accepted theme={null}
  {
    "job_id": "403cd78e-bb56-43e8-8b4d-c64d484f1b69",
    "status": "pending",
    "message": "Property showcase generation request accepted for async processing",
    "request_id": "req_6zx30eGMHakr7QQiiKNFf"
  }
  ```

  ```json 400 Bad Request (Missing required field) theme={null}
  {
    "error": {
      "code": "VALIDATION_ERROR",
      "message": "price is required",
      "details": {
        "field": "fields.price",
        "issues": [
          { "path": "fields.price", "message": "price is required" }
        ]
      },
      "request_id": "req_6zx30eGMHakr7QQiiKNFf"
    }
  }
  ```

  ```json 400 Bad Request (Field not accepted by template) theme={null}
  {
    "error": {
      "code": "VALIDATION_ERROR",
      "message": "label is not a valid field for the info_story template",
      "details": {
        "field": "fields.label",
        "issues": [
          { "path": "fields.label", "message": "label is not a valid field for the info_story template" }
        ]
      },
      "request_id": "req_6zx30eGMHakr7QQiiKNFf"
    }
  }
  ```

  ```json 400 Bad Request (Invalid template) theme={null}
  {
    "error": {
      "code": "VALIDATION_ERROR",
      "message": "Invalid enum value. Expected 'hero_impact' | 'info_story' | 'luxury_vertical' | 'dynamic_collage' | 'ad_performance' | 'cover_blurs' | 'minimalist' | 'magazine' | 'dark_minimalist' | 'tech' | 'luxury_real_estate' | 'modern', received 'nope'",
      "details": {
        "field": "template_id",
        "issues": [
          { "path": "template_id", "message": "Invalid enum value. Expected 'hero_impact' | 'info_story' | 'luxury_vertical' | 'dynamic_collage' | 'ad_performance' | 'cover_blurs' | 'minimalist' | 'magazine' | 'dark_minimalist' | 'tech' | 'luxury_real_estate' | 'modern', received 'nope'" }
        ]
      },
      "request_id": "req_6zx30eGMHakr7QQiiKNFf"
    }
  }
  ```

  ```json 400 Bad Request (Wrong number of supporting images) theme={null}
  {
    "error": {
      "code": "VALIDATION_ERROR",
      "message": "The luxury_vertical template requires exactly 3 supporting images",
      "details": {
        "field": "supporting_image_urls",
        "issues": [
          { "path": "supporting_image_urls", "message": "The luxury_vertical template requires exactly 3 supporting images" }
        ]
      },
      "request_id": "req_6zx30eGMHakr7QQiiKNFf"
    }
  }
  ```

  ```json 400 Bad Request (Invalid URL) theme={null}
  {
    "error": {
      "code": "VALIDATION_ERROR",
      "message": "main_image_url must be a valid URL",
      "details": {
        "field": "main_image_url",
        "issues": [
          { "path": "main_image_url", "message": "main_image_url must be a valid URL" }
        ]
      },
      "request_id": "req_6zx30eGMHakr7QQiiKNFf"
    }
  }
  ```

  ```json 400 Bad Request (Image rejected) theme={null}
  {
    "error": {
      "code": "UNSUPPORTED_IMAGE_FORMAT",
      "message": "main_image_url: Unsupported image type \"text/html\". Allowed: PNG, JPEG, GIF, WebP, BMP, TIFF, AVIF, HEIC, ICO, SVG",
      "request_id": "req_6zx30eGMHakr7QQiiKNFf"
    }
  }
  ```

  ```json 402 Payment Required theme={null}
  {
    "error": {
      "code": "INSUFFICIENT_CREDITS",
      "message": "Not enough credits to process the request.",
      "request_id": "req_6zx30eGMHakr7QQiiKNFf"
    }
  }
  ```

  ```json 415 Unsupported Media Type theme={null}
  {
    "error": {
      "code": "INVALID_CONTENT_TYPE",
      "message": "Request Content-Type must be application/json",
      "request_id": "req_6zx30eGMHakr7QQiiKNFf"
    }
  }
  ```
</ResponseExample>

## Webhook Payload

If you provide a `webhook_url`, VibePeak sends a POST request when the job finishes, carrying `X-VibePeak-Event`, `X-VibePeak-Signature` and `X-VibePeak-Timestamp` headers. Delivery is attempted up to 3 times with exponential backoff; a `4xx` response from your endpoint stops the retries. See [Webhooks](/concepts/webhooks) for signature verification.

### Completed

```json theme={null}
{
  "event": "showcase.completed",
  "job_id": "403cd78e-bb56-43e8-8b4d-c64d484f1b69",
  "status": "succeeded",
  "kind": "generation",
  "template_id": "luxury_vertical",
  "output_image_url": "https://vibepeak-property-showcase-production.s3.eu-west-1.amazonaws.com/showcases/4d7fbd2c-8675-4fa9-aa62-570d4088b8a5/showcase_luxury_vertical_1788184569063.jpg",
  "error": null,
  "created_at": "2026-08-31T13:56:05.223+00:00",
  "completed_at": "2026-08-31T13:56:20.061+00:00"
}
```

### Failed

```json theme={null}
{
  "event": "showcase.failed",
  "job_id": "403cd78e-bb56-43e8-8b4d-c64d484f1b69",
  "status": "failed",
  "kind": "generation",
  "template_id": "luxury_vertical",
  "output_image_url": null,
  "error": "An unexpected error occurred while generating the property showcase. Please try again later.",
  "created_at": "2026-08-31T13:56:05.223+00:00",
  "completed_at": "2026-08-31T13:56:20.061+00:00"
}
```

<Note>
  On failure `error` is always the fixed string above — the underlying provider message is never exposed. Use `request_id` from the create call when contacting support.
</Note>

## Error Codes

| Code                         | Status | Description                                                                 |
| ---------------------------- | ------ | --------------------------------------------------------------------------- |
| `VALIDATION_ERROR`           | 400    | Invalid request parameters or missing/invalid per-template fields           |
| `INVALID_IMAGE_URL`          | 400    | An image URL is malformed or uses a non-http(s) scheme                      |
| `IMAGE_INACCESSIBLE`         | 400    | An image URL could not be fetched                                           |
| `IMAGE_TIMEOUT`              | 400    | Fetching an image URL timed out                                             |
| `UNSUPPORTED_IMAGE_FORMAT`   | 400    | Image is not an accepted format (see Accepted formats above)                |
| `IMAGE_TOO_LARGE`            | 400    | Image exceeds the 8 MB size limit                                           |
| `SSRF_BLOCKED`               | 400    | An image URL points to a private/internal network                           |
| `INVALID_JSON`               | 400    | Request body is not valid JSON                                              |
| `INVALID_API_KEY`            | 401    | Invalid or missing API key                                                  |
| `INSUFFICIENT_CREDITS`       | 402    | Not enough credits to process the request                                   |
| `PLAN_REQUIRED`              | 403    | Plan doesn't include API access                                             |
| `INVALID_CONTENT_TYPE`       | 415    | `Content-Type` header is missing or is not `application/json`               |
| `CONCURRENCY_LIMIT_EXCEEDED` | 429    | Concurrent job limit reached                                                |
| `EXTERNAL_SERVICE_ERROR`     | 502    | The image generation service could not be reached; the job is marked failed |

<Note>
  **Error shape.** `VALIDATION_ERROR` responses carry a `details` object of the form `{ "field": "<dotted.path>", "issues": [{ "path": "<dotted.path>", "message": "..." }] }` — `field` is the first failing path and `issues` lists every failure. All other error codes (including the image codes above) return no `details`.
</Note>

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

## Credits

This endpoint charges **2 credits** per request for accounts **without an annual plan**. Accounts on an **annual plan are not charged** (0 credits). Credits are charged upon successful job creation and are not refunded if generation fails.

<Warning>
  **Test mode is not supported on this endpoint.** Unlike the video endpoints, a `vpk_test_` key is not sandboxed here — the request runs the real generation pipeline and charges credits. Use it only when you intend to generate a real image. See [Test Mode](/concepts/test-mode).
</Warning>

## Next Steps

After creating a job:

1. **Wait for the webhook**: your `webhook_url` receives a `showcase.completed` or `showcase.failed` notification. Verify the `X-VibePeak-Signature` header as described in [Webhooks](/concepts/webhooks).
2. **Download the image**: read `output_image_url` from the `showcase.completed` payload.

<Warning>
  There is no polling endpoint for showcase jobs. [Get Task](/api-reference/videos/get-task) serves video tasks only and returns `404 TASK_NOT_FOUND` for a showcase `job_id`. If you do not set a `webhook_url`, the generated image cannot be retrieved.
</Warning>
