{
  "openapi": "3.0.0",
  "info": {
    "title": "Ad Delayer API Reference",
    "version": "",
    "description": "Below you will find the technical specifications, request schemas, and response models for our endpoints.\n\nChoose an endpoint from the sidebar to view code samples and run test requests.\n"
  },
  "tags": [
    {
      "name": "v2 endpoints",
      "description": "Endpoints that are part of BRIA API version 2.\n"
    }
  ],
  "externalDocs": {
    "description": "Register and get API Access",
    "url": "https://platform.bria.ai/organization-management/api-keys"
  },
  "servers": [
    {
      "url": "https://engine.prod.bria-api.com/v2"
    }
  ],
  "paths": {
    "/ads/delayer": {
      "post": {
        "summary": "Ad Delayer",
        "tags": [
          "v2 endpoints"
        ],
        "operationId": "delayer-ad",
        "description": "Turn a flat ad image into editable layers — a background plate, product and logo cutouts, live text with typography, and vector shapes — returned as one structured JSON document. Trained exclusively on licensed data for safe commercial use.\n\nLatency is on average under 1 minute.\n\n**Input Assumptions**\n\nThe input is assumed to be a **flat ad**. Valid images that aren't ads — an ordinary photograph, say — are out of scope, and the result isn't guaranteed.\n\nFields the API doesn't recognize are currently ignored rather than rejected, so check your spelling — a mistyped parameter name fails silently and you get default behavior.\n\nSubmissions are not idempotent — a retried POST is a new job. Failed jobs are never billed.\n\n**Response Handling**\n\nSubmitting returns a `request_id` and a `status_url` to poll. On completion the status body carries a **pointer**, not the layers: fetch `result.url` to get the layer document, then each image layer's `asset_path` separately.\n\nOne caveat when reporting a problem: the `request_id` in a response body identifies that response, not your job. The durable handle is the id inside the `status_url` you got at submit — quote that.\n\n**The Layer Document**\n\n```json\n{\n  \"canvas\": { \"width\": 1000, \"height\": 1500 },\n  \"font_stylesheets\": [\"https://fonts.googleapis.com/css2?family=Inter…\"],\n  \"layers\": [ … ]\n}\n```\n\n| Field | Type | Description |\n|---|---|---|\n| `canvas` | object | `width` / `height`, matching the input image. |\n| `font_stylesheets` | `string[]` | Returned in the result. Google Fonts stylesheet URLs covering every font referenced by text layers. Load these before rendering. |\n| `layers` | array | Every layer in the ad. **Sort by `z_order` before rendering** — array order is not guaranteed to match paint order. |\n\nOn every layer:\n\n| Field | Type | Description |\n|---|---|---|\n| `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` — `text_3_font` beside `text_3_svg`. |\n| `type` | string | `image`, `vector`, or `text`. |\n| `subtype` | string | `background`, `logo`, `product`, `primary_copy`, `secondary_copy`, `cta`, `compliance`, `icon`, `decorative`. |\n| `bbox` | object | `x`, `y`, `width`, `height` in canvas pixels. |\n| `z_order` | integer | Paint order, from 0 upward with no gaps; lowest paints first. Sort on this — don't rely on array order. Each half of a copy pair takes its own number, so after dropping hidden layers, gaps in the visible sequence are expected. |\n| `hidden` | boolean | Present and `true` only on the half of a copy pair the mode did not choose. **Skip those layers.** Absent means visible — it is never `false`, so test for `hidden === true`. |\n\nImage layers:\n\n| Field | Type | Description |\n|---|---|---|\n| `asset_path` | string | Hosted image. Cutouts are transparent. PNG, except on the `_svg` half of a copy pair, which is an SVG of the traced letterforms. |\n| `image_fit.object_fit` | string | `cover` or `contain`, with CSS `object-fit` semantics. |\n| `image_fit.object_position_x`<br>`image_fit.object_position_y` | string | Optional. Where the image sits in its box when the fit leaves slack — `left`/`center`/`right` and `top`/`center`/`bottom`. Absent means centered. |\n\nVector layers:\n\n| Field | Type | Description |\n|---|---|---|\n| `style.background_color` | string | Solid fill, `#RRGGBB`. |\n| `style.background_gradient` | object | `stops[]` of `{offset, color}`, plus `kind` and `angle_deg`. Stop colors may carry an 8-digit `#RRGGBBAA` alpha suffix. |\n| `style.border_radius_px` | number | Corner radius, when present. |\n| `style.box_shadow` | object | Optional. `offset_x_px`, `offset_y_px`, `blur_px`, `spread_px`, `color` and `inset` (boolean), with CSS box-shadow semantics. |\n\nVector layers carry no shape enum — treat them as boxes with a fill.\n\nText layers:\n\n| Field | Type | Description |\n|---|---|---|\n| `text` | string | Live, editable content. Explicit line breaks are `\\n` and are preserved when you re-render. |\n| `text_style.color` | string | `#RRGGBB`. |\n| `text_style.font_family` | string | A CSS font stack — nearest match first, fallbacks inline. |\n| `text_style.font_weight` | integer | 100–900. |\n| `text_style.font_size_px` | number | Pixels. |\n| `text_style.letter_spacing_px` | number | Pixels. Omitted when zero. |\n| `text_style.line_height` | number | Unitless multiplier over font size. |\n| `text_style.text_align` | string | Alignment of the text itself: `left`, `center`, `right`. Optional. |\n| `text_style.align_x` / `align_y` | string | Alignment of the block inside its box, as CSS flexbox values: `flex-start`, `center`, `flex-end`. |\n| `text_style.no_wrap` | boolean | `true` forbids wrapping inside the box. |\n| `text_style.uppercase` / `underline` / `italic` | boolean | Style flags. |\n\n<details>\n\n<summary style=\"cursor: pointer; color: #007bff; font-weight: bold;\">Click to view a real copy pair (JSON, text_mode: svg)</summary>\n\n```json\n[\n  {\n    \"id\": \"text_5_svg\",\n    \"type\": \"image\",\n    \"subtype\": \"primary_copy\",\n    \"bbox\": { \"x\": 150.0, \"y\": 409.05, \"width\": 764.0, \"height\": 139.95 },\n    \"z_order\": 8,\n    \"asset_path\": \"https://temp.bria.ai/results/860f1a2b73f847e59f284d6f860f2ddb/text_5.svg\",\n    \"image_fit\": { \"object_fit\": \"contain\" }\n  },\n  {\n    \"id\": \"text_5_font\",\n    \"type\": \"text\",\n    \"subtype\": \"primary_copy\",\n    \"bbox\": { \"x\": 150.0, \"y\": 409.05, \"width\": 764.0, \"height\": 139.95 },\n    \"z_order\": 9,\n    \"hidden\": true,\n    \"text\": \"40% OFF*\",\n    \"text_style\": {\n      \"color\": \"#FFFFFF\",\n      \"font_family\": \"'Montserrat', 'Inter', 'Helvetica Neue', Arial, sans-serif\",\n      \"font_weight\": 900,\n      \"font_size_px\": 149.58,\n      \"letter_spacing_px\": -1.93,\n      \"line_height\": 1.0,\n      \"align_x\": \"flex-start\",\n      \"align_y\": \"center\",\n      \"no_wrap\": true,\n      \"uppercase\": false,\n      \"underline\": false,\n      \"italic\": false\n    }\n  }\n]\n```\n\nWith `text_mode: font` the same pair returns with `hidden: true` on `text_5_svg` instead.\n\n</details>\n\n**The two halves of a copy layer**\n\nCopy comes back in both forms regardless of `text_mode`, so one result can be rendered either way and an editor can offer the choice without a second job. Each copy slot therefore produces two layers:\n\n| Layer | Type | Carries |\n|---|---|---|\n| `<slot>_svg` | `image` | `asset_path` to an SVG of the letterforms traced out of the reference ad. |\n| `<slot>_font` | `text` | `text` and `text_style` — live, editable, in the nearest matching font. |\n\nBoth halves carry the copy's subtype (`primary_copy`, `cta`, and so on), share the same bbox, and take their own `z_order`.\n\n`text_mode` decides which one is live. The other carries `hidden: true` and must not be drawn — render both and the ad shows its copy twice.\n\n`svg` is best suited to plain, solid-color type where the original appearance must be preserved. Choose `font` when the copy will be edited, resized, or localized, and for text with fills, multiple colors, or overlapping elements — filled fonts are not supported in SVG and return a transparent layer.\n\nA wordmark logo — a brand written out in lettering, as against 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 (on a wordmark the `_font` suffix marks the fallback half, not a text layer). A logo is never typeset.\n\nLayers with nothing to pair, which is most of them, keep their plain id and never carry `hidden`.\n\nTwo things worth doing rather than ignoring: skipping the hidden half saves an asset download per copy layer, and in `svg` mode nothing needs `font_stylesheets` at all, because no text layer is drawn (the field may also be omitted entirely when no webfont is needed, so read it defensively).\n\n**Rendering It Back**\n\n**Drop every layer with `hidden: true`, then sort the rest by `z_order`** — the array often arrives already in that order, but not always. Then absolutely position each layer at its `bbox`, inside a canvas-sized stage. Image layers become an `<img>` with the given `object-fit`; vector layers become a div with the fill; text layers become a flex box using `align_x` and `align_y`. Load `font_stylesheets` first, or your text metrics will be wrong.\n\nThe examples above come from a full-resolution (enterprise) run, which is why the canvas exceeds 800 px.\n",
        "parameters": [
          {
            "in": "header",
            "name": "api_token",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "attachments": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 1,
                    "items": {
                      "type": "string"
                    },
                    "description": "Exactly one image — a publicly reachable URL, or a `data:image/…;base64,…` URI. PNG, JPEG or WEBP, up to 10 MB and 800 px per dimension (enterprise accounts have no dimension cap). More than one entry is rejected. This parameter is required."
                  },
                  "text_mode": {
                    "type": "string",
                    "enum": [
                      "svg",
                      "font"
                    ],
                    "default": "svg",
                    "description": "Where each copy layer's glyphs come from. `svg` traces them out of the reference ad as vector outlines, so the original letterforms are kept exactly; `font` typesets them as live, editable text in the nearest matching font. Either way both forms come back and one is marked `hidden` — see **The two halves of a copy layer**. The default is `svg` so the result is faithful to the original by default; the editable half is always present to switch to."
                  },
                  "webhook_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Optional URL for receiving the result via webhook when the async job completes, with the same body as a status poll. See [Webhooks](https://docs.bria.ai/webhooks)."
                  }
                },
                "required": [
                  "attachments"
                ]
              },
              "example": {
                "attachments": [
                  "https://bria-image-repository.s3.amazonaws.com/images/c41b39ec1a69a5d3.jpg"
                ],
                "text_mode": "svg"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted (Asynchronous). You can track the progress and retrieve the final result using the Status Service. For more details, refer to the [Status Service](https://docs.bria.ai/status) section.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AsyncInitialResponse"
                },
                "example": {
                  "request_id": "860f1a2b73f847e59f284d6f860f2ddb",
                  "status_url": "https://engine.prod.bria-api.com/v2/status/860f1a2b73f847e59f284d6f860f2ddb"
                }
              }
            }
          },
          "400": {
            "description": "Bad request. No image supplied in `attachments`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized. Missing or invalid `api_token`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "413": {
            "description": "Request body over the platform cap.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity. More than one entry in `attachments`, an invalid value for a supported field, or an image over the 800 px per-dimension cap. Cap rejections carry the message *\"Image exceeds 800 px per dimension on this endpoint. Resize the source image, or use full-resolution as an enterprise user at Bria.ai/contact-us.\"*",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": 422,
                    "message": "The request was well-formed but was unable to be followed due to semantic errors.",
                    "details": "['Invalid Input -> only one image URL is supported per request']"
                  },
                  "request_id": "3a51f1c2f0de4b6c9a3e2b41a7c9d0e8"
                }
              }
            }
          },
          "429": {
            "description": "Request limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "5XX": {
            "description": "Internal Server Error. The image could not be fetched or decoded, or the pipeline failed. Submit again; if it persists, contact support and quote the `status_url` of the run.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/status/{request_id}": {
      "get": {
        "summary": "Poll Job Status",
        "tags": [
          "v2 endpoints"
        ],
        "operationId": "get-ad-delayer-status",
        "description": "Status runs `IN_PROGRESS` to `COMPLETED` or a terminal error, and never moves backwards. Results and every referenced asset stay downloadable for **30 days** after the job reaches a terminal status.\n\nAn unrecognized or expired id comes back as `\"status\": \"UNKNOWN\"` with **HTTP 200**, not a 404. Treat `UNKNOWN` as not-found and stop polling.\n\nOn completion the body carries a **pointer**, not the layers: fetch `result.url` to get the layer document, then each image layer's `asset_path` separately. The layer document contract is documented on the Ad Delayer endpoint.\n\nFailures use the standard platform error envelope, where `error.code` repeats the numeric HTTP status and `details` carries the specifics.\n",
        "parameters": [
          {
            "in": "header",
            "name": "api_token",
            "schema": {
              "type": "string"
            },
            "required": true
          },
          {
            "in": "path",
            "name": "request_id",
            "schema": {
              "type": "string"
            },
            "required": true,
            "description": "The id inside the `status_url` returned at submit."
          }
        ],
        "responses": {
          "200": {
            "description": "Current job status. Also returned for unrecognized or expired ids, as `UNKNOWN`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DelayerStatusResponse"
                },
                "examples": {
                  "inProgress": {
                    "summary": "In progress",
                    "value": {
                      "request_id": "860f1a2b73f847e59f284d6f860f2ddb",
                      "status": "IN_PROGRESS"
                    }
                  },
                  "completed": {
                    "summary": "Completed",
                    "value": {
                      "request_id": "860f1a2b73f847e59f284d6f860f2ddb",
                      "status": "COMPLETED",
                      "result": {
                        "status": "completed",
                        "text": "Constructed a layered ad.",
                        "url": "https://temp.bria.ai/results/860f1a2b73f847e59f284d6f860f2ddb/creation.json"
                      }
                    }
                  },
                  "unknownId": {
                    "summary": "Unknown or expired id",
                    "value": {
                      "request_id": "860f1a2b73f847e59f284d6f860f2ddb",
                      "status": "UNKNOWN"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized. Missing or invalid `api_token`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AsyncInitialResponse": {
        "type": "object",
        "properties": {
          "request_id": {
            "type": "string"
          },
          "status_url": {
            "type": "string"
          }
        },
        "required": [
          "request_id",
          "status_url"
        ]
      },
      "DelayerStatusResponse": {
        "type": "object",
        "properties": {
          "request_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "`IN_PROGRESS`, `COMPLETED`, or `UNKNOWN` (unrecognized or expired id, returned with HTTP 200), or a terminal error status. Never moves backwards."
          },
          "result": {
            "type": "object",
            "description": "Present when `status` is `COMPLETED`. A pointer to the hosted result — fetch `url` for the layer document, then each image layer's `asset_path` separately.",
            "properties": {
              "status": {
                "type": "string",
                "example": "completed"
              },
              "text": {
                "type": "string",
                "example": "Constructed a layered ad."
              },
              "url": {
                "type": "string",
                "description": "The hosted layer document (JSON). Downloadable for 30 days."
              }
            }
          }
        },
        "required": [
          "request_id",
          "status"
        ]
      },
      "ErrorObject": {
        "type": "object",
        "properties": {
          "code": {
            "type": "integer",
            "example": 422
          },
          "message": {
            "type": "string"
          },
          "details": {
            "type": "string"
          }
        },
        "required": [
          "code",
          "message",
          "details"
        ]
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorObject"
          },
          "request_id": {
            "type": "string",
            "description": "Identifies this response, not your job — the durable handle is the id inside the `status_url` you got at submit."
          }
        },
        "required": [
          "error",
          "request_id"
        ]
      }
    }
  }
}