---
title: Best practices
description: One worked example — create a live cut, read a clip receipt, retry one scene on the same thread.
---

One worked example for Mobidoo backend integrators — what a create → receipt →
clip-retry loop can look like. Not a contract.

Team workspace API key:
[Team Formats need a team key](/formats/call#team-formats-need-a-team-key).

API surface: [Live Commerce API](/enterprise/mobidoo/live-commerce).  
How `input` and typed receipts work in depth:
[Calling a Format — what is carried](/formats/call#what-is-accepted-and-what-is-carried),
[Structured output](/formats/structured-output).

## 1. Example create

Instruction + convenient `input` + the partner `output_schema` (so the receipt
is typed — full cut and per-scene clips). Tabs are cURL / TypeScript /
JavaScript / Python for the same body.

<!-- api-call-example:format-mobidoo-lc-product -->

### What the create body has to get right

Four things in that body are load-bearing. Here is the same body the tab above
sends, as JSON — the run it came from returned a 34.72 s cut, 7 scenes, $7.02 —
with the long strings abridged for length:

```json
{
  "instruction": "라이브 커머스 세로 영상. input.script.segments의 INTRO/MID/FIN 대본을 그대로 사용하고 문장을 임의로 바꾸지 말 것. 대본을 늘리거나 문장을 추가하지 말 것 — 이 대본 분량이 곧 영상 길이입니다. BGM 없음, 자막 없음, 9:16 세로, 30fps. 화자는 input.host_image_url의 인물 1인.",
  "output_schema": {
    "name": "mobidoo/live-commerce/v1",
    "strict": true,
    "schema": {
      "type": "object",
      "additionalProperties": false,
      "required": ["live_title", "live_description", "full_video", "opening", "middle", "closing", "schedule_images"],
      "properties": {
        "live_title": { "type": ["string", "null"] },
        "live_description": { "type": ["string", "null"] },
        "full_video": { "$ref": "SumeMediaFile#" },
        "opening": { "type": "array", "items": { "$ref": "#/$defs/scene" } },
        "middle": { "type": "array", "items": { "$ref": "#/$defs/scene" } },
        "closing": { "type": "array", "items": { "$ref": "#/$defs/scene" } },
        "schedule_images": { "type": "array", "items": { "$ref": "SumeMediaFile#" } }
      },
      "$defs": {
        "scene": {
          "type": "object",
          "additionalProperties": false,
          "required": ["id", "role", "act", "status", "script", "tags", "duration_seconds", "video"],
          "properties": {
            "id": { "type": "string" },
            "role": { "type": "string", "enum": ["talk", "broll", "under_banner"] },
            "act": { "type": ["string", "null"] },
            "status": { "type": "string", "enum": ["succeeded", "stand-in", "failed"] },
            "script": { "type": ["string", "null"] },
            "tags": { "type": "array", "items": { "type": "string" } },
            "duration_seconds": { "type": ["number", "null"] },
            "video": { "$ref": "SumeMediaFile#" }
          }
        }
      }
    }
  },
  "primary_output_key": "full_video",
  "generation_spend_cap_usd": 20,
  "input": {
    "product_url": "https://item.gmarket.co.kr/Item?goodscode=4364915596",
    "product_name": "AHC 마스터즈 아쿠아리치 선크림 듀오세트",
    "on_card_name": "AHC 아쿠아리치 선크림 듀오",
    "host_image_url": "https://media.sume.com/assets/asset_707ef7a4149340d494a12278ef4e95fe/source.png",
    "vo_language": "ko",
    "price": { "list": "32,900원", "sale": "23,030원", "discount_label": "30%" },
    "highlights": ["SPF50+ PA++++ 4중 UV 에이징 케어", "…"],
    "script": {
      "segments": [
        { "tag": "INTRO", "text": "안녕하세요, AI 쇼호스트 유라입니다. …" },
        { "tag": "MID", "text": "구성은 삼십 밀리리터 본품 네 개, 듀오세트 두 개이고 …" },
        { "tag": "FIN", "text": "가격은 정가 삼만 이천구백원에서 이만 삼천삼십원, …" }
      ]
    }
  }
}
```

**`output_schema` is the whole object** — `{ name, strict, schema }` with the
JSON Schema inline. The registered name on its own is not enough. Pair it with
`primary_output_key`, and confirm it took: the create response echoes
`output_schema.source`, which reads `request_override` when your schema is the
one in force.

**`generation_spend_cap_usd` has to be non-zero** and comfortably above your
estimate — a zero or missing cap is not "no limit". $20 was the cap on the run
above; it billed $7.02.

**`input` is flat snake\_case** — `product_url`, `product_name`, `on_card_name`,
`host_image_url`, `vo_language`, `price`, `highlights`, `script.segments`. No
nesting under a wrapper key, no camelCase.

**Partner ALL-CAPS segment tags work.** `INTRO` / `MID` / `FIN` need no
conversion before you send them; the receipt reports them back normalized to
`Intro` / `Mid` / `Fin` in `act` and `tags`. Match on the receipt's casing, not
on what you sent.

Anything that has no recognized `input` key belongs in `instruction`. That is
where the run above put "use the segments verbatim", "do not lengthen the
script", "no BGM, no captions, 9:16".

### Script length is the duration control

There is no duration parameter. The finished cut is as long as the script takes
to read, so the only lever on length is how much text goes into
`script.segments`. On Korean copy the ratio across this pack was roughly **8.2
characters per second** of finished video — 283 characters came back as a
34.72 s cut. Use it to size a slot before you write the script; it is a planning
number, not an SLA.

You get a run back:

```json
{
  "id": "arun_42ed6a48b3d44092",
  "thread_id": "thr_96012fce-d2e8-445b-bee0-d939e355db02",
  "status": "processing"
}
```

Wait for terminal — poll [`status_url` / `result_url`](/formats/runs) or take
the [run webhook](/agents/run-webhooks) (`format.run.terminal`). One webhook per
turn, not one per scene.

When it lands, check the receipt actually carries the script you sent before you
show it to an operator. `middle: []` and `closing: []` satisfy the schema, so a
run that read only part of the script can still come back `completed` with no
error on it. Comparing the returned `script` text against your segments is a
cheap guard.

## 2. Example receipt

The snippet above is a fill-in template. Everything from here down is copied
verbatim out of a **real production run** — `arun_42ed6a48b3d44092` on Format
v5, a Korean sunscreen cut, 11 scenes, 73.36 s, $14.96 — so every
`media.sume.com` URL below is a live file you can open. Abridged to 3 of the 11
scene rows (`sc_1`–`sc_3`, `sc_5`, `sc_6`, `sc_8`–`sc_10` dropped for length).
Schema details: [Structured output](/formats/structured-output).

```json
{
  "live_title": "AHC 아쿠아리치 선크림 듀오 라이브",
  "live_description": "SPF50+ PA++++ 선케어와 듀오세트 구성, 증정 혜택을 한 번에 소개합니다.",
  "full_video": {
    "type": "video",
    "url": "https://media.sume.com/artifacts/artf_ANhCBRoC8EHphQqGSniAPaVDqZYnYE_E/timeline.mp4",
    "content_type": "video/mp4",
    "file_name": null,
    "size_bytes": 12049030,
    "width": 720,
    "height": 1280,
    "duration_ms": 73360,
    "expires_at": null
  },
  "opening": [
    {
      "id": "sc_0",
      "role": "talk",
      "act": "Intro",
      "status": "succeeded",
      "script": "안녕하세요, AI 쇼호스트 유라입니다.",
      "tags": ["Intro"],
      "duration_seconds": 2.67,
      "video": {
        "type": "video",
        "url": "https://media.sume.com/artifacts/artf_l67fvKiIvfr2VGKnGCJ9oBP8UoJOHyAi/video.mp4",
        "content_type": "video/mp4",
        "file_name": null,
        "size_bytes": null,
        "width": 720,
        "height": 1280,
        "duration_ms": 2670,
        "expires_at": null
      }
    }
  ],
  "middle": [
    {
      "id": "sc_4",
      "role": "broll",
      "act": "Mid",
      "status": "succeeded",
      "script": "이번에는 듀오세트 두 개, 즉 삼십 밀리리터 용량 네개로 구성되어 있어요. 여기에 증정품까지 함께 준비되어 있는데요.",
      "tags": ["Mid"],
      "duration_seconds": 7.2,
      "video": {
        "type": "video",
        "url": "https://media.sume.com/artifacts/artf_ZVzP_Y02FpCdBIf2g-0Z6vLdpcfYaAw2/timeline.mp4",
        "content_type": "video/mp4",
        "file_name": null,
        "size_bytes": null,
        "width": 720,
        "height": 1280,
        "duration_ms": 7200,
        "expires_at": null
      }
    }
  ],
  "closing": [
    {
      "id": "sc_7",
      "role": "under_banner",
      "act": "Fin",
      "status": "succeeded",
      "script": "특히 유브이비뿐 아니라 롱 유브이에이, 숏 유브이에이까지 모두 차단하는 사중 유브이 에이징 케어가 적용됐다는 점이 눈에 띄는데요. …",
      "tags": ["Fin"],
      "duration_seconds": 17.76,
      "video": {
        "type": "video",
        "url": "https://media.sume.com/artifacts/artf_nlQMSVkbv8OlbOWfycZQmNj7LH3VAy7/timeline.mp4",
        "content_type": "video/mp4",
        "file_name": null,
        "size_bytes": null,
        "width": 720,
        "height": 1280,
        "duration_ms": 17760,
        "expires_at": null
      }
    }
  ],
  "schedule_images": []
}
```

For a “retry this clip” control, persist four things off this turn:

| Store | From | Used for |
| --- | --- | --- |
| `run.id` | create response | becomes `previous_run_id` on the retry |
| `thread_id` | create response | groups every turn of one production — **read-only**, never sent back in a body |
| `scene.id` | each receipt row | names the clip to retry — frozen for the production, so never key off array index |
| `scene.status` | each receipt row | `stand-in` / `failed` is what enables the retry button |

Store `scene.id` as an opaque string. This run returned `sc_0` … `sc_10`;
older productions on earlier Format versions returned other shapes. Persist
whatever the receipt gives you and never parse or reconstruct it.

Every scene in this run came back `succeeded`, so the retry below is an
operator asking for a different take, not a repair. The other trigger is a row
that came back retryable — a dashboard lights up “retry this clip” on either
of these:

```json
{ "id": "sc_7", "status": "stand-in" }
{ "id": "sc_9", "status": "failed" }
```

Illustrative statuses only — this production returned 11/11 `succeeded`.

## 3. Example retry

Dashboard loop: operator selects a clip → your backend already has
`previous_run_id` (create run id) and `scene_id` (from the receipt row). Post
the same Format endpoint with that `scene_id` in `input` and the **same
`output_schema` as create** so the typed receipt stays the same shape.

Do **not** put `thread_id` in the body. It is a response field; sending it is
rejected with `unknown_parameter`. `previous_run_id` is the only continuation
key.

What is left is `instruction` — and that is the field that decides what you get
back.

### Prefer concrete per-scene feedback

When the operator clicks “retry this clip” because they do not like the take,
ask them why and put their answer in `instruction`: **which scene, what is
wrong with it, how it should change.** A retry that carries a real note is the
recommended shape; the fixed template below is the fallback, not the default.

Four parts, in the instruction, in whatever language the production runs in:

1. the scene — `scene_id` and role, repeated in prose so the note is unambiguous
2. the observed defect — what the operator actually saw in this take
3. the concrete fix — what to do instead, as instructions, not adjectives
4. the guardrails — keep the VO spine, script text, host, wardrobe, set;
   vertical 9:16, no BGM, no captions

Both instruction examples below are lifted from a second production on this
recipe — a different thread from the receipts above, which is why their
`previous_run_id` is not the one in §2. They are shown as bodies, not receipts.

The first is a `broll` scene that came back as a packshot floating in an empty
room. Note the banner rule in it: on-frame card and banner typography has to
finish inside the **top 40 %** of the frame, because the live UI covers the
bottom.

<!-- api-call-example:format-mobidoo-lc-retry-scene-instructed -->

A `talk` scene reads the same way — same four parts, a different defect. This
one was a greeting where the host's gaze dropped and her eyes closed on some
frames:

```json
{
  "previous_run_id": "arun_a045f6a01f5d4b20",
  "instruction": "지정한 씬 하나만 다시 만들어 주세요. 다른 씬과 VO 스파인, 대본 텍스트는 한 글자도 바꾸지 마세요.\n\n고칠 씬: sc_0 (talk / Intro). 대사는 \"안녕하세요, AI 쇼호스트 유라입니다.\" 그대로입니다.\n지금 무엇이 잘못됐는지: 인사 구간인데 호스트의 시선이 아래로 떨어지고 눈이 감기는 프레임이 있습니다.\n\n어떻게 바꿀지:\n- 호스트가 카메라를 더 정면으로 바라보게 해 주세요. 인사 내내 시선이 카메라에 머물러야 합니다.\n- 눈을 뜬 상태로 또렷하게, 표정은 조금 더 밝고 따뜻한 톤으로.\n- 대사 텍스트와 길이는 절대 바꾸지 마세요. 같은 문장, 같은 목소리입니다.\n- 동일 인물 유지. 헤어, 의상, 세트는 지금 그대로 두세요.\n- 세로 9:16, 무BGM, 무자막 유지.",
  "input": { "scene_id": "sc_0" },
  "output_schema": { "...": "identical to create" },
  "primary_output_key": "full_video",
  "generation_spend_cap_usd": 3
}
```

Why bother: a `talk` scene retried with the bare template has come back
**byte-identical** to the take the operator rejected, while the same scene
retried with a note like the one above re-rendered and fixed the gaze. Treat
that as integrator guidance, not an API guarantee — a bare retry is not
promised to be identical and an instructed one is not promised to change. What
you control is how much the instruction tells the run. §5 measures one of these
end to end — eight named fixes on a price card, every one applied, six sibling
clips untouched.

### The bare template is the mechanical fallback

For a `stand-in` or `failed` row there is nothing to describe — the operator
never saw a take to object to. A fixed template is the right call there, and it
is also the honest default for a plain “roll it again”:

<!-- api-call-example:format-mobidoo-lc-retry-scene -->

New run, same thread:

```json
{
  "id": "arun_808e4eb3e6c04cf7",
  "thread_id": "thr_96012fce-d2e8-445b-bee0-d939e355db02",
  "previous_run_id": "arun_42ed6a48b3d44092",
  "status": "processing"
}
```

Retrying several clips at once is the same call with `scene_ids` and the plural
instruction — nothing else changes:

```json
{
  "input": { "scene_ids": ["sc_7", "sc_9"] },
  "instruction": "Retry the selected scenes only. Keep every other scene and the VO spine unchanged."
}
```

Clip retry re-renders how a scene *looks*. Changing what it *says* — wording,
price read, line length — is a new production with new scene ids; see
[What clip retry can and cannot change](/enterprise/mobidoo/live-commerce#what-clip-retry-can-and-cannot-change).

### Two caveats before you wire the button

**Serialize retries on a thread.** Do not post two retries off the same
`previous_run_id` while the first is still running — one production thread,
one turn in flight. Queue the second retry until the first reaches a terminal
status; retrying two scenes in the same turn is what `scene_ids` is for.

**`previous_run_id` picks the thread, not a snapshot.** Naming an older run
does not branch from that older receipt — the new receipt is assembled from
the thread's latest scene state, so a scene some earlier turn already changed
stays changed. To diff a retry, compare it against the **previous turn's**
receipt, not against the create.

## 4. Example retry receipt

Again a full receipt — the whole `opening` / `middle` / `closing` list, not a
patch and not a delta. `sc_7` carries a new media URL; every other scene keeps
the exact URL it already had, and `full_video` is re-assembled. Same three rows
as above so you can diff them by eye:

```json
{
  "live_title": "AHC 아쿠아리치 선크림 듀오 라이브",
  "live_description": "SPF50+ PA++++ 선케어와 듀오세트 구성, 증정 혜택을 한 번에 소개합니다.",
  "full_video": {
    "type": "video",
    "url": "https://media.sume.com/artifacts/artf_QhBuhWEgA9eWzGheRZ65mEGM2aJF9TXj/timeline.mp4",
    "content_type": "video/mp4",
    "file_name": null,
    "size_bytes": 12080322,
    "width": 720,
    "height": 1280,
    "duration_ms": 73360,
    "expires_at": null
  },
  "opening": [
    {
      "id": "sc_0",
      "role": "talk",
      "act": "Intro",
      "status": "succeeded",
      "script": "안녕하세요, AI 쇼호스트 유라입니다.",
      "tags": ["Intro"],
      "duration_seconds": 2.67,
      "video": {
        "type": "video",
        "url": "https://media.sume.com/artifacts/artf_l67fvKiIvfr2VGKnGCJ9oBP8UoJOHyAi/video.mp4",
        "content_type": "video/mp4",
        "file_name": null,
        "size_bytes": null,
        "width": 720,
        "height": 1280,
        "duration_ms": 2670,
        "expires_at": null
      }
    }
  ],
  "middle": [
    {
      "id": "sc_4",
      "role": "broll",
      "act": "Mid",
      "status": "succeeded",
      "script": "이번에는 듀오세트 두 개, 즉 삼십 밀리리터 용량 네개로 구성되어 있어요. 여기에 증정품까지 함께 준비되어 있는데요.",
      "tags": ["Mid"],
      "duration_seconds": 7.2,
      "video": {
        "type": "video",
        "url": "https://media.sume.com/artifacts/artf_ZVzP_Y02FpCdBIf2g-0Z6vLdpcfYaAw2/timeline.mp4",
        "content_type": "video/mp4",
        "file_name": null,
        "size_bytes": null,
        "width": 720,
        "height": 1280,
        "duration_ms": 7200,
        "expires_at": null
      }
    }
  ],
  "closing": [
    {
      "id": "sc_7",
      "role": "under_banner",
      "act": "Fin",
      "status": "succeeded",
      "script": "특히 유브이비뿐 아니라 롱 유브이에이, 숏 유브이에이까지 모두 차단하는 사중 유브이 에이징 케어가 적용됐다는 점이 눈에 띄는데요. …",
      "tags": ["Fin"],
      "duration_seconds": 17.76,
      "video": {
        "type": "video",
        "url": "https://media.sume.com/artifacts/artf_Hy18xtqPCFNhombVGGN4r7laaFkZ37_o/timeline.mp4",
        "content_type": "video/mp4",
        "file_name": null,
        "size_bytes": null,
        "width": 720,
        "height": 1280,
        "duration_ms": 17760,
        "expires_at": null
      }
    }
  ],
  "schedule_images": []
}
```

What the two receipts prove, measured on this pair:

| | Create `arun_42ed6a48b3d44092` | Retry `arun_808e4eb3e6c04cf7` |
| --- | --- | --- |
| `thread_id` | `thr_96012fce-…` | same |
| scene rows returned | 11 | 11 — full list, not a patch |
| sibling scene URLs | — | 10 of 11 byte-identical |
| `sc_7` URL | `artf_nlQMSVkb…` | new — `artf_Hy18xtqP…` |
| `sc_7` duration | 17.76 s | 17.76 s — VO spine did not move |
| `full_video` | 73.360 s, 12,049,030 B | re-assembled, 73.360 s, 12,080,322 B |
| billable | $14.959638 | **$0.742530** — about a twentieth |
| wall | 11m08s | 2m53s |

The old `sc_7` URL still resolves `200` after the retry. Prior-turn artifacts
are immutable, so a create receipt you already stored stays valid — you are
free to keep both takes and let the operator pick.

**A regenerated clip is a new generation, not a re-encode.** Anything
generative inside the frame can drift between takes. In the pair above the
structured overlay facts held exactly (price, discount label, SPF grade, set
composition), but decorative on-pack copy rendered into the product art changed
wording. Do not promise your operators a pixel-identical frame with one detail
fixed; promise them another take.

## 5. A shorter pair, measured

§2–§4 walk a 73 s, 11-scene production. The same loop on the 34.72 s cut from §1
is worth a second look, because it puts a number on what a targeted retry costs
next to re-running the whole thing:

| | Create `arun_206837aa88d34c8d` | Instructed retry `arun_0c315b373e24433a` |
| --- | --- | --- |
| thread | `thr_45c64858-…` | same |
| `previous_run_id` | `null` | `arun_206837aa88d34c8d` |
| script sent | 283 KO chars, `INTRO` / `MID` / `FIN` | — |
| result | 34.72 s, 7 scenes, all `succeeded` | 34.72 s, 7 scenes, one moved |
| billed | $7.0221 | **$0.7361** |
| wall | 10m 08s | **2m 24s** |

The retry body is the §3 shape and nothing more: `previous_run_id`,
`input: { "scene_id": "sc_5" }`, the **same `output_schema` as create**, and an
`instruction` that names the scene, the defect and the fix. `sc_5` was the
closing price card — an `under_banner` scene carrying the product name and price
and little else. Abridged:

> 고칠 씬: sc_5 (under_banner / Fin, 클로징 가격 카드, 9.44초).
>
> 지금 무엇이 잘못됐는지: 카드에 제품명과 가격만 있고, 이 상품의 핵심 소구점인
> 자외선 차단 지수(SPF50+ PA++++)가 빠져 있습니다. … "30% 할인"이 카드에서 가장
> 크게 잡혀 있고, 정작 소비자가 결제하는 금액인 23,030원이 그보다 작게 보입니다.
> … 제품 이미지 위쪽에 빈 여백이 크게 남아 카드 상단이 비어 보입니다.
>
> 어떻게 바꿀지: 제품명 바로 아래에 "SPF50+ PA++++" 한 줄을 배지 형태로 추가해
> 주세요. … 판매가 23,030원을 카드에서 가장 큰 요소로 만들어 주세요. "30%
> 할인"은 그보다 작게, 보조 배지로 내려 주세요. 정가 32,900원은 취소선을 유지해
> 주세요. … 배경 톤, 카드 색감, 제품 사진 구도(박스 1개 + 튜브 2개)는 지금 그대로
> 두세요.

Eight specific asks, each one an edit rather than an adjective, and behind them
a list of what must not change. All eight landed, and nothing outside `sc_5`
moved:

| Row | Changed |
| --- | --- |
| `sc_0`–`sc_4`, `sc_6` | no — same artifact URL on all six |
| `sc_5` | yes — `artf_psot5zRM…/images.png` → `artf_GWQTB6n2…/timeline.mp4` |
| `full_video` | yes — re-concat, still 34,720 ms, new bytes |

**Diff per-scene URLs, not `full_video`.** `full_video` is re-assembled on every
turn, so its URL changes whether or not any clip re-rendered — it proves
nothing on its own. The per-scene URL is the signal, and for `talk` scenes hash
the bytes too, since a talk retry has come back byte-identical on a fresh URL.

**`scene.video` is not always a video.** In the create receipt `sc_5.video` was a
PNG still — `"type": "image"`, `"content_type": "image/png"`, with `width`,
`height` and `duration_ms` all `null` and the length carried only by the row's
`duration_seconds: 9.44`. After the retry the same scene id held an MP4:
`"type": "video"`, 720×1280, `duration_ms: 9440`. Read `type` and `content_type`
off each row rather than assuming a video element can play it, take the length
from `duration_seconds`, and expect the type to change across turns on one scene
id.

**A targeted retry cost about a tenth of a re-create here** — $0.74 against
$7.02, 2m 24s against 10m 08s. When an operator wants one clip changed, retry
that clip; re-running the production is the expensive way to get the same fix.

That’s the loop. Details live on
[Live Commerce API](/enterprise/mobidoo/live-commerce) and
[Structured output](/formats/structured-output).
