# Decompose an ad into layers

Turn a flat ad image into editable layers, returned as one structured JSON document: a background plate, product and logo cutouts, live text with typography, and vector shapes. Trained exclusively on licensed data for safe commercial use. Latency is on average under 1 minute.
**How it works**
1. Submit the ad. The response carries a `request_id` and a `status_url`.
2. Poll the `status_url` ([status endpoint](/status)) or pass a `webhook_url` ([webhooks](/getting-started/async-requests#webhooks)). Both deliver the completion payload shown under **Callbacks** below.
3. On completion, `result.url` points to the hosted layer document; each image layer's `asset_path` is a hosted file. The document and its assets stay downloadable for 30 days.

**Input assumptions**
The input is assumed to be a flat ad. Ordinary photographs are out of scope and the result is not guaranteed. Unknown fields are currently ignored rather than rejected, so a misspelled parameter silently falls back to its default. Submissions are not idempotent: a retried POST is a new job. Failed jobs are never billed.
**Reporting a problem**
The `request_id` in an error body identifies that response, not your job. The durable handle is the id inside the `status_url` you received at submit; quote that.
**The layer document**
The field-by-field contract (document, layer, image, vector and text fields), the two halves of every copy layer, a real example and a rendering guide are in the [Ad Delayer guide](/ads).
The Ad Delayer turns a finished, flat ad image into its editable parts: a background plate, product and logo cutouts, live text with full typography, and vector shapes. The result is one JSON layer document you can render, edit, resize or localize programmatically. Use it to recover a lost design file, make a shipped ad editable again, or feed an ad into your own templating pipeline.
| You send | You get back |
|  --- | --- |
| One flat ad image (PNG, JPEG or WEBP) | A hosted layer document: canvas size, the web fonts it needs, and a z-ordered list of layers with bounding boxes and type-specific styling; image layers as hosted transparent PNGs |

### How a job runs
1. **Submit** the ad. The API returns a `request_id` and a `status_url` immediately.
2. **Wait** for completion by polling the `status_url` (the shared [status endpoint](/status)) every few seconds, or pass a `webhook_url` and let Bria POST the result to you ([webhooks](/getting-started/async-requests#webhooks)). Both deliver the same body.
3. **Fetch the layer document.** The completed status body carries a pointer, not the layers: `result.url` is the JSON document.
4. **Fetch assets as needed.** Each image layer's `asset_path` is a hosted file; download the ones you render.

**Python SDK**

```python
import httpx
from bria_client import BriaSyncClient

client = BriaSyncClient()  # reads BRIA_API_TOKEN

job = client.submit(
    endpoint="ads/delayer",
    payload={"attachments": ["https://example.com/ads/spring-sale.jpg"]},
)
status = client.poll(job, interval=5, timeout=600)

doc = httpx.get(status.result.url, timeout=30).json()

layers = sorted((l for l in doc["layers"] if l.get("hidden") is not True), key=lambda l: l["z_order"])
for layer in layers:
    print(layer["z_order"], layer["id"], layer["type"], layer["subtype"], layer["bbox"])
```
**TypeScript SDK**

```typescript
import { BriaClient } from "@bria-ai/client";

const client = new BriaClient(); // reads BRIA_API_TOKEN

// 1 + 2. Submit and wait
const job = await client.submit("ads/delayer", { attachments: ["https://example.com/ads/spring-sale.jpg"] });
const status = await client.poll(job, { interval: 5, timeout: 600 });

// 3. Fetch the layer document
type Layer = { id: string; type: string; subtype: string; z_order: number; bbox: number[]; hidden?: boolean };
const doc: { layers: Layer[] } = await (await fetch(String(status.result?.url))).json();

// 4. Work with the visible layers in paint order
const layers = doc.layers.filter((l) => l.hidden !== true).sort((a, b) => a.z_order - b.z_order);
for (const layer of layers) console.log(layer.z_order, layer.id, layer.type, layer.subtype, layer.bbox);
```
**cURL**

```bash
curl -s -X POST https://engine.prod.bria-api.com/v2/ads/delayer \
  -H "api_token: $BRIA_API_TOKEN" -H "Content-Type: application/json" \
  -d '{"attachments": ["https://example.com/ads/spring-sale.jpg"]}'

curl -s "https://engine.prod.bria-api.com/v2/status/<request_id>" -H "api_token: $BRIA_API_TOKEN"

curl -s "<result.url>" | jq '.layers | map(select(.hidden != true)) | sort_by(.z_order) | .[] | {z_order, id, type, subtype}'
```
The completed status body looks like this:

```json
{
  "request_id": "860f1a2b73f847e59f284d6f860f2ddb",
  "status": "COMPLETED",
  "result": {
    "status": "completed",
    "text": "Constructed a layered ad.",
    "url": "https://temp.bria.ai/results/860f1a2b73f847e59f284d6f860f2ddb/creation.json"
  }
}
```
Two details specific to this endpoint:
- An unrecognized or expired `request_id` is reported as `"status": "UNKNOWN"` with HTTP `200`. Treat it as not found and stop polling.
- The `request_id` inside an error body identifies that response, not your job. When reporting a problem, quote the id inside the `status_url` you received at submit.

### Request essentials
| Parameter | Description |
|  --- | --- |
| `attachments` | Exactly one image: a public URL or a `data:image/…;base64,…` URI. PNG, JPEG or WEBP, up to 10 MB and 1350 px per dimension (no dimension cap on Enterprise). Required. |
| `webhook_url` | Optional. Receive the completion body by POST instead of polling. |

Inputs are assumed to be flat ads. Ordinary photographs are out of scope and results for them are not guaranteed. Unknown fields are currently ignored rather than rejected, so a misspelled parameter silently falls back to its default. Submissions are not idempotent: a retried POST is a new job. Failed jobs are never billed.
### The layer document

```json
{
  "canvas": { "width": 1000, "height": 1500 },
  "font_stylesheets": ["https://fonts.googleapis.com/css2?family=Inter…"],
  "layers": [ ... ]
}
```
| Field | Type | Description |
|  --- | --- | --- |
| `canvas` | object | `width` and `height`, matching the input image. |
| `font_stylesheets` | `string[]` | Google Fonts stylesheet URLs covering every font referenced by text layers. Load them before rendering text. May be omitted when no web font is needed, so read it defensively. |
| `layers` | array | Every layer in the ad. Sort by `z_order` before rendering; array order is not guaranteed to match paint order. |

#### Fields on every layer
| Field | Type | Description |
|  --- | --- | --- |
| `id` | string | Result-scoped: `background_1`, `text_3`, `image_1`, `shape_1`, plus the literal `canvas_background`. A copy layer arrives as a pair suffixed `_font` and `_svg`. |
| `type` | string | `image`, `vector` or `text`. |
| `subtype` | string | `background`, `logo`, `product`, `primary_copy`, `secondary_copy`, `cta`, `compliance`, `icon`, `decorative`. |
| `bbox` | object | `x`, `y`, `width`, `height` in canvas pixels. |
| `z_order` | integer | Paint order from 0 upward with no gaps; lowest paints first. Each half of a copy pair takes its own number, so gaps appear once hidden layers are dropped. |
| `hidden` | boolean | Present and `true` only on the inactive half of a copy pair. Skip those layers. Absent means visible; it is never `false`, so test for `hidden === true`. |

#### Image layers
| Field | Type | Description |
|  --- | --- | --- |
| `asset_path` | string | Hosted image. Cutouts are transparent PNGs; the `_svg` half of a copy pair is an SVG of the traced letterforms. |
| `image_fit.object_fit` | string | `cover` or `contain`, with CSS `object-fit` semantics. |
| `image_fit.object_position_x`, `image_fit.object_position_y` | string | Optional. Where the image sits when the fit leaves slack: `left`, `center`, `right` and `top`, `center`, `bottom`. Absent means centered. |

#### Vector layers
Vector layers carry no shape enum; treat them as boxes with a fill.
| Field | Type | Description |
|  --- | --- | --- |
| `style.background_color` | string | Solid fill, `#RRGGBB`. |
| `style.background_gradient` | object | `stops[]` of `{offset, color}` plus `kind` and `angle_deg`. Stop colors may carry an 8-digit `#RRGGBBAA` alpha suffix. |
| `style.border_radius_px` | number | Corner radius, when present. |
| `style.box_shadow` | object | Optional. `offset_x_px`, `offset_y_px`, `blur_px`, `spread_px`, `color` and `inset`, with CSS `box-shadow` semantics. |

#### Text layers

```jsonc
{
  "text": "string — live, editable content. Explicit line breaks are \\n and are preserved when you re-render",
  "text_style": {
    "color": "string — #RRGGBB",
    "font_family": "string — a CSS font stack, nearest match first, fallbacks inline",
    "font_weight": "integer, 100-900",
    "font_size_px": "number",
    "letter_spacing_px": "number — pixels; omitted when zero",
    "line_height": "number — unitless multiplier over the font size",
    "text_align": "string, optional — \"left\" | \"center\" | \"right\"",
    "align_x": "string — CSS flexbox value: \"flex-start\" | \"center\" | \"flex-end\"",
    "align_y": "string — same values as align_x",
    "no_wrap": "boolean — true forbids wrapping inside the box",
    "uppercase": "boolean",
    "underline": "boolean",
    "italic": "boolean"
  }
}
```
### Copy layers: two halves
Copy comes back in both forms, so one result can be rendered either way and an editor can offer the choice without a second job. Each copy slot produces two layers with the same `subtype` and `bbox` and their own `z_order`:
| Layer | Type | Carries |
|  --- | --- | --- |
| `<slot>_svg` | `image` | `asset_path` to an SVG of the letterforms traced out of the original ad. |
| `<slot>_font` | `text` | `text` and `text_style`: live, editable, in the nearest matching font. |

The `hidden: true` flag marks which half is inactive; that layer must not be drawn — render both and the ad shows its copy twice.
A wordmark logo (a brand written out in lettering rather than a pictorial mark) is paired the same way, except that both halves are `image` layers: `_svg` is the traced outline and `_font` is the raster crop. A logo is never typeset. Layers with nothing to pair, which is most of them, keep their plain id and never carry `hidden`.
Skipping the hidden half saves an asset download per copy layer, and when no text layer is drawn nothing needs `font_stylesheets` at all.
**A real copy pair**

```json
[
  {
    "id": "text_5_svg",
    "type": "image",
    "subtype": "primary_copy",
    "bbox": { "x": 150.0, "y": 409.05, "width": 764.0, "height": 139.95 },
    "z_order": 8,
    "asset_path": "https://temp.bria.ai/results/860f1a2b73f847e59f284d6f860f2ddb/text_5.svg",
    "image_fit": { "object_fit": "contain" }
  },
  {
    "id": "text_5_font",
    "type": "text",
    "subtype": "primary_copy",
    "bbox": { "x": 150.0, "y": 409.05, "width": 764.0, "height": 139.95 },
    "z_order": 9,
    "hidden": true,
    "text": "40% OFF*",
    "text_style": {
      "color": "#FFFFFF",
      "font_family": "'Montserrat', 'Inter', 'Helvetica Neue', Arial, sans-serif",
      "font_weight": 900,
      "font_size_px": 149.58,
      "letter_spacing_px": -1.93,
      "line_height": 1.0,
      "align_x": "flex-start",
      "align_y": "center",
      "no_wrap": true,
      "uppercase": false,
      "underline": false,
      "italic": false
    }
  }
]
```
This example comes from a full-resolution Enterprise run, which is why the canvas exceeds 1350 px.
### Rendering the ad back
1. Load every URL in `font_stylesheets`, or text metrics will be wrong.
2. Drop every layer with `hidden: true`, then sort the rest by `z_order`.
3. Create a stage the size of `canvas` and absolutely position each layer at its `bbox`.
4. Image layers become an `<img>` with the given `object-fit`; vector layers become a box with the fill; text layers become a flex box using `align_x` and `align_y`.

A minimal browser renderer that follows those rules:

```javascript
function renderAd(doc, stage) {
  for (const href of doc.font_stylesheets ?? []) {
    const link = document.createElement("link");
    link.rel = "stylesheet";
    link.href = href;
    document.head.appendChild(link);
  }
  Object.assign(stage.style, { position: "relative", width: `${doc.canvas.width}px`, height: `${doc.canvas.height}px` });

  const layers = doc.layers.filter((l) => l.hidden !== true).sort((a, b) => a.z_order - b.z_order);
  for (const layer of layers) {
    let el;
    if (layer.type === "image") {
      el = document.createElement("img");
      el.src = layer.asset_path;
      el.style.objectFit = layer.image_fit?.object_fit ?? "contain";
      el.style.objectPosition = `${layer.image_fit?.object_position_x ?? "center"} ${layer.image_fit?.object_position_y ?? "center"}`;
    } else if (layer.type === "vector") {
      el = document.createElement("div");
      const s = layer.style ?? {};
      if (s.background_color) el.style.background = s.background_color;
      if (s.border_radius_px) el.style.borderRadius = `${s.border_radius_px}px`;
      // background_gradient and box_shadow map onto CSS linear-gradient() and box-shadow
    } else {
      el = document.createElement("div");
      const t = layer.text_style;
      el.textContent = layer.text;
      Object.assign(el.style, {
        display: "flex", justifyContent: t.align_x, alignItems: t.align_y,
        color: t.color, fontFamily: t.font_family, fontWeight: t.font_weight,
        fontSize: `${t.font_size_px}px`, lineHeight: t.line_height,
        letterSpacing: `${t.letter_spacing_px ?? 0}px`, textAlign: t.text_align ?? "left",
        whiteSpace: t.no_wrap ? "nowrap" : "pre-wrap",
        textTransform: t.uppercase ? "uppercase" : "none",
        textDecoration: t.underline ? "underline" : "none",
        fontStyle: t.italic ? "italic" : "normal",
      });
    }
    Object.assign(el.style, {
      position: "absolute",
      left: `${layer.bbox.x}px`, top: `${layer.bbox.y}px`,
      width: `${layer.bbox.width}px`, height: `${layer.bbox.height}px`,
    });
    stage.appendChild(el);
  }
}
```
### Limits and retention
|  |  |
|  --- | --- |
| Input size | Up to 10 MB, PNG, JPEG or WEBP |
| Input dimensions | Up to 1350 px per side. Enterprise accounts have no cap and receive full-resolution layers. [Contact us](https://bria.ai/contact-us) |
| Retention | The layer document and every referenced asset stay downloadable for 30 days after the job reaches a terminal status |
| Billing | Failed jobs are not billed |

### Use it from an agent
The hosted and local [MCP server](/mcp-authentication) exposes this capability as the `ad_to_layers` tool; agents that write code can use the [Python SDK](/integration-methods/python-sdk) as shown above.

Endpoint: POST /ads/delayer
Security: api_token

## Request fields (application/json):

  - `attachments` (array, required)
    Exactly one image: a publicly reachable URL, or a `data:image/…;base64,…` URI. PNG, JPEG or WEBP, up to 10 MB and 1350 px per dimension (Enterprise accounts have no dimension cap). More than one entry is rejected. This parameter is required.

  - `webhook_url` (string)
    Optional URL that receives the completion payload by POST when the job reaches a terminal state, with the same body as a status poll. See [webhooks](/getting-started/async-requests#webhooks).

## Response 200 fields (application/json):

  - `request_id` (string, required)
    Example: 860f1a2b73f847e59f284d6f860f2ddb

  - `status_url` (string, required)
    Example: https://engine.prod.bria-api.com/v2/status/860f1a2b73f847e59f284d6f860f2ddb

## Response 400 fields (application/json):

  - `error` (object, required)

  - `error.code` (integer, required)
    Example: 422

  - `error.message` (string, required)
    Example: Unprocessable Entity

  - `error.details` (string, required)
    Example: ['Invalid Input -> only one image URL is supported per request']

  - `request_id` (string, required)
    Identifies this response, not your job; the durable handle is the id inside the `status_url` you received at submit.
    Example: 3a51f1c2f0de4b6c9a3e2b41a7c9d0e8

## Response 401 fields (application/json):

  - `error` (object, required)

  - `error.code` (integer, required)
    Example: 422

  - `error.message` (string, required)
    Example: Unprocessable Entity

  - `error.details` (string, required)
    Example: ['Invalid Input -> only one image URL is supported per request']

  - `request_id` (string, required)
    Identifies this response, not your job; the durable handle is the id inside the `status_url` you received at submit.
    Example: 3a51f1c2f0de4b6c9a3e2b41a7c9d0e8

## Response 413 fields (application/json):

  - `error` (object, required)

  - `error.code` (integer, required)
    Example: 422

  - `error.message` (string, required)
    Example: Unprocessable Entity

  - `error.details` (string, required)
    Example: ['Invalid Input -> only one image URL is supported per request']

  - `request_id` (string, required)
    Identifies this response, not your job; the durable handle is the id inside the `status_url` you received at submit.
    Example: 3a51f1c2f0de4b6c9a3e2b41a7c9d0e8

## Response 422 fields (application/json):

  - `error` (object, required)

  - `error.code` (integer, required)
    Example: 422

  - `error.message` (string, required)
    Example: Unprocessable Entity

  - `error.details` (string, required)
    Example: ['Invalid Input -> only one image URL is supported per request']

  - `request_id` (string, required)
    Identifies this response, not your job; the durable handle is the id inside the `status_url` you received at submit.
    Example: 3a51f1c2f0de4b6c9a3e2b41a7c9d0e8

## Response 429 fields (application/json):

  - `error` (object, required)

  - `error.code` (integer, required)
    Example: 422

  - `error.message` (string, required)
    Example: Unprocessable Entity

  - `error.details` (string, required)
    Example: ['Invalid Input -> only one image URL is supported per request']

  - `request_id` (string, required)
    Identifies this response, not your job; the durable handle is the id inside the `status_url` you received at submit.
    Example: 3a51f1c2f0de4b6c9a3e2b41a7c9d0e8

## Response 5XX fields (application/json):

  - `error` (object, required)

  - `error.code` (integer, required)
    Example: 422

  - `error.message` (string, required)
    Example: Unprocessable Entity

  - `error.details` (string, required)
    Example: ['Invalid Input -> only one image URL is supported per request']

  - `request_id` (string, required)
    Identifies this response, not your job; the durable handle is the id inside the `status_url` you received at submit.
    Example: 3a51f1c2f0de4b6c9a3e2b41a7c9d0e8

