Embed a Format in your product

You have a product with your own customers. You want a button in your UI that produces a Sume-generated video or image for the customer who clicked it. This page is the end-to-end recipe for that.

The shape is always the same:

Your customer never talks to Sume. Your server holds one Sume API key, runs Formats on their behalf, and maps the results back onto your own records.

Read Calling a Format for the invoke contract itself and Structured output for shaping the result. This page is about the integration around them.

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. Prefer a webhook when delivery is on (§4). There is no SSE stream yet — events_url is null — so onStatus is status polling, not a log feed.

The SDK is a convenience, not a requirement — every call here is one HTTP request you can make with fetch, and the API reference remains the source of truth for fields. What it buys you is the loops nobody enjoys writing: subscribeFormatRun / waitForRun and verifyWebhook.

Once webhook delivery is on for your environment you want the push path instead — §4 below, and Waiting for runs for the tradeoff.

1. Key custody

One Sume account, one server-side key, many of your customers. Sume has no per-end-user credential to hand out, and there is no browser-safe key.

RuleWhy
The key lives in your server's environment, never in client JavaScript, a mobile bundle, or a NEXT_PUBLIC_* variable.A Sume key spends your credits. Anyone holding it can run any Format you own, up to your caps.
Never proxy the key. Proxy the call.A "pass-through" endpoint that forwards the browser's payload with your key attached is the same leak, one hop later. Your endpoint should accept your customer's identifiers and construct the Sume request itself.
Give your endpoint your own authorization check.Sume authenticates you, not your customer. Deciding that this customer may run that Format is your product's job.
Rotate by creating a new key and retiring the old one.Scopes cannot be added to an existing key — see below.

Create the key at API Keys with the formats:read and formats:write scopes.

Keys minted before Format API triggers shipped do not carry those scopes, and scopes cannot be added afterward. An old key fails every run with 403 insufficient_scope. Create a new one. Service-account keys cannot create Format runs at all — they fail with details.reason of service_account_format_runs_unsupported.

The client sends x-api-key; do not add an Authorization header of your own, because the API rejects a request that carries both credentials with 401 unauthorized. Without the SDK this is a plain POST to https://api.sume.com/v1/formats/{handle}/{slug}/runs — see Calling a Format.

Idempotency-Key

Your customers will double-click. Your job queue will redeliver. Both turn into two paid runs unless the key is derived from the thing being made, not from the moment of asking.

DoDo not
Hash your own stable identifiers — tenant id, order id, Format slug, and a version you bump when you deliberately want a re-run.uuidv4() per request. It makes the header decorative.
Namespace by customer.A key built only from the order id — two tenants with colliding ids share a run.
Store the returned run_id against your record before you return to the browser.Rely on re-deriving the key later to find the run. You can, but a stored id is one lookup instead of one replay.

Replay semantics, exactly:

ReplayResult
Same key, same body200 with the original run receipt and idempotency_hit: true. No second run, no second charge.
Same key, different body — including a different instruction409 idempotency_conflict. Nothing runs.
No keyEvery call starts a new paid run.

3. Pick a spend cap per run

Every Format carries a generation spend cap. A run can never spend past its own effective cap, and the Format's cap is what a run inherits when it names none.

generation_spend_cap_usd on the run request names that run's own ceiling, up to the platform maximum of $500 — a number above the Format's own cap is honored, and anything above $500 is a 400.

That makes the cap the natural place to express your own plan tiers:

Read the Format's own cap from PublicFormat.generation_spend_cap_usd_micros — always a number, defaulting to $400 for a Format that never named one. The effective cap for a given run comes back on the receipt as usage.generation_spend_cap_usd_micros.

Caps bound generation spend. The terminal receipt reports what the run actually spent against that ceiling as usage.billable_amount_usd_micros, which is enough to show a per-run cost in your own UI — but it excludes the agent's own LLM turn, so it is not the run's total cost and it is not an invoice. Bill your customer from your own records and reconcile against GET /v1/usage.

4. Receive the result

A Format run is asynchronous. You have two ways to learn it finished, and they carry the identical receipt.

WebhookPoll
You doRegister communication.webhook_url, verify the signature, return 2xx.Loop on status_url until terminal.
AvailableLive on api.dev.sume.com. Accepted but not delivered on api.sume.com yet.Everywhere, today.
Costs youOne public HTTPS endpoint.One timer per in-flight run.

Build the webhook receiver now — the contract is final. On development, a URL you supply starts receiving signed POSTs today. On production, keep the poll path (or subscribeFormatRun) until delivery is enabled; the same receiver then works without code changes. Full contract: Run webhooks.

Verify every delivery

Sume signs the raw body with HMAC-SHA256 over <timestamp>.<raw_body> and sends sume-v1=<hex> in x-sume-webhook-signature. Verify before you parse.

verifyWebhook is async because it runs on WebCrypto, which is what keeps it usable from Workers and Deno as well as Node. Options and header names: Verifying webhooks. Writing the check by hand — in another language, or at a gateway in front of your app — is a dozen lines against a published scheme: Run webhooks.

Read your signing secret on the Webhooks tab of the dashboard (/dashboard/webhooks), or from GET /v1/webhooks/signing-secret with any API key carrying account:read. It is derived for your workspace, so a valid signature proves the delivery was signed for you rather than for anyone holding a shared secret. Store it as SUME_COM_WEBHOOK_SIGNING_SECRET, the same name Sume's delivery worker uses, the same way you store the API key.

Four things that bite integrators here:

  • Verify against the raw body. A framework that parses JSON for you and hands you an object has already destroyed the bytes that were signed. In Express, mount express.raw({ type: "application/json" }) on this route only.
  • Return 2xx fast, then work. The delivery attempt budget is 10 seconds. A receiver that renders video before responding gets retried while it works.
  • Dedupe on request_id. Retries repeat it. Ten attempts against a flaky endpoint must not become ten rows in your database.
  • 3xx is not a delivery. Redirects are not followed. Register the final URL, not a redirector, and not an HTTP one — non-HTTPS, localhost, and private-range URLs are rejected at submit with 400 invalid_request and re-checked at delivery time.

Job webhooks are a different surface

If you also call POST /v1/models/... directly, those emit generation-job webhooks (job.completed and friends) with job_id, described in Webhooks. Different events, different payload, different lifecycle.

The signature scheme is identical, so one verifier covers both — but route on event and never assume a body has run_id. A single receiver handling both should switch on the event name first and treat anything unrecognized as a 204, so a new event type does not become a 500 and a retry storm.

5. Map artifacts into your UI

A terminal completed receipt carries three media-bearing fields:

FieldUse it for
primary_output_urlThe one thing to show. null when the Format produced no single primary file.
artifacts[]Everything 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 inside it points at the same URLs. See Structured output.

Every URL is a durable media.sume.com HTTPS URL. They do not expire, which is what makes embedding practical — you can store the URL against your record and render it forever without a refresh dance.

Two consequences worth designing around:

  • A durable URL is a public URL. Anyone who has it can fetch it. It will end up in your logs, your error reports, and your customer's browser history. If your product's model is that customer A must never see customer B's output, proxy the bytes through your own authenticated route, or copy them into your own storage at receipt time and serve from there.
  • Copy, or link, but decide. Linking is free and instant. Copying costs you storage but survives you ever leaving Sume. Copy on the webhook, before you mark the record ready, if you want that guarantee.

artifacts[] is empty until the run is terminal, and it is drawn from the same job ledger that fills output — the two always agree.

6. Failure taxonomy

Runs fail in four distinguishable places. Your UI needs a different message for each; collapsing them into "something went wrong" is the fastest way to a support ticket you cannot answer.

At submit — nothing ran, nothing was charged

CodeStatusWhat it means for your integration
insufficient_scope403Your key lacks formats:read / formats:write, or it is a service-account key. Fix your key, not your request.
format_not_found404Unknown handle, unknown slug, or a Format this key does not own. Also what a Format by Sume returns at an account handle — it answers at sume/{slug}.
format_not_forkable409You addressed a built-in capability rather than a Format. Call one of the Formats by Sume, or your own.
format_api_trigger_disabled409The API trigger is off for that Format.
format_inactive409The Format is inactive.
format_run_in_progress409Only with on_active_run: "reject". Retry later or surface "already running".
idempotency_conflict409Same key, different body. Your key derivation is unstable — fix that before retrying.
invalid_request400Includes a webhook_url that is not a public HTTPS URL.

Treat 4xx here as a bug in your call, not a transient. Retrying an insufficient_scope forever is a common and expensive mistake.

At run — a run existed, and did not produce a result

status is failed, and the receipt carries error plus output_error. Notably:

error.codeMeaning
unattended_blockedThe run hit a gate it could not satisfy without a human — no matching avatar, a missing input it would have asked about in chat. The message is written to be shown.
format_run_failedThe generic failure. Read error.message. A run that wanted to spend past its cap lands here too, so a plan tier whose runs keep failing is the first thing to check against usage.generation_spend_cap_usd_micros.

Retry a run failure with a new idempotency key — the old key is bound to the run that already failed, and reusing it returns that same failed receipt.

API runs are unattended. A Format written for interactive chat may pause to ask a human for approval; over the API those approvals are pre-granted and the run carries on within its spend cap. So completed is a real result — you will never be handed a half-finished run labelled done.

Terminal, but not a failure

statusHandle it as
canceledSomeone called POST /v1/format-runs/{id}/cancel. 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 off the response you got back rather than waiting for a POST. Format default is allow (concurrent runs); Action default is skip — do not copy Action examples into Format calls. See Calling a Format.

At delivery — the run is fine, your endpoint was not

A delivery outcome never changes the run. Ten refused attempts leave you with a failed delivery and a run that is still completed. Fetch it from result_url.

The one delivery case that needs code: a receipt over 1 MiB arrives with payload: null and error.code of payload_too_large, carrying the result_url to fetch instead. A handler that assumes payload is an object will throw on your largest, most valuable runs.

Checklist before you ship

  • SUME_API_KEY is server-only and absent from every client bundle.
  • Your run endpoint authorizes your own customer before it calls Sume.
  • Idempotency-Key is derived from stable identifiers, not generated 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 under a second.
  • Deliveries are deduped on request_id.
  • payload: null (oversized receipt) falls back to result_url.
  • SUME_COM_WEBHOOK_SIGNING_SECRET is set from the value on /dashboard/webhooks, and its fingerprint matches x-sume-webhook-secret-fingerprint on a delivery.
  • Poll on status_url remains wired as a backup (webhooks are live on api.sume.com).
  • Every error code above maps to a message your support team can act on.

Next