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
- Submit the ad. The response carries a
request_idand astatus_url. - Poll the
status_url(status endpoint) or pass awebhook_url(webhooks). Both deliver the completion payload shown under Callbacks below. - On completion,
result.urlpoints to the hosted layer document; each image layer'sasset_pathis 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 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 |
- Submit the ad. The API returns a
request_idand astatus_urlimmediately. - Wait for completion by polling the
status_url(the shared status endpoint) every few seconds, or pass awebhook_urland let Bria POST the result to you (webhooks). Both deliver the same body. - Fetch the layer document. The completed status body carries a pointer, not the layers:
result.urlis the JSON document. - Fetch assets as needed. Each image layer's
asset_pathis 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_idis reported as"status": "UNKNOWN"with HTTP200. Treat it as not found and stop polling. - The
request_idinside an error body identifies that response, not your job. When reporting a problem, quote the id inside thestatus_urlyou received at submit.
| Parameter | Description |
|---|---|
attachments | Exactly 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_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.
{
"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. |
| 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. |
| 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 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": "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 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
[
{
"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
}
}
]- Load every URL in
font_stylesheets, or text metrics will be wrong. - Drop every layer with
hidden: true, then sort the rest byz_order. - Create a stage the size of
canvasand absolutely position each layer at itsbbox. - Image layers become an
<img>with the givenobject-fit; vector layers become a box with the fill; text layers become a flex box usingalign_xandalign_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);
}
}| Input size | Up to 10 MB, PNG, JPEG or WEBP |
| Input dimensions | Up to 2048 px per side. Enterprise accounts have no cap and receive full-resolution layers. 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 |
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.
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.
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.
- Basic request
- With a webhook
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)Accepted (asynchronous). Track progress with the Status Service or a webhook; the completion payload is shown under Callbacks.
{ "request_id": "860f1a2b73f847e59f284d6f860f2ddb", "status_url": "https://engine.prod.bria-api.com/v2/status/860f1a2b73f847e59f284d6f860f2ddb" }