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.

RuleWhy
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.

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

ReplayResult
Same key, same body200 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 keyEvery 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.

WebhookPoll
You doRegister communication.webhook_url, verify the signature, and return 2xx.Loop on status_url until the run is terminal.
Availableapi.dev.sume.com and api.sume.com.Everywhere.
Costs youOne 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 2xx fast, 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.
  • 3xx is 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 with 400 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:

FieldUse it for
primary_output_urlThe 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 }.
outputThe 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

CodeStatusWhat it means for your integration
insufficient_scope403Your key does not have formats:read / formats:write, or it is a service-account key. Correct your key, not your request.
format_not_found404Unknown 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_forkable409You addressed a built-in capability, not a Format. Call one of the Formats by Sume, or one of your own Formats.
format_api_trigger_disabled409The API trigger is off for that Format.
format_inactive409The Format is inactive.
format_run_in_progress409Occurs only with on_active_run: "reject". Retry later, or show "already running".
idempotency_conflict409Same key, different body. Your key derivation is not stable. Correct it before you retry.
invalid_request400Includes 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.codeMeaning
unattended_blockedThe 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_failedThe 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

statusHandle it as
canceledSomeone called POST /v1/format-runs/{id}/cancel. Sume sends no webhook. Use the cancel response and poll status_url.
skippedYou 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_KEY is server-only and is not in any client bundle.
  • Your run endpoint authorizes your own customer before it calls Sume.
  • You derive Idempotency-Key from stable identifiers, and you do not generate it per request.
  • generation_spend_cap_usd is set per plan tier.
  • The webhook receiver verifies sume-v1 against the raw body and returns 2xx in less than a second.
  • Your receiver dedupes deliveries on request_id.
  • For payload: null (oversized receipt), your handler uses result_url.
  • You set SUME_COM_WEBHOOK_SIGNING_SECRET from the value on /dashboard/webhooks, and its fingerprint matches x-sume-webhook-secret-fingerprint on a delivery.
  • Poll on status_url stays wired as a backup (webhooks are live on api.sume.com).
  • Every error code above maps to a message that your support team can use.

Next