Mobidoo

Best practices

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.

API surface: Live Commerce API.
How input and typed receipts work in depth: Calling a Format — what is carried, 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.

Example create — AHC 30 s live cut

POST /v1/formats/{handle}/{slug}/runs

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:

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_caseproduct_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:

Wait for terminal — poll status_url / result_url or take the run webhook (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 runarun_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_1sc_3, sc_5, sc_6, sc_8sc_10 dropped for length). Schema details: Structured output.

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

StoreFromUsed for
run.idcreate responsebecomes previous_run_id on the retry
thread_idcreate responsegroups every turn of one production — read-only, never sent back in a body
scene.ideach receipt rownames the clip to retry — frozen for the production, so never key off array index
scene.statuseach receipt rowstand-in / failed is what enables the retry button

Store scene.id as an opaque string. This run returned sc_0sc_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:

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.

Example retry — scene note in the instruction

POST /v1/formats/{handle}/{slug}/runs

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:

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”:

Example retry — selected scene on the same thread

POST /v1/formats/{handle}/{slug}/runs

New run, same thread:

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

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.

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:

What the two receipts prove, measured on this pair:

Create arun_42ed6a48b3d44092Retry arun_808e4eb3e6c04cf7
thread_idthr_96012fce-…same
scene rows returned1111 — full list, not a patch
sibling scene URLs10 of 11 byte-identical
sc_7 URLartf_nlQMSVkb…new — artf_Hy18xtqP…
sc_7 duration17.76 s17.76 s — VO spine did not move
full_video73.360 s, 12,049,030 Bre-assembled, 73.360 s, 12,080,322 B
billable$14.959638$0.742530 — about a twentieth
wall11m08s2m53s

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_206837aa88d34c8dInstructed retry arun_0c315b373e24433a
threadthr_45c64858-…same
previous_run_idnullarun_206837aa88d34c8d
script sent283 KO chars, INTRO / MID / FIN
result34.72 s, 7 scenes, all succeeded34.72 s, 7 scenes, one moved
billed$7.0221$0.7361
wall10m 08s2m 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:

RowChanged
sc_0sc_4, sc_6no — same artifact URL on all six
sc_5yes — artf_psot5zRM…/images.pngartf_GWQTB6n2…/timeline.mp4
full_videoyes — 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 and Structured output.