> ## 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.

# Custom Templates

> Register your own reference design as a property showcase template, then generate showcases that keep its layout

A custom template turns generation into an **edit of a design you already own**: a brochure, a flyer, a portal card or any finished layout. You register it once with a public `reference_image_url`, VibePeak analyzes it into a structural `spec` (the photo slots it holds, its logo area, the text fields it prints, its colour palette, its typography and its background) and returns a `confidence` score with the result. You can correct anything the analysis got wrong, or skip the analysis entirely by sending your own `spec`.

Every showcase created with `template_id: "custom"` and that `custom_template_id` replaces the photos, the logo and the text of the reference with yours while keeping its layout, its proportions and its style.

<Note>
  **Requirements.** Custom templates are available on the **Pro**, **Max** and **Enterprise** plans. A key on any other plan is rejected with `403 PLAN_REQUIRED`, whose `details` carry `current_plan` and `required_plans: ["Pro", "Max", "Enterprise"]`. The gate applies to [Create](#create) and to every showcase generated with `template_id: "custom"`.

  Each account keeps at most **10 active templates**. Creating an eleventh returns `409 CUSTOM_TEMPLATE_LIMIT_REACHED` with `details.max`. Archive one to make room; archived templates never count towards the limit.
</Note>

Creation is **synchronous**: the call returns `201` with the stored template. There is no job, no webhook and **no credit cost** on any of the five operations below. Credits are only spent when you render a showcase with the template, at the usual [2 credits per showcase](/api-reference/images/create-property-showcase#credits).

<Warning>
  **Keep the reference image reachable.** The reference is not copied into VibePeak: every generation reads `reference_image_url` again at render time. If the URL stops resolving, showcases built on that template fail their image checks. Send `reference_image_url` on the [showcase request](/api-reference/images/create-property-showcase) to point a single generation at a different copy of the design without changing the stored template.
</Warning>

## How it works

<Steps>
  <Step title="Register the design">
    `POST` the reference URL and a name. VibePeak analyzes the image and stores the detected `spec`, its `confidence` and the `aspect_ratio` it was authored at.
  </Step>

  <Step title="Review the spec">
    Read `spec.image_slots`, `spec.logo_area` and `spec.text_fields`: they define exactly how many photos a showcase must carry, whether a logo is accepted, and which property fields are required. The analysis also fills `spec.text_sources` with the exact text it read for each field; correct it with a `PATCH` if it misread the design. A `confidence` below `0.7` means the detection deserves a careful read.
  </Step>

  <Step title="Correct it if needed">
    `PATCH` the template with a full `spec` to fix a slot, a missing text field or the wrong aspect ratio. The stored spec is what every later generation obeys.
  </Step>

  <Step title="Generate">
    [Create a showcase](/api-reference/images/create-property-showcase) with `template_id: "custom"` and `custom_template_id`, supplying exactly the photos and fields the spec declares.
  </Step>
</Steps>

## Create

`POST /v1/real-estate/property-showcase/custom-templates`

Registers a reference design. With no `spec` in the body, the reference image is analyzed and the detected layout is stored. With a `spec`, the analysis is skipped, your layout is stored as-is and `confidence` comes back as `1`.

The reference URL is validated before any analysis runs: it must be publicly reachable, be an accepted image format and weigh at most 8 MB. A failure returns the image error code on the `reference_image_url` field, with the field name prefixed onto the message, e.g. `"reference_image_url: Image exceeds the 8 MB limit"`.

### Request body

<ParamField body="name" type="string" required>
  Your label for the template, 1 to 80 characters. Leading and trailing whitespace is trimmed; an empty or whitespace-only name returns `"name is required"` and a longer one returns `"name must be at most 80 characters"`.
</ParamField>

<ParamField body="reference_image_url" type="string" required>
  Publicly reachable URL of the design to reproduce, at most 2048 characters. For analysis it must be a **PNG, JPEG or WebP** of at most 8 MB; an image VibePeak cannot read as a layout returns `400 INVALID_REFERENCE_IMAGE`. The URL is stored and read again on every generation, so it must stay reachable.
</ParamField>

<ParamField body="spec" type="object">
  Optional layout description. Send it to skip the analysis and store exactly the layout you describe. Omit it to have VibePeak analyze the reference image for you. When you send it, `confidence` is returned as `1`.

  <Expandable title="spec properties">
    <ParamField body="version" type="integer" required>
      Spec format version. Always `1`.
    </ParamField>

    <ParamField body="aspect_ratio" type="string" required>
      Orientation the design is authored at, and the ratio every showcase built on it renders at. One of `9:16`, `16:9`, `1:1`, `4:5`, `5:4`, `3:4`, `4:3`, `3:2`, `2:3`. A showcase request cannot override it: `aspect_ratio` is rejected on `template_id: "custom"`.
    </ParamField>

    <ParamField body="layout_type" type="string" required>
      Free-text summary of the composition, e.g. `"vertical flyer with a hero photo and a bottom data band"`. At most 120 characters.
    </ParamField>

    <ParamField body="image_slots" type="object[]" required>
      The photo areas of the design, 1 to 4 entries, in reading order. **The first slot is the main photo** and is filled by `main_image_url`; every other slot is filled, in order, by `supporting_image_urls`. A showcase must therefore carry exactly `image_slots.length - 1` supporting images.

      <Expandable title="Slot properties">
        <ParamField body="id" type="string" required>
          Identifier of the slot, matching `^[a-z][a-z0-9_]{0,23}$` (lowercase letter first, then lowercase letters, digits or underscores, up to 24 characters). Must be unique inside `image_slots`, otherwise the request is rejected with `"image_slots ids must be unique"`.
        </ParamField>

        <ParamField body="position" type="string" required>
          Where the slot sits in the design, e.g. `"top two thirds, full width"`. 1 to 120 characters.
        </ParamField>

        <ParamField body="role" type="string" required>
          What the slot shows, e.g. `"main exterior photo"` or `"kitchen detail"`. 1 to 120 characters.
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField body="logo_area" type="object | null" required>
      The area the design reserves for a logo, or `null` when it has none. This is the contract for `logo_image_url` on a showcase: with a `logo_area`, the logo is **required**; with `null`, sending one is **rejected**.

      <Expandable title="Logo area properties">
        <ParamField body="position" type="string" required>
          Where the logo sits, e.g. `"bottom left of the data band"`. 1 to 120 characters.
        </ParamField>

        <ParamField body="shape" type="string" required>
          One of `rectangular`, `circular` or `other`.
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField body="text_fields" type="string[]" required>
      The property data the design prints, as a unique subset of `price`, `size`, `label`, `bedrooms`, `bathrooms`, `parking`. This is the complete allow-list for `fields` on a showcase, and **every listed field is required**. A repeated entry is rejected with `"text_fields must be unique"`. An empty array is valid and means the design prints no property data.
    </ParamField>

    <ParamField body="text_sources" type="object">
      Optional map from a field id to the exact visible text of that field's value in the reference, with no fixed caption, e.g. `{ "price": "450.000 €", "bedrooms": "4" }` rather than `{ "price": "Precio de Venta: 450.000 €" }`. Without a key, that field is located by its role in the design instead of a quoted match; correct a misread value with [Update](#update).

      Every key must also appear in `text_fields`, or the request is rejected with `"text_sources keys must be listed in text_fields"`. Each value must be a single line, free of control characters, from 1 to 120 characters.
    </ParamField>

    <ParamField body="palette" type="string[]" required>
      Up to 4 dominant colours of the design as `#RRGGBB` hex strings, e.g. `["#0B2545", "#FFFFFF"]`. May be empty.
    </ParamField>

    <ParamField body="typography_style" type="string" required>
      Free-text description of the lettering, e.g. `"condensed sans-serif headings, light body"`. At most 120 characters.
    </ParamField>

    <ParamField body="background" type="string" required>
      Free-text description of the backdrop, e.g. `"solid navy with a thin gold rule"`. At most 120 characters.
    </ParamField>
  </Expandable>
</ParamField>

### Response

<ResponseField name="custom_template_id" type="string" required>
  Identifier of the stored template. Pass it as `custom_template_id` when you create a showcase.
</ResponseField>

<ResponseField name="name" type="string" required>
  The stored name, trimmed.
</ResponseField>

<ResponseField name="spec" type="object" required>
  The stored layout, in the shape documented above. It is what every generation with this template obeys. When the analysis ran, `spec.text_sources` carries the original text it read for each field; when you supplied the `spec` yourself, it carries back whatever you sent.
</ResponseField>

<ResponseField name="confidence" type="number" required>
  How confident the analysis is that it read the design correctly, from `0` to `1`. Always `1` when you supplied the `spec` yourself.

  A value **below `0.7`** is not a failure: the template is stored and usable. It means the slots, the logo area or the text fields were hard to identify, so read the spec before you generate and correct it with [Update](#update) if needed.
</ResponseField>

<ResponseField name="aspect_ratio" type="string" required>
  Mirror of `spec.aspect_ratio`, the ratio showcases built on this template render at.
</ResponseField>

<ResponseField name="warnings" type="string[]" required>
  Non-blocking notes about the analysis. Empty when you supplied the `spec`. Currently one value is emitted:

  * `image_slots_truncated`: the design appears to hold more than 4 photo areas and only the first 4 were kept. The template works, but it reproduces fewer photos than the reference. Check `spec.image_slots` and adjust it with [Update](#update) if the wrong areas were kept.
</ResponseField>

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

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

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.vibepeak.ai/v1/real-estate/property-showcase/custom-templates \
    -H "Authorization: Bearer vpk_live_xxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Agency flyer 2026",
      "reference_image_url": "https://example.com/designs/agency-flyer.png"
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch('https://api.vibepeak.ai/v1/real-estate/property-showcase/custom-templates', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer vpk_live_xxxxx',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      name: 'Agency flyer 2026',
      reference_image_url: 'https://example.com/designs/agency-flyer.png'
    })
  });

  const template = await response.json();
  console.log(`Template ${template.custom_template_id} (confidence ${template.confidence})`);
  console.log(`Supporting images required: ${template.spec.image_slots.length - 1}`);
  ```

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

  response = requests.post(
      'https://api.vibepeak.ai/v1/real-estate/property-showcase/custom-templates',
      headers={
          'Authorization': 'Bearer vpk_live_xxxxx',
          'Content-Type': 'application/json'
      },
      json={
          'name': 'Agency flyer 2026',
          'reference_image_url': 'https://example.com/designs/agency-flyer.png'
      }
  )

  template = response.json()
  print(f"Template {template['custom_template_id']} (confidence {template['confidence']})")
  print(f"Supporting images required: {len(template['spec']['image_slots']) - 1}")
  ```
</CodeGroup>

```json 201 Created theme={null}
{
  "custom_template_id": "8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11",
  "name": "Agency flyer 2026",
  "spec": {
    "version": 1,
    "aspect_ratio": "9:16",
    "layout_type": "vertical flyer with a hero photo and a bottom data band",
    "image_slots": [
      { "id": "hero", "position": "top two thirds, full width", "role": "main exterior photo" },
      { "id": "detail_left", "position": "bottom left thumbnail", "role": "interior detail" },
      { "id": "detail_right", "position": "bottom right thumbnail", "role": "interior detail" }
    ],
    "logo_area": { "position": "bottom left of the data band", "shape": "rectangular" },
    "text_fields": ["price", "size", "bedrooms"],
    "text_sources": { "price": "450.000 €", "bedrooms": "4" },
    "palette": ["#0B2545", "#FFFFFF", "#C8A45C"],
    "typography_style": "condensed sans-serif headings, light body",
    "background": "solid navy with a thin gold rule"
  },
  "confidence": 0.86,
  "aspect_ratio": "9:16",
  "warnings": [],
  "created_at": "2026-09-12T10:04:11.482+00:00",
  "request_id": "req_6zx30eGMHakr7QQiiKNFf"
}
```

### Errors

| Code | Status | Description |
| - | - | - |
| `VALIDATION_ERROR` | 400 | Missing or malformed `name`, `reference_image_url` or `spec` |
| `INVALID_REFERENCE_IMAGE` | 400 | The reference image could not be analyzed (unreadable, unsupported type, too large or unreachable) |
| `INVALID_IMAGE_URL` | 400 | `reference_image_url` is malformed or uses a non-http(s) scheme |
| `IMAGE_INACCESSIBLE` | 400 | `reference_image_url` could not be fetched |
| `IMAGE_TIMEOUT` | 400 | Fetching `reference_image_url` timed out |
| `UNSUPPORTED_IMAGE_FORMAT` | 400 | `reference_image_url` is not an accepted image format |
| `IMAGE_TOO_LARGE` | 400 | The reference image exceeds the 8 MB limit |
| `SSRF_BLOCKED` | 400 | `reference_image_url` points to a private/internal network |
| `INVALID_API_KEY` | 401 | Invalid or missing API key |
| `PLAN_REQUIRED` | 403 | The plan does not include custom templates (`details.required_plans`) |
| `CUSTOM_TEMPLATE_LIMIT_REACHED` | 409 | 10 active templates already exist (`details.max`) |
| `INVALID_CONTENT_TYPE` | 415 | `Content-Type` header is missing or is not `application/json` |
| `CUSTOM_TEMPLATE_CREATE_FAILED` | 500 | The template could not be stored |
| `ANALYSIS_UNAVAILABLE` | 502 | The analysis is temporarily unavailable; retry later or send your own `spec` |

```json 409 Conflict (Limit reached) theme={null}
{
  "error": {
    "code": "CUSTOM_TEMPLATE_LIMIT_REACHED",
    "message": "You can keep at most 10 custom templates. Archive one to create another.",
    "details": {
      "max": 10
    },
    "request_id": "req_6zx30eGMHakr7QQiiKNFf"
  }
}
```

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

## List

`GET /v1/real-estate/property-showcase/custom-templates`

Returns your active templates, newest first, with how many more you may create. Archived templates are not listed; read one by id with [Get](#get).

### Response

<ResponseField name="custom_templates" type="object[]" required>
  Your active templates.

  <Expandable title="Template properties">
    <ResponseField name="custom_template_id" type="string" required>
      Identifier to pass as `custom_template_id` on a showcase request.
    </ResponseField>

    <ResponseField name="name" type="string" required>
      Your label for the template.
    </ResponseField>

    <ResponseField name="reference_image_url" type="string" required>
      The stored reference design, read again on every generation.
    </ResponseField>

    <ResponseField name="spec" type="object" required>
      The stored layout, in the shape documented under [Create](#create).
    </ResponseField>

    <ResponseField name="confidence" type="number" required>
      Confidence of the analysis that produced the spec, `0` to `1`. `1` for a spec you supplied.
    </ResponseField>

    <ResponseField name="aspect_ratio" type="string" required>
      Mirror of `spec.aspect_ratio`.
    </ResponseField>

    <ResponseField name="archived_at" type="string | null" required>
      Always `null` in this list; archived templates are excluded.
    </ResponseField>

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

    <ResponseField name="updated_at" type="string" required>
      ISO 8601 timestamp of the last change.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="remaining" type="integer" required>
  How many more templates you may create, that is `max` minus the number of active templates. Never negative.
</ResponseField>

<ResponseField name="max" type="integer" required>
  Maximum number of active templates per account. Currently `10`.
</ResponseField>

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

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.vibepeak.ai/v1/real-estate/property-showcase/custom-templates \
    -H "Authorization: Bearer vpk_live_xxxxx"
  ```

  ```javascript Node.js theme={null}
  const response = await fetch('https://api.vibepeak.ai/v1/real-estate/property-showcase/custom-templates', {
    headers: { 'Authorization': 'Bearer vpk_live_xxxxx' }
  });

  const { custom_templates, remaining, max } = await response.json();
  console.log(`${custom_templates.length} templates, ${remaining} of ${max} slots left`);
  ```

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

  response = requests.get(
      'https://api.vibepeak.ai/v1/real-estate/property-showcase/custom-templates',
      headers={'Authorization': 'Bearer vpk_live_xxxxx'}
  )

  data = response.json()
  print(f"{len(data['custom_templates'])} templates, {data['remaining']} of {data['max']} slots left")
  ```
</CodeGroup>

```json 200 OK theme={null}
{
  "custom_templates": [
    {
      "custom_template_id": "8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11",
      "name": "Agency flyer 2026",
      "reference_image_url": "https://example.com/designs/agency-flyer.png",
      "spec": {
        "version": 1,
        "aspect_ratio": "9:16",
        "layout_type": "vertical flyer with a hero photo and a bottom data band",
        "image_slots": [
          { "id": "hero", "position": "top two thirds, full width", "role": "main exterior photo" },
          { "id": "detail_left", "position": "bottom left thumbnail", "role": "interior detail" },
          { "id": "detail_right", "position": "bottom right thumbnail", "role": "interior detail" }
        ],
        "logo_area": { "position": "bottom left of the data band", "shape": "rectangular" },
        "text_fields": ["price", "size", "bedrooms"],
        "palette": ["#0B2545", "#FFFFFF", "#C8A45C"],
        "typography_style": "condensed sans-serif headings, light body",
        "background": "solid navy with a thin gold rule"
      },
      "confidence": 0.86,
      "aspect_ratio": "9:16",
      "archived_at": null,
      "created_at": "2026-09-12T10:04:11.482+00:00",
      "updated_at": "2026-09-12T10:04:11.482+00:00"
    }
  ],
  "remaining": 9,
  "max": 10,
  "request_id": "req_6zx30eGMHakr7QQiiKNFf"
}
```

### Errors

| Code | Status | Description |
| - | - | - |
| `INVALID_API_KEY` | 401 | Invalid or missing API key |
| `CUSTOM_TEMPLATE_READ_FAILED` | 500 | The templates could not be read |

## Get

`GET /v1/real-estate/property-showcase/custom-templates/{id}`

Reads one template, archived ones included. A template that belongs to another account is reported as missing, never as forbidden.

<ParamField path="id" type="string" required>
  Identifier of the template, as returned by [Create](#create) or [List](#list).
</ParamField>

### Response

<ResponseField name="custom_template" type="object" required>
  The template, in the shape documented under [List](#list). `archived_at` carries the archive timestamp when the template has been archived, and `null` otherwise.
</ResponseField>

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

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.vibepeak.ai/v1/real-estate/property-showcase/custom-templates/8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11 \
    -H "Authorization: Bearer vpk_live_xxxxx"
  ```

  ```javascript Node.js theme={null}
  const templateId = '8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11';

  const response = await fetch(`https://api.vibepeak.ai/v1/real-estate/property-showcase/custom-templates/${templateId}`, {
    headers: { 'Authorization': 'Bearer vpk_live_xxxxx' }
  });

  const { custom_template } = await response.json();
  console.log(custom_template.spec.text_fields);
  ```

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

  template_id = '8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11'

  response = requests.get(
      f'https://api.vibepeak.ai/v1/real-estate/property-showcase/custom-templates/{template_id}',
      headers={'Authorization': 'Bearer vpk_live_xxxxx'}
  )

  print(response.json()['custom_template']['spec']['text_fields'])
  ```
</CodeGroup>

```json 200 OK theme={null}
{
  "custom_template": {
    "custom_template_id": "8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11",
    "name": "Agency flyer 2026",
    "reference_image_url": "https://example.com/designs/agency-flyer.png",
    "spec": {
      "version": 1,
      "aspect_ratio": "9:16",
      "layout_type": "vertical flyer with a hero photo and a bottom data band",
      "image_slots": [
        { "id": "hero", "position": "top two thirds, full width", "role": "main exterior photo" },
        { "id": "detail_left", "position": "bottom left thumbnail", "role": "interior detail" },
        { "id": "detail_right", "position": "bottom right thumbnail", "role": "interior detail" }
      ],
      "logo_area": { "position": "bottom left of the data band", "shape": "rectangular" },
      "text_fields": ["price", "size", "bedrooms"],
      "palette": ["#0B2545", "#FFFFFF", "#C8A45C"],
      "typography_style": "condensed sans-serif headings, light body",
      "background": "solid navy with a thin gold rule"
    },
    "confidence": 0.86,
    "aspect_ratio": "9:16",
    "archived_at": null,
    "created_at": "2026-09-12T10:04:11.482+00:00",
    "updated_at": "2026-09-12T10:04:11.482+00:00"
  },
  "request_id": "req_6zx30eGMHakr7QQiiKNFf"
}
```

### Errors

| Code | Status | Description |
| - | - | - |
| `INVALID_API_KEY` | 401 | Invalid or missing API key |
| `NOT_FOUND` | 404 | No such template, or it belongs to another account |

## Update

`PATCH /v1/real-estate/property-showcase/custom-templates/{id}`

Renames a template, replaces its layout, or both. The reference image is immutable: to change the design, register a new template.

<ParamField path="id" type="string" required>
  Identifier of the template to update.
</ParamField>

### Request body

At least one of `name` or `spec` is required. An empty body returns a `VALIDATION_ERROR` on the `name` path with the message `"At least one of name or spec is required"`.

<ParamField body="name" type="string">
  New label for the template, 1 to 80 characters.
</ParamField>

<ParamField body="spec" type="object">
  Replacement layout, **complete** and in the shape documented under [Create](#create). It is not merged with the stored one: what you send becomes the spec. A new `aspect_ratio` inside it also moves the ratio showcases render at.

  This is how you correct an analysis: read the stored spec, fix the slot, the text field or the logo area that is wrong, and send the whole object back.
</ParamField>

### Response

<ResponseField name="custom_template" type="object" required>
  The updated template, in the shape documented under [List](#list). `confidence` keeps the value of the analysis that produced the original spec.
</ResponseField>

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

<CodeGroup>
  ```bash cURL theme={null}
  curl -X PATCH https://api.vibepeak.ai/v1/real-estate/property-showcase/custom-templates/8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11 \
    -H "Authorization: Bearer vpk_live_xxxxx" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Agency flyer 2026 (revised)"
    }'
  ```

  ```javascript Node.js theme={null}
  const templateId = '8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11';

  const response = await fetch(`https://api.vibepeak.ai/v1/real-estate/property-showcase/custom-templates/${templateId}`, {
    method: 'PATCH',
    headers: {
      'Authorization': 'Bearer vpk_live_xxxxx',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ name: 'Agency flyer 2026 (revised)' })
  });

  const { custom_template } = await response.json();
  console.log(custom_template.updated_at);
  ```

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

  template_id = '8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11'

  response = requests.patch(
      f'https://api.vibepeak.ai/v1/real-estate/property-showcase/custom-templates/{template_id}',
      headers={
          'Authorization': 'Bearer vpk_live_xxxxx',
          'Content-Type': 'application/json'
      },
      json={'name': 'Agency flyer 2026 (revised)'}
  )

  print(response.json()['custom_template']['updated_at'])
  ```
</CodeGroup>

### Errors

| Code | Status | Description |
| - | - | - |
| `VALIDATION_ERROR` | 400 | Empty body, or a malformed `name` or `spec` |
| `INVALID_API_KEY` | 401 | Invalid or missing API key |
| `NOT_FOUND` | 404 | No such template, or it belongs to another account |
| `CUSTOM_TEMPLATE_ARCHIVED` | 409 | The template is archived and can no longer be edited |
| `INVALID_CONTENT_TYPE` | 415 | `Content-Type` header is missing or is not `application/json` |
| `CUSTOM_TEMPLATE_UPDATE_FAILED` | 500 | The template could not be updated |

## Delete (archive)

`DELETE /v1/real-estate/property-showcase/custom-templates/{id}`

Archives a template. Nothing is erased: the row is kept with an `archived_at` timestamp, and the showcases you already generated with it are unaffected.

An archived template stops counting towards the limit of 10 and disappears from [List](#list), but stays readable by id through [Get](#get). It can no longer be updated, and a new showcase that names it is rejected with `409 CUSTOM_TEMPLATE_ARCHIVED`.

The call is **idempotent**: archiving a template that is already archived returns the same `archived_at` and changes nothing.

<ParamField path="id" type="string" required>
  Identifier of the template to archive.
</ParamField>

### Response

<ResponseField name="custom_template_id" type="string" required>
  Identifier of the archived template.
</ResponseField>

<ResponseField name="archived_at" type="string" required>
  ISO 8601 timestamp of the archive. Unchanged on a repeated call.
</ResponseField>

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

<CodeGroup>
  ```bash cURL theme={null}
  curl -X DELETE https://api.vibepeak.ai/v1/real-estate/property-showcase/custom-templates/8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11 \
    -H "Authorization: Bearer vpk_live_xxxxx"
  ```

  ```javascript Node.js theme={null}
  const templateId = '8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11';

  const response = await fetch(`https://api.vibepeak.ai/v1/real-estate/property-showcase/custom-templates/${templateId}`, {
    method: 'DELETE',
    headers: { 'Authorization': 'Bearer vpk_live_xxxxx' }
  });

  const result = await response.json();
  console.log(`Archived at ${result.archived_at}`);
  ```

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

  template_id = '8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11'

  response = requests.delete(
      f'https://api.vibepeak.ai/v1/real-estate/property-showcase/custom-templates/{template_id}',
      headers={'Authorization': 'Bearer vpk_live_xxxxx'}
  )

  print(f"Archived at {response.json()['archived_at']}")
  ```
</CodeGroup>

```json 200 OK theme={null}
{
  "custom_template_id": "8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11",
  "archived_at": "2026-09-14T08:21:40.117+00:00",
  "request_id": "req_6zx30eGMHakr7QQiiKNFf"
}
```

### Errors

| Code | Status | Description |
| - | - | - |
| `INVALID_API_KEY` | 401 | Invalid or missing API key |
| `NOT_FOUND` | 404 | No such template, or it belongs to another account |
| `CUSTOM_TEMPLATE_UPDATE_FAILED` | 500 | The template could not be archived |

## Generating with a custom template

[Create a showcase](/api-reference/images/create-property-showcase) with `template_id: "custom"` and the `custom_template_id` of a template you own. The stored spec decides the request:

* **Fields.** `fields` accepts exactly the spec's `text_fields`, and **every one of them is required**. A field the spec does not declare returns `"<field> is not a valid field for the custom template"`; a declared field that is missing or empty returns `"<field> is required for this custom template"`.
* **Photos.** `main_image_url` fills the first slot and `supporting_image_urls` fills the rest, so the request carries exactly `image_slots.length - 1` supporting images. Any other count returns `"This custom template requires exactly N supporting images"`.
* **Logo.** With a `logo_area`, `logo_image_url` is required (`"This custom template requires logo_image_url"`). Without one, sending it returns `"logo_image_url is not supported by this custom template"`.
* **Orientation.** The showcase renders at the spec's `aspect_ratio`. Sending `aspect_ratio` or `brand_profile_id` on a custom showcase is rejected.
* **Optional overrides.** `reference_image_url` points this single generation at a different copy of the design, and `custom_instruction` (up to 250 characters) appends a free-text tweak to the edit instruction.

Generation is asynchronous and behaves exactly like any other showcase: `202 Accepted`, then the finished image arrives at your `webhook_url` as `showcase.completed`. It costs the usual 2 credits for accounts without an annual plan.

<CodeGroup>
  ```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": "custom",
      "custom_template_id": "8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11",
      "custom_instruction": "Keep the price in the gold accent colour",
      "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"
      ],
      "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: 'custom',
      custom_template_id: '8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11',
      custom_instruction: 'Keep the price in the gold accent colour',
      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'
      ],
      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': 'custom',
          'custom_template_id': '8f1c3b2a-9d4e-4a71-93bb-5a2f6c0d7e11',
          'custom_instruction': 'Keep the price in the gold accent colour',
          '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'
          ],
          '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']}")
  ```
</CodeGroup>

<Note>
  **Error shape.** `VALIDATION_ERROR` responses carry a `details` object of the form `{ "field": "<dotted.path>", "issues": [{ "path": "<dotted.path>", "message": "..." }] }`, where `field` is the first failing path and `issues` lists every failure. `CUSTOM_TEMPLATE_LIMIT_REACHED` carries `details.max` and `PLAN_REQUIRED` carries `details.current_plan` and `details.required_plans`; the image error codes and the remaining codes carry no `details`. See [Error Handling](/concepts/error-handling).
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.