Skip to content

Decompose an ad into layers

Request

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) or pass a webhook_url (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.

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 sendYou 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) every few seconds, or pass a webhook_url and let Bria POST the result to you (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

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

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

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:

{
  "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

ParameterDescription
attachmentsExactly one image: a public URL or a data:image/…;base64,… URI. PNG, JPEG or WEBP, up to 10 MB and 2048 px per dimension (no dimension cap on Enterprise). Required.
webhook_urlOptional. 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

{
  "canvas": { "width": 1000, "height": 1500 },
  "font_stylesheets": ["https://fonts.googleapis.com/css2?family=Inter…"],
  "layers": [ ... ]
}
FieldTypeDescription
canvasobjectwidth and height, matching the input image.
font_stylesheetsstring[]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.
layersarrayEvery layer in the ad. Sort by z_order before rendering; array order is not guaranteed to match paint order.

Fields on every layer

FieldTypeDescription
idstringResult-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.
typestringimage, vector or text.
subtypestringbackground, logo, product, primary_copy, secondary_copy, cta, compliance, icon, decorative.
bboxobjectx, y, width, height in canvas pixels.
z_orderintegerPaint 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.
hiddenbooleanPresent 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

FieldTypeDescription
asset_pathstringHosted image. Cutouts are transparent PNGs; the _svg half of a copy pair is an SVG of the traced letterforms.
image_fit.object_fitstringcover or contain, with CSS object-fit semantics.
image_fit.object_position_x, image_fit.object_position_ystringOptional. 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.

FieldTypeDescription
style.background_colorstringSolid fill, #RRGGBB.
style.background_gradientobjectstops[] of {offset, color} plus kind and angle_deg. Stop colors may carry an 8-digit #RRGGBBAA alpha suffix.
style.border_radius_pxnumberCorner radius, when present.
style.box_shadowobjectOptional. offset_x_px, offset_y_px, blur_px, spread_px, color and inset, with CSS box-shadow semantics.

Text layers

{
  "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:

LayerTypeCarries
<slot>_svgimageasset_path to an SVG of the letterforms traced out of the original ad.
<slot>_fonttexttext 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

[
  {
    "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
    }
  }
]

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:

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 sizeUp to 10 MB, PNG, JPEG or WEBP
Input dimensionsUp to 2048 px per side. Enterprise accounts have no cap and receive full-resolution layers. Contact us
RetentionThe layer document and every referenced asset stay downloadable for 30 days after the job reaches a terminal status
BillingFailed jobs are not billed

Use it from an agent

The hosted and local MCP server exposes this capability as the ad_to_layers tool; agents that write code can use the Python SDK as shown above.

Security
api_token
Bodyapplication/json
attachmentsArray of strings, = 1 itemsrequired

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

webhook_urlstring, (uri)

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.

POST
/ads/delayer
from bria_client import BriaSyncClient

client = BriaSyncClient()  # reads BRIA_API_TOKEN

job = client.submit(
    endpoint="ads/delayer",
    payload={
        "attachments": [
            "https://bria-image-repository.s3.amazonaws.com/images/c41b39ec1a69a5d3.jpg",
        ],
    },
)
result = client.poll(job, interval=2, timeout=120)
print(result.result.url)

Responses

Accepted (asynchronous). Track progress with the Status Service or a webhook; the completion payload is shown under Callbacks.

Bodyapplication/json
request_idstringrequired
Example:"860f1a2b73f847e59f284d6f860f2ddb"
status_urlstringrequired
Example:"https://engine.prod.bria-api.com/v2/status/860f1a2b73f847e59f284d6f860f2ddb"
Response
{ "request_id": "860f1a2b73f847e59f284d6f860f2ddb", "status_url": "https://engine.prod.bria-api.com/v2/status/860f1a2b73f847e59f284d6f860f2ddb" }

Callbacks

Completion payload (webhook or status poll)
post