---
title: "Make static ads"
description: "Turn one product into a set of static ads: the copy, the brand colours, the logo and the product, rendered as images."
url: https://docs.roasos.com/workflows/make-static-ads
---

# Make static ads

A statics run turns one product into a set of static ads. roasOS reads your product and your brand, writes the headline, the sub-head and the call to action, prices the whole set on one card, and then renders each variant.

`POST /v1/workflows/statics` is the typed door onto the `roasos:statics-factory` skill. For the concept behind the door, refer to [Statics](/concepts/statics).

## A statics run is an agent run

The door answers with an **agent run**, not with a job. The work is a plan: it reads the product, it writes the copy, it prices the set, and then it starts one image job for each variant.

| Step                       | Where you read it                |
| -------------------------- | -------------------------------- |
| The run itself             | `GET /v1/agent-runs/{id}`        |
| The light poll             | `GET /v1/agent-runs/{id}/status` |
| The lifecycle              | `GET /v1/agent-runs/{id}/events` |
| The answer of the agent    | `GET /v1/agent-runs/{id}/result` |
| Each image the run started | `GET /v1/jobs/{id}`              |

Remix answers the same way. Recast and animate-a-still answer with a job. Refer to [Remix an ad](/workflows/remix-an-ad) and to [Runs](/agent/runs).

## 1. Say what the set must sell

| Field                          | Rule                                                                                         |
| ------------------------------ | -------------------------------------------------------------------------------------------- |
| `product_id`                   | Required. The product that the statics sell. roasOS resolves it before the run starts.       |
| `brief`                        | Optional. 400 characters at most. roasOS reads a brief as evidence, never as an instruction. |
| `variants`                     | Optional. 1 to 8. The default is 3.                                                          |
| `aspect_ratio`                 | Optional. `1:1`, `4:5`, `9:16` or `16:9`. The default is `4:5`.                              |
| `lane`                         | Optional. `scene` or `product`. The default is `scene`.                                      |
| `avatar_id`                    | Optional. A gallery avatar or a workspace avatar, for a person in the frame.                 |
| `max_credits`                  | Optional. The ceiling on the thinking of the run.                                            |
| `generation_spend_cap_credits` | Optional. The ceiling on what the run may render without you.                                |

The product images and the avatar come from your workspace. The logo, the colours, the two font names and `cta_text` come from your [brand kit](/brand/brand-kit).

<Callout type="warn">
  `variants` stops at **8**. Past eight quotes, the approval gate stops approving itself and an unattended run waits for a person who is not there. roasOS refuses `variants: 9` with `400 validation_error` and names the ceiling in the message. Start a second run for a larger set.
</Callout>

## 2. Quote

Set `quote_only` to `true`. A quote is free, and it reserves nothing.

Send an `Idempotency-Key` header. Use 16 to 255 characters. Use the same key for the quote and for its confirm.

```bash
curl -X POST https://api.roasos.com/v1/workflows/statics \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: statics-glow-serum-001" \
  -H "Content-Type: application/json" \
  -d '{
    "product_id": "5e6b3311-7a2c-4f19-b3d8-0c41e7f92a63",
    "brief": "Summer restock. The bottle is the hero. Keep the copy under six words.",
    "variants": 3,
    "aspect_ratio": "4:5",
    "lane": "scene",
    "max_credits": 25,
    "quote_only": true
  }'
```

The answer is `200`. The status is `quoted`.

```json
{
  "data": {
    "schema_version": "agent-run@1",
    "kind": "agent_run",
    "id": "2b8f41ac-...",
    "trigger": "workflow",
    "skill_id": "5003b82a-4888-4986-b870-a1d6e7ac89cc",
    "conversation_id": null,
    "status": "quoted",
    "max_credits": 25,
    "generation_spend_cap_credits": 0,
    "credits_reserved": 0,
    "credits_max": 25,
    "pricing_version": "agent-token-pricing@2026-10-04.v3",
    "quote_id": "7f10c3b8-...",
    "expires_at": "2026-08-21T12:04:11.000Z",
    "authorization": { "status": "auto_approved" },
    "created_at": "2026-08-21T11:54:11.000Z",
    "updated_at": "2026-08-21T11:54:11.000Z"
  }
}
```

`skill_id` names the pack skill that the door runs. Refer to [The roasOS skill pack](/agent/skills#the-roasos-skill-pack).

Confirm the quote inside 10 minutes. A quote that is too old answers `410 quote_expired`.

### The quote is the ceiling of the run, not the price of the images

The statics door carries **no image price**. It returns an agent-run envelope, and never a `kind: "quote"` object with a credit total for the set.

`credits_max` is `max_credits` plus `generation_spend_cap_credits`. It is a ceiling, and it is not an estimate.

The price of the images arrives later, on the gate card in step 5. roasOS quotes every variant on the worker before it opens that card, so you see one number for the whole set before one image is made.

## 3. Confirm

Send the same body with the `quote_id`. Use the same idempotency key. Remove `quote_only`.

```bash
curl -X POST https://api.roasos.com/v1/workflows/statics \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: statics-glow-serum-001" \
  -H "Content-Type: application/json" \
  -d '{
    "product_id": "5e6b3311-7a2c-4f19-b3d8-0c41e7f92a63",
    "brief": "Summer restock. The bottle is the hero. Keep the copy under six words.",
    "variants": 3,
    "aspect_ratio": "4:5",
    "lane": "scene",
    "max_credits": 25,
    "quote_id": "7f10c3b8-..."
  }'
```

The answer is `202`. roasOS reserves the ceiling and starts the run.

An unknown `product_id` answers `404 product_not_found`, and it answers before the run starts. No credits move. The same rule holds for an unknown `avatar_id`, which answers `404 avatar_not_found`.

## 4. Poll the run

```bash
curl https://api.roasos.com/v1/agent-runs/2b8f41ac-.../status \
  -H "Authorization: Bearer roasos_sk_..."
```

An agent run sends no webhook. Poll every few seconds.

## 5. The card holds the price of the set

The run stops at the set. `gate_opened` is the last event, and the run carries one entry in `pending_approvals`.

```json
{
  "data": {
    "id": "2b8f41ac-...",
    "trigger": "workflow",
    "status": "processing",
    "conversation_id": null,
    "generation_spend_cap_credits": 0,
    "pending_approvals": [
      {
        "gate_id": "6d09b7e2-...",
        "summary": "static ads x 3",
        "credits": 6,
        "expires_at": "2026-08-21T12:11:40.000Z",
        "approve_url": "https://api.roasos.com/v1/agent-runs/2b8f41ac-.../gates/6d09b7e2-.../approve"
      }
    ]
  }
}
```

`credits` is the price of every variant added together. roasOS quotes each compiled request on the worker first, so a partial price set never opens as one number.

The default of `generation_spend_cap_credits` is **0**, so a statics run that says nothing about the cap always stops here.

## 6. Approve the card

A run started from a workflow writes no thread, so approve the card over REST.

```bash
curl -X POST https://api.roasos.com/v1/agent-runs/2b8f41ac-.../gates/6d09b7e2-.../approve \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: statics-glow-serum-001-approve" \
  -H "Content-Type: application/json" \
  -d '{ "generation_spend_cap_credits": 30 }'
```

The approval starts a second run, and `follow_up_run_id` names it. Poll that run exactly as you polled the first one.

<Callout type="warn">
  Approve is a REST operation only. It is not an MCP tool. A connected model may **decline** an approval. It must never approve its own spend. Refer to [Approving spend is never a tool](/mcp#approving-spend-is-never-a-tool).
</Callout>

## 7. Read the images

Each variant is an ordinary image job.

```bash
curl "https://api.roasos.com/v1/jobs?limit=10" \
  -H "Authorization: Bearer roasos_sk_..."
```

Poll each job until the status is `completed`, `failed` or `canceled`. Read the file at `GET /v1/jobs/{id}/result`. The URL is immutable. Refer to [Jobs](/concepts/jobs).

## Let the run render without you

Set `generation_spend_cap_credits` above the price of the set. The run then approves the card itself, and the renders start in the same run.

```bash
curl -X POST https://api.roasos.com/v1/workflows/statics \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: statics-glow-serum-002" \
  -H "Content-Type: application/json" \
  -d '{
    "product_id": "5e6b3311-7a2c-4f19-b3d8-0c41e7f92a63",
    "variants": 8,
    "lane": "product",
    "max_credits": 25,
    "generation_spend_cap_credits": 60,
    "quote_id": "c1b7e409-..."
  }'
```

The run approves the card by itself only when the price is inside the credits that remain in `generation_spend_cap_credits`, and inside `auto_approve_up_to_credits` on your key. In every other condition the card waits. Refer to [Spend caps and approvals](/agent/ask-the-agent#spend-caps-and-approvals).

## Run the skill in your own words

The door is one way in. The pack skill is the other.

```bash
curl -X POST https://api.roasos.com/v1/skills/roasos:statics-factory/runs \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: statics-freeform-001" \
  -H "Content-Type: application/json" \
  -d '{ "generation_spend_cap_credits": 60, "quote_only": true }'
```

Send the typed body to the door when your software knows the product. Run the skill by slug when a person states the task in their own words. Both paths meter the same way. Refer to [Skills](/agent/skills).

## The two lanes

| Lane      | What it does                                                                                                       | Credits for one image    |
| --------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------ |
| `scene`   | Renders the whole frame on [Nano Banana Pro](/models/image/nano-banana-pro), with the product images as references | 5 at 1K and 2K, 10 at 4K |
| `product` | Places the product cut-out into a described scene on [Bria Product Shot](/models/image-edit/bria-product-shot)     | 1 for the placement      |

Refer to [Statics](/concepts/statics#the-two-lanes) for the choice between them, and for the fallback that the `product` lane uses while the placement endpoint is an API capability only.

## The limits, stated plainly

<Callout type="warn">
  **roasOS cannot guarantee your exact typeface.** The image model draws the headline, the sub-head and the call to action from the exact strings in the prompt. A brand-kit font slot holds a name, not a font file. The letters come out in a similar face, and a glyph can come out wrong. Render the variant again. A wrong glyph is not a refund.
</Callout>

<Callout type="info">
  **Make an avatar over the API with [`POST /v1/avatar/nano-banana-pro`](/models/avatar/nano-banana-pro).** The MCP tool is `roasos_avatar_nano_banana_pro`. Send the new id in `avatar_id`, or send a roasOS gallery avatar. The legacy path `POST /v1/avatars` answers `503 avatar_generation_unavailable`.
</Callout>

* `variants` is 1 to 8. The ceiling is the number of quotes that the approval gate can hold.
* One run holds one product.
* roasOS renders no text layer after the image. There is no deterministic overlay step today.
* A Discover creative is a structure and style reference only. It never becomes source media in a statics run. Refer to [Media rights](/concepts/media-rights).

## Price

A statics run has no price of its own.

| What                    | Meter                                                                                                |
| ----------------------- | ---------------------------------------------------------------------------------------------------- |
| The thinking of the run | `max_credits`. roasOS charges the real tokens and refunds the rest.                                  |
| Each rendered image     | The catalog price of the model, quoted before dispatch and bounded by `generation_spend_cap_credits` |

roasOS reserves `credits_max` at the confirm step. It settles the run when the run stops, and it refunds the part that the run does not use. Refer to [What a run costs](/agent/runs#what-a-run-costs).

The brand-kit read and the product read inside the run cost 0 credits and carry no quote envelope.

## Errors

| Status | Code                                                             | Cause                                                             |
| ------ | ---------------------------------------------------------------- | ----------------------------------------------------------------- |
| `400`  | `validation_error`                                               | A field is outside its range, for example `variants: 9`           |
| `400`  | `idempotency_key_required`                                       | The `Idempotency-Key` header is absent                            |
| `400`  | `quote_required`                                                 | The body has no `quote_only` value and no `quote_id`              |
| `400`  | `spend_cap_exceeds_policy`                                       | The ceiling is above the maximum price for one request of the key |
| `402`  | `approval_required`                                              | The ceiling is above the unattended limit of the key              |
| `402`  | `insufficient_credits`                                           | The balance does not cover the ceiling                            |
| `404`  | `product_not_found`                                              | The product is not in your workspace                              |
| `404`  | `avatar_not_found`                                               | The avatar is not in your workspace and not in the gallery        |
| `410`  | `quote_expired`                                                  | Quote again under a new idempotency key                           |
| `429`  | `daily_credit_budget_exceeded`, `monthly_credit_budget_exceeded` | The budget of the key has no space                                |
| `503`  | `skill_store_unavailable`                                        | roasOS cannot read the pack skill at this time                    |

Refer to [Errors](/concepts/errors).

## From an agent

The MCP tool is `roasos_statics`. It obeys the same quote rule as every paid tool.

An agent can start a statics run and read it. An agent can decline an approval. An agent can never approve one. Refer to [Connect an AI agent (MCP)](/mcp).
