Cookbook
Each recipe on this page has the shape of a real production request. Placeholders replace the
data of the customer. Replace acme/live-commerce, the URLs, and the copy with your values. Keep
the structure.
Do this setup one time:
A live-commerce run with typed output and a webhook
This is the most frequent production call. It has a product page, a host image, a tagged script, and a schema that names the assembled cut and each scene. It also has a per-run cap and a webhook. Keep the body in a file. Real scripts are too long for a shell heredoc.
run-body.json:
What each key does:
| Key | Why it is there |
|---|---|
instruction | Decisions: use the script as written, the framing, and what not to add. It is prose, much shorter than 4000 characters. |
input | Data: the recipe reads the keys that it knows (product_url, host_image_url, vo_language, script, price). The other keys go to the run as context. You can put your own reference data (sheet_no) here, but it will not come back in output. |
output_schema | full_video is the deliverable. scenes[] gives you each clip with a stable id that you can retry. SumeMediaFile# is the built-in media shape. Each property is necessary. An optional property is a nullable property. |
primary_output_key | Makes primary_output_url the assembled cut. If a run fills scenes but not full_video, the receipt is failed, not a false success. |
generation_spend_cap_credits | The ceiling for this run, in credits. A production live-commerce run needs a much larger cap than a single-scene retry. |
communication.webhook_url | The API sends one signed POST when the run ends, thus no poll is necessary. Keep result_url as the backup. |
From the 202, store data.id and data.thread_id with your row. Use the first value to get
the receipt. Use the second value to group retries.
When the webhook arrives, payload.output.full_video.url is the show and
payload.output.scenes[] are the clips. The retry recipe below repairs a scene with
status: "stand-in" or "failed".
A webhook receiver
The receiver does four steps in this sequence. First, it verifies the signature against the raw
bytes. Then it answers 2xx quickly. Next, it dedupes on request_id. Only after these three
steps, it acts on outcome. The two versions below handle the oversized-receipt case
(payload: null).
Node
This version uses only the Web Request API. Thus, it works in a Next.js route handler, Hono,
Workers, or Deno. It uses verifyWebhook from @sume-com/sdk.
If you cannot use the SDK, you can write the check in a dozen lines. First, reject a timestamp
that is more than five minutes off. Then calculate the HMAC-SHA256 of ${timestamp}.${raw} with
your secret, hex-encoded. Compare the result in constant time against the value after sume-v1=
in x-sume-webhook-signature. The full function is on
Run webhooks.
Python
This version uses FastAPI. It reads the raw body before it parses the JSON.
Two problems occur in each first receiver. First, a framework that parses JSON for you destroys the signed bytes. Thus, read the raw body on this route. Second, a receiver that renders video before it answers uses all of the 10-second attempt budget. Then the API retries the delivery while the receiver works. Thus, record and then answer before you process the event.
You can do a test of the receiver without a real run. POST /v1/webhooks/test-deliveries (or
Send test on the dashboard) sends a webhook.test payload to your URL. To replay a real
delivery, use POST /v1/format-runs/{run_id}/webhook/redeliver.
Retry one scene on the same thread
A run is one turn of a conversation. To do a clip again, continue that conversation with
previous_run_id. Name the scene in the request. The voice track, the other clips, and the script
do not change. The API returns the full scene list, re-assembled.
Put the note of an operator in instruction (which scene, what is wrong, how it must change).
Keep input.scene_id as the machine-readable pointer. For two scenes at the same time, use
"scene_ids": ["sc_7", "sc_9"].
The API returns a new run (arun_…, a new receipt, its own webhook) on the same thread_id.
output.scenes[] is the full list again. The retried scene has a new URL, the other scenes keep
their URLs, and the API re-assembles full_video at a new URL. Plan the cap for a single-scene
retry as a fraction of the credits of the create. Measured production retries used approximately
one twentieth of the credits of the first run. Always send a cap.
A retry is a new take, not a re-encode. The API generates all the generative parts of that scene again. If the look changes, retry the scene. If the words, the host, or the product change, start a new production with new scene ids.
The API refuses the continuation with 400 previous_run_not_resumable when the earlier run left
nothing. It refuses with 409 previous_run_not_terminal while the earlier run is still in
progress. It refuses with 400 previous_run_format_mismatch if you address a different Format.
Refer to Continue a run.
Batch a sheet with bulk runs
One row of a broadcast sheet becomes one item. The sheet becomes one POST …/bulk-runs. The
queue keeps concurrency runs in progress. When a slot becomes free, the queue starts the next
run.
bulk-body.json has two keys. Each item is the same as the body of a single run (the first
recipe on this page), one time for each row. On the client side, skip rows that do not have a
finished script. There is no empty item.
202 returns a queue (frq_…). In this queue, the first concurrency items are already
running. Keep your own sheet-row ↔ index map. items[i].index is the position that you
submitted.
To get the progress, poll the queue, not the children:
The batch shape makes three things clear. The queue has no webhook, because
communication.webhook_url is per item. Queue completed means that each item is terminal, not
that each item succeeded. Thus, branch on counts.failed and on the output_error of each child.
A used Idempotency-Key returns 202 with the old queue, thus mint a new key for each batch.
For the full contract, refer to Bulk runs.
Wait for a run without a webhook
If you cannot expose an endpoint (a script, a CI job, a one-off), poll with backoff. Stop when the run gets to a terminal status.
In TypeScript, subscribeFormatRun does the create and the loop in one call. It resolves on all
terminal statuses. Thus, a failed run is a result that you branch on, not an exception:
Its default timeout is 20 minutes. For long-form video, increase the timeout. Use expires_at on
the receipt as the true ceiling. A timeout does not cancel the run. The run continues, and it
continues to use credits. Thus, keep the run id and read the run again at a later time.
Read a Format before you call it
This call helps in a settings screen or a preflight. With it, you can make sure that the address resolves for the key that you hold. You can also see what the Format takes and its cap.
You can get a 404 format_not_found here with a key that you think is right. This almost always
means that you need the other key. Team Formats answer only to keys created in the team workspace.
A 403 workspace_key_required tells you the same thing, with a more helpful message. You get it when
the owner of your key is a member of that team.
Next
- Create a run: all the fields of the body
- Runs and results: the receipt, the poll rules, and the webhook rules
- Errors and spend: what each code means and what to do
- Embed a Format in your product: key custody, spend tiers, and how to handle artifacts for a multi-tenant product