Embed a Format in your product
You have a product with your own customers. You want a button in your UI that makes a Sume-generated video or image for the customer who clicked the button. This page gives the end-to-end procedure for that.
The flow is always the same:
Your customer never talks to Sume. Your server holds one Sume API key. The server runs Formats for your customers and maps the results onto your own records.
For the invoke contract, read Calling a Format. To shape the result, read Structured output. This page gives the integration around those two topics.
0. The whole flow
With @sume-com/sdk (0.2.0+), the happy path is subscribeFormatRun:
one call creates the Format run and waits for the terminal receipt. In
production, we recommend a webhook (§4). Sume has no SSE stream yet. events_url
(/v1/format-runs/{id}/events) is a polled phase timeline. Thus, onStatus
gives the status from a poll, not from a log feed.
The SDK makes the work easier, but you do not have to use it. Each call here is
one HTTP request that you can make with fetch. The
API reference stays the source of truth for fields. The SDK
gives you the loops that nobody likes to write: subscribeFormatRun /
waitForRun and verifyWebhook.
In production, use the push path. Refer to §4 below, and to Waiting for runs for the tradeoff.
1. Key custody
One Sume account and one server-side key serve many of your customers. Sume has no per-end-user credential that you can give to a customer. Sume also has no browser-safe key.
| Rule | Why |
|---|---|
Keep the key in your server's environment. Never put the key in client JavaScript, a mobile bundle, or a NEXT_PUBLIC_* variable. | A Sume key spends your credits. Anyone who has the key can run any Format that you own, up to your caps. |
| Never proxy the key. Proxy the call. | A "pass-through" endpoint forwards the browser's payload and attaches your key. That is the same leak, one hop later. Your endpoint must accept your customer's identifiers and make the Sume request itself. |
| Give your endpoint your own authorization check. | Sume authenticates you, not your customer. Your product must decide if this customer can run that Format. |
| To rotate a key, create a new key and retire the old key. | You cannot add scopes to an existing key. Refer to the text below. |
Create the key at API Keys with the
formats:read and formats:write scopes.
Keys that Sume minted before the release of Format API triggers do not have
those scopes. You cannot add the scopes later. An old key fails every run with
403 insufficient_scope. Create a new key. Service-account keys cannot create
any Format runs. Their requests fail with details.reason of
service_account_format_runs_unsupported.
The client sends x-api-key. Do not add your own Authorization header. If a
request has both credentials, the API rejects it with 401 unauthorized.
Without the SDK, this call is a plain POST to
https://api.sume.com/v1/formats/{handle}/{slug}/runs. Refer to
Calling a Format.
Idempotency-Key
Your customers will double-click. Your job queue will redeliver. Each of these events makes two paid runs. To prevent this, derive the key from the item that the run makes. Do not derive the key from the time of the request.
| Do | Do not |
|---|---|
| Hash your own stable identifiers: tenant id, order id, Format slug, and a version. Bump the version only when you want a re-run. | uuidv4() per request. Then the header has no effect. |
| Namespace the key by customer. | A key that you build only from the order id. Two tenants with the same ids then share a run. |
Before you return to the browser, store the returned run_id with your record. | Derive the key again later to find the run. You can do this, but a stored id is one lookup, not one replay. |
These are the accurate replay semantics:
| Replay | Result |
|---|---|
| Same key, same body | 200 with the original run receipt and idempotency_hit: true. Sume does not start a second run or make a second charge. |
Same key, different body (a different instruction is also a different body) | 409 idempotency_conflict. Nothing runs. |
| No key | Every call starts a new paid run. |
3. Pick a spend cap per run
Every Format has a generation spend cap. A run can never spend more than its own effective cap. If a run names no cap, the run inherits the Format's cap.
generation_spend_cap_usd on the run request names the ceiling of that run, up
to the platform maximum of $500. Sume accepts a number above the Format's own
cap. A number above $500 gets a 400.
Thus, the cap is the natural place for your own plan tiers:
Read the Format's own cap from
PublicFormat.generation_spend_cap_usd_micros. This value is always a number.
If a Format never named a cap, the default is $400. The receipt gives the
effective cap of a run as usage.generation_spend_cap_usd_micros.
Caps set a limit on generation spend. The terminal receipt reports the actual
spend of the run against that ceiling as usage.billable_amount_usd_micros.
This value is sufficient to show a per-run cost in your own UI, but it does not
include the agent's own LLM turn. Thus, it is not the total cost of the run, and
it is not an invoice. Bill your customer from your own records. Reconcile
against GET /v1/usage.
4. Receive the result
A Format run is asynchronous. You have two ways to know when the run is complete. Both ways give the same receipt.
| Webhook | Poll | |
|---|---|---|
| You do | Register communication.webhook_url, verify the signature, and return 2xx. | Loop on status_url until the run is terminal. |
| Available | api.dev.sume.com and api.sume.com. | Everywhere. |
| Costs you | One public HTTPS endpoint. | One timer per in-flight run. |
Build the webhook receiver first. Keep the poll path (or
subscribeFormatRun) wired as the backup for a time when your
endpoint is down. For the full contract, refer to
Run webhooks. For a complete receiver in Node and
Python, refer to Cookbook.
Verify every delivery
Sume signs the raw body with HMAC-SHA256 over <timestamp>.<raw_body>. Sume
sends sume-v1=<hex> in x-sume-webhook-signature. Verify the signature
before you parse the body.
verifyWebhook is async because it runs on WebCrypto. WebCrypto lets you use
it from Workers and Deno, and also from Node. For options and header names,
refer to Verifying webhooks. You can also write the check
yourself, in a different language or at a gateway in front of your app. That
check is a dozen lines against a published scheme:
Run webhooks.
Read your signing secret on the Webhooks tab of the dashboard
(/dashboard/webhooks). Or, get it from GET /v1/webhooks/signing-secret with
any API key that has account:read. Sume derives the secret for your
workspace. Thus, a valid signature proves that Sume signed the delivery for you,
not for anyone who has a shared secret. Store the secret as
SUME_COM_WEBHOOK_SIGNING_SECRET, the name that Sume's delivery worker uses.
Store it in the same way as the API key.
Four items frequently cause problems for integrators here:
- Verify against the raw body. Some frameworks parse JSON for you and give you an object. Such a framework already destroyed the signed bytes. In Express, mount
express.raw({ type: "application/json" })on this route only. - Return
2xxfast, then work. The delivery attempt budget is 10 seconds. If a receiver renders video before it responds, Sume retries the delivery while the receiver works. - Dedupe on
request_id. Retries send the same value again. Ten attempts against an unreliable endpoint must not become ten rows in your database. 3xxis not a delivery. Sume does not follow redirects. Register the final URL, not a redirector, and not an HTTP URL. Sume rejects non-HTTPS, localhost, and private-range URLs at submit with400 invalid_request, and checks them again at delivery time.
Job webhooks are a different surface
If you also call POST /v1/models/... directly, those calls emit
generation-job webhooks (job.completed and related events) with job_id.
Webhooks gives the details. These webhooks have
different events, a different payload, and a different lifecycle.
The signature scheme is the same, so one verifier covers both. But route on
event, and never assume that a body has run_id. A single receiver for both
must first switch on the event name. The receiver must send a 204 for any
event that it does not know. Thus, a new event type does not cause a 500 and a
retry storm.
5. Map artifacts into your UI
A terminal completed receipt has three fields that contain media:
| Field | Use it for |
|---|---|
primary_output_url | The one item to show. It is null when the Format produced no single primary file. |
artifacts[] | All the items that the run generated: { id, type, url, content_type, size_bytes, width, height, duration_ms, checksum_sha256 }. |
output | The Format's structured result, projected onto output_schema. Media in the result points to the same URLs. Refer to Structured output. |
Every URL is a durable media.sume.com HTTPS URL. These URLs do not
expire. Thus, an embed is practical. You can store the URL with your record
and render it forever, with no refresh step.
Design your integration for these two results:
- A durable URL is a public URL. Anyone who has the URL can fetch it. The URL will go into your logs, your error reports, and your customer's browser history. If your product must never let customer A see customer B's output, proxy the bytes through your own authenticated route. Or, at receipt time, copy the bytes into your own storage and serve them from there.
- Copy, or link, but decide. A link is free and instant. A copy costs you storage, but the copy stays available if you stop the use of Sume. If you want that guarantee, copy on the webhook, before you mark the record ready.
artifacts[] is empty until the run is terminal. Sume fills it from the same
job ledger that fills output. Thus, the two fields always agree.
6. Failure taxonomy
Runs fail in four different places. Your UI must have a different message for each place. If you show only "something went wrong" for all four, you will quickly get a support ticket that you cannot answer.
At submit — nothing ran, nothing was charged
| Code | Status | What it means for your integration |
|---|---|---|
insufficient_scope | 403 | Your key does not have formats:read / formats:write, or it is a service-account key. Correct your key, not your request. |
format_not_found | 404 | Unknown handle, unknown slug, or a Format that this key does not own. A Format by Sume also returns this code at an account handle. Such a Format answers at sume/{slug}. |
format_not_forkable | 409 | You addressed a built-in capability, not a Format. Call one of the Formats by Sume, or one of your own Formats. |
format_api_trigger_disabled | 409 | The API trigger is off for that Format. |
format_inactive | 409 | The Format is inactive. |
format_run_in_progress | 409 | Occurs only with on_active_run: "reject". Retry later, or show "already running". |
idempotency_conflict | 409 | Same key, different body. Your key derivation is not stable. Correct it before you retry. |
invalid_request | 400 | Includes a webhook_url that is not a public HTTPS URL. |
A 4xx here is a bug in your call, not a transient error. If you retry an
insufficient_scope forever, you make a frequent and expensive mistake.
At run — a run existed, and did not produce a result
status is failed, and the receipt has error and output_error. Important
codes:
error.code | Meaning |
|---|---|
unattended_blocked | The run hit a gate that it could not pass without a human. Examples: no avatar that matches, or a missing input that the run asks about in chat. Sume writes the message so that you can show it. |
format_run_failed | The generic failure. Read error.message. A run that tried to spend more than its cap also gets this code. Thus, if the runs of a plan tier fail again and again, first compare them with usage.generation_spend_cap_usd_micros. |
To retry a failed run, use a new idempotency key. The old key is bound to the run that failed. If you use the old key again, you get the same failed receipt.
API runs are unattended. A Format for interactive chat can pause to ask a
human for approval. Over the API, those approvals are pre-granted, and the run
continues in its spend cap. Thus, completed is a real result. You will never
get a half-finished run that shows as done.
Terminal, but not a failure
status | Handle it as |
|---|---|
canceled | Someone called POST /v1/format-runs/{id}/cancel. Sume sends no webhook. Use the cancel response and poll status_url. |
skipped | You passed on_active_run: "skip", and a run was already in flight. A skipped run never delivers a webhook. The create response already told you, with skip_reason populated. Read the status from that response. Do not wait for a POST. The Format default is allow (concurrent runs). The Action default is skip. Do not copy Action examples into Format calls. Refer to Calling a Format. |
At delivery — the run is fine, your endpoint was not
A delivery result never changes the run. After ten refused attempts, you have a
failed delivery and a run that is still completed. Fetch the run from
result_url.
One delivery case needs code. A receipt over 1 MiB arrives with
payload: null and error.code of payload_too_large. That delivery gives
the result_url, and you fetch the receipt from it. If a handler assumes that
payload is an object, it will throw on your largest, most valuable runs.
Checklist before you ship
-
SUME_API_KEYis server-only and is not in any client bundle. - Your run endpoint authorizes your own customer before it calls Sume.
- You derive
Idempotency-Keyfrom stable identifiers, and you do not generate it per request. -
generation_spend_cap_usdis set per plan tier. - The webhook receiver verifies
sume-v1against the raw body and returns2xxin less than a second. - Your receiver dedupes deliveries on
request_id. - For
payload: null(oversized receipt), your handler usesresult_url. - You set
SUME_COM_WEBHOOK_SIGNING_SECRETfrom the value on/dashboard/webhooks, and its fingerprint matchesx-sume-webhook-secret-fingerprinton a delivery. - Poll on
status_urlstays wired as a backup (webhooks are live onapi.sume.com). - Every error code above maps to a message that your support team can use.
Next
- TypeScript SDK — the client that this page uses, from install to first run
- Calling a Format — the invoke contract
- Bulk runs — a server-side queue of those runs
- Structured output — schemas, the projection, failure modes
- Runs and results — the receipt, field by field
- Run webhooks — full details of delivery, signatures, and retries
- Webhooks — generation-job webhooks, the other surface