---
title: "Analyze an ad"
description: "Break one ad into its shots, its transcript, its style, and its insights."
url: https://docs.roasos.com/workflows/analyze-an-ad
---

# Analyze an ad

Analyze reads one ad and returns its structure. Use the result to learn why an ad works. Use it also as the input to a recast or to a new generation.

An analysis runs as a job with its own read path. Quote the run, confirm the run, poll the status, then read the result.

## 1. Choose a source

Send exactly one source. roasOS derives the kind of the analysis from the media. Never send the kind.

| Field              | Use it for                                                                                                                            |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `video_url`        | A video on `https://cdn.roasos.com` that your workspace owns                                                                          |
| `image_url`        | A still on `https://cdn.roasos.com` that your workspace owns                                                                          |
| `discover_item_id` | A creative from the roasOS Discover library — read the ids with [`GET /v1/discover/items`](/api-reference/discover/listDiscoverItems) |

Add an optional `note` of up to 400 characters to point the analysis at one thing. roasOS treats the note as evidence, never as an instruction.

| Source                                 | Analysis kind | Credits |
| -------------------------------------- | ------------- | ------- |
| A video                                | `video`       | 3       |
| A still                                | `image`       | 1       |
| A result that your workspace can reuse | either        | 0       |

A video must be 36 MiB or smaller. A still must be 8 MiB or smaller. A larger file answers `400 analysis_media_too_large`, and it answers before the price. No credits move on that refusal. The analysis of a large video can take up to three minutes.

## 2. Quote

Set `quote_only` to `true`. A quote is free.

```bash
curl -X POST https://api.roasos.com/v1/workflows/analyze \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: analyze-competitor-hook-001" \
  -H "Content-Type: application/json" \
  -d '{
    "discover_item_id": "6f3a9c21-4b0e-4c3a-9f52-1d7a8c0e5b44",
    "note": "focus on the first three seconds",
    "quote_only": true
  }'
```

```json
{
  "data": {
    "schema_version": "workflow-analysis@1",
    "kind": "workflow_analysis",
    "id": "0a4e1d77-...",
    "workflow": "analyze",
    "status": "quoted",
    "analysis_kind": "video",
    "cache_disposition": "live_analysis",
    "reserved_credits": 0,
    "maximum_credits": 3,
    "pricing_version": "workflow-pricing@2026-10-01.v2",
    "quote_id": "b81c5f30-...",
    "expires_at": "2026-08-21T10:17:44.000Z",
    "authorization": { "status": "auto_approved" },
    "created_at": "2026-08-21T10:07:44.000Z",
    "updated_at": "2026-08-21T10:07:44.000Z"
  }
}
```

Read `analysis_kind`. It tells you the price before you confirm.

## 3. Confirm

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

```bash
curl -X POST https://api.roasos.com/v1/workflows/analyze \
  -H "Authorization: Bearer roasos_sk_..." \
  -H "Idempotency-Key: analyze-competitor-hook-001" \
  -H "Content-Type: application/json" \
  -d '{
    "discover_item_id": "6f3a9c21-4b0e-4c3a-9f52-1d7a8c0e5b44",
    "note": "focus on the first three seconds",
    "quote_id": "b81c5f30-..."
  }'
```

The answer is `202`. The credits are now reserved.

```json
{
  "data": {
    "schema_version": "workflow-analysis@1",
    "kind": "workflow_analysis",
    "id": "0a4e1d77-...",
    "workflow": "analyze",
    "status": "queued",
    "analysis_kind": "video",
    "cache_disposition": "live_analysis",
    "reserved_credits": 3,
    "maximum_credits": 3,
    "pricing_version": "workflow-pricing@2026-10-01.v2",
    "created_at": "2026-08-21T10:07:44.000Z",
    "updated_at": "2026-08-21T10:08:02.000Z"
  }
}
```

## 4. Poll the status

Analyze sends no webhook. Poll the status every few seconds.

```bash
curl https://api.roasos.com/v1/workflows/analyses/0a4e1d77-.../status \
  -H "Authorization: Bearer roasos_sk_..."
```

The status changes from `queued` to `processing`. It then becomes `succeeded` or `failed`. A `succeeded` run carries `result_ready: true`.

<Callout>
  An analysis uses its own status words. The terminal success is `succeeded`, and it is never `completed`. A generation carries the five job statuses instead, and its terminal success is `completed`. Refer to [Jobs](/concepts/jobs).
</Callout>

```json
{
  "data": {
    "schema_version": "workflow-analysis@1",
    "kind": "workflow_analysis",
    "id": "0a4e1d77-...",
    "workflow": "analyze",
    "status": "succeeded",
    "analysis_kind": "video",
    "cache_disposition": "live_analysis",
    "reserved_credits": 3,
    "settled_credits": 3,
    "maximum_credits": 3,
    "pricing_version": "workflow-pricing@2026-10-01.v2",
    "completed_at": "2026-08-21T10:08:31.000Z",
    "result_ready": true,
    "created_at": "2026-08-21T10:07:44.000Z",
    "updated_at": "2026-08-21T10:08:31.000Z"
  }
}
```

`GET /v1/workflows/analyses/{id}` returns the same envelope. Use `/status` for the poll loop.

## 5. Read the result

```bash
curl https://api.roasos.com/v1/workflows/analyses/0a4e1d77-.../result \
  -H "Authorization: Bearer roasos_sk_..."
```

<Callout type="warn">
  `/result` returns `409 analysis_not_completed` while the run is not terminal. This is not an error in your code. Poll `/status` first.
</Callout>

A video returns numbered shots with timings, a transcript, a character sheet, the global style, the product moments and the insights.

```json
{
  "data": {
    "schema_version": "workflow-analysis-result@1",
    "kind": "workflow_analysis_result",
    "analysis_id": "0a4e1d77-...",
    "workflow": "analyze",
    "status": "succeeded",
    "analysis_kind": "video",
    "cache_disposition": "live_analysis",
    "model": "gemini-3-flash-preview",
    "pricing_version": "workflow-pricing@2026-10-01.v2",
    "reserved_credits": 3,
    "maximum_credits": 3,
    "settled_credits": 3,
    "completed_at": "2026-08-21T10:08:31.000Z",
    "analysis": {
      "durationSec": 24.5,
      "format": "talking-head UGC",
      "hookArchetype": "problem callout",
      "ctaType": "shop now",
      "hookEndSec": 3.2,
      "transcript": [
        { "start": 0, "end": 3.2, "text": "I stopped buying the expensive one." }
      ],
      "characterSheet": {
        "count": 1,
        "people": [
          {
            "id": "person-1",
            "observable": "woman, mid-20s, dark curly hair, grey hoodie",
            "primary": true,
            "presence": "on-camera"
          }
        ]
      },
      "suggestedLook": "iphone-one-take",
      "global": {
        "style": "handheld phone capture, natural window light",
        "scene": "small kitchen, morning",
        "location": "kitchen counter beside a window",
        "locks": ["keep the bottle label readable"]
      },
      "shots": [
        {
          "index": 1,
          "tStart": 0,
          "tEnd": 3.2,
          "hardCut": true,
          "shotType": "selfie_arm",
          "camera": "arm's length, chest up, slight downward angle",
          "action": "She holds the bottle beside her face and speaks to camera.",
          "events": [{ "at": 1.4, "what": "she lifts the bottle into frame" }],
          "dialogue": {
            "at": 0.2,
            "text": "I stopped buying the expensive one.",
            "delivery": "on-camera"
          },
          "onScreenText": "3 weeks in",
          "product": "held"
        }
      ],
      "productMoments": [{ "at": 1.4, "how": "held" }],
      "insights": {
        "hook": "A refusal in the first line. It names the category and the price objection at once.",
        "structure": "Hook, three proof beats, one price comparison, one call to action.",
        "persuasion": ["price anchoring", "before and after", "first-person proof"],
        "whatToBorrow": ["open on a refusal, not on the product"],
        "audience": "women 25 to 40 who buy skincare in a pharmacy",
        "weaknesses": ["the call to action is spoken only, never on screen"]
      },
      "confidence": { "shots": 0.86, "transcript": 0.94 }
    }
  }
}
```

A still returns a different document. It gives the layout, the copy by role, the product placement, the person, the style and a recreate prompt.

```json
{
  "format": "product-on-background",
  "aspect": "4:5",
  "layout": "Product centred on the lower third. Headline across the top third.",
  "headline": "Two weeks. One bottle.",
  "cta": "Shop now",
  "otherText": ["Dermatologist tested"],
  "product": {
    "present": true,
    "description": "amber glass serum bottle with a black dropper",
    "placement": "centred, upright, on a stone slab"
  },
  "characterSheet": { "count": 0, "people": [] },
  "style": "soft daylight product photography, warm neutral palette",
  "lighting": "single soft key from the left, gentle falloff",
  "background": "cream stone slab against a plain wall",
  "palette": ["#F1E7DA", "#C08A4B", "#2B2B2B"],
  "hookArchetype": "result claim",
  "recreatePrompt": "A single {PRODUCT} standing upright on a cream stone slab, soft daylight from the left, warm neutral palette, generous negative space above the product for a headline, 4:5.",
  "confidence": 0.91
}
```

The recreate prompt holds the placeholders `{PRODUCT}` and `{PERSON}`. Replace them with your own product and your own person. The prompt never carries the copy text of the source, and it never carries the identity of the person in the source.

## Cache hits are free

roasOS keeps one analysis for each piece of media. If your workspace can reuse an earlier analysis, the answer is `succeeded` at once, with `cache_disposition: cache_hit` and zero credits. Read the result immediately.

A Discover creative is shared across workspaces. Your own upload stays in your own workspace.

## Failure and refunds

A failed run carries `settled_credits: 0`. roasOS refunds the reservation in full.

| Code                        | Cause                                            |
| --------------------------- | ------------------------------------------------ |
| `analysis_failed`           | The model call did not produce a usable document |
| `analysis_media_unreadable` | roasOS could not read the media                  |
| `analysis_timed_out`        | The run passed its time limit                    |

## Rate window

The analysis runs on the included lane of roasOS. That lane holds an hourly window for each workspace: 500 analyses per hour. A request over the window answers `429 included_ancillary_ai_window_exceeded` and carries a `Retry-After` header. Wait for the seconds in that header. Then send the request again with the same idempotency key.

A cache hit never reaches the lane, so it never meets this window.

## From an agent

Four MCP tools cover this workflow: `roasos_analyze_ad`, `roasos_get_ad_analysis`, `roasos_get_ad_analysis_status` and `roasos_get_ad_analysis_result`. The quote rule is the same. Refer to [Connect an AI agent (MCP)](/mcp).

## Next

* Put your own person into the ad you analyzed: [Recast a video](/workflows/recast-a-video).
* Animate a still that you analyzed: [Animate a still](/workflows/animate-a-still).
