Trigger.dev public tokens: keep the Sume key server-side
Trigger.dev v4.6.2 hardened authorization for public tokens. Whatever token your browser holds, a Sume API key must never be one of them. Here is the split.

Keep the Sume API key on your server and give the browser only your own, narrowly scoped credentials. The Trigger.dev changelog lists v4.6.2 (Sep 16, 2026) with "Authorization hardening and cleaner chat agent trace spans", covering realtime payloads, public tokens and chat sessions. That is a reminder of the pattern, not a Sume feature: a public token is meant for the client, a Sume key is not.
What the changelog says
The v4.6.2 entry names three surfaces: realtime payloads, public tokens and chat sessions. The changelog line is all this post relies on; check the release notes for what exactly changed before you upgrade.
What Sume has instead of a public token
Sume has no per-end-user credential and no browser-safe key. The embed recipe puts it plainly: one Sume account, one server-side key, many of your customers. A Sume key spends your credits, so anyone holding it can run any Format you own up to your caps.
| Credential | Lives in | Safe in a browser? |
|---|---|---|
| Trigger.dev public token | Client, issued per use | Designed for it |
| Sume API key | Your server environment | No |
| Your own session cookie | Browser | Yes, it is yours |
Rules from the Sume docs
The Format embedding recipe lists the custody rules. They apply whether your background jobs run on Trigger.dev or anywhere else.
- Keep the key in server environment, never in client JavaScript, a mobile bundle or a
NEXT_PUBLIC_*variable. - Proxy the call, not the key. A pass-through endpoint that forwards the browser payload with your key attached is the same leak one hop later.
- Run your own authorization check. Sume authenticates you, not your customer.
- Rotate by creating a new key and retiring the old one, since scopes cannot be added to an existing key.
A server-side shape that works
Your endpoint accepts your customer's identifiers, builds the Sume request itself, and sets a spend cap and an idempotency key. The TypeScript SDK path from the recipe looks like this.
import { createSumeClient, subscribeFormatRun } from "@sume-com/sdk";
const client = createSumeClient({ apiKey: process.env.SUME_API_KEY! });
const run = await subscribeFormatRun({
client,
path: { handle: "acme", slug: "product-promo" },
idempotencyKey: `order-${order.id}`,
body: {
input: { product_url: order.productUrl },
generation_spend_cap_usd: 3,
},
});Where a task runner fits
If a Trigger.dev task calls Sume, the key is a secret in the task environment, like any other server code. Whatever the task reports back to the browser through a public token should be your own record ids and status, not Sume credentials or signed URLs. Prefer a webhook to polling for results in production, as the recipe advises.
Sources
Related posts
More in Developers
- Estimate a Sume TTS bill in Python before you submit: count characters
Sume TTS is $0.0475 per 1,000 characters, spaces and punctuation included. A small Python function prices a script and flags ones near the limits.
- TTS 400: Provide exactly one of transcript_source or transcript
This 400 means the TTS body had both transcript and transcript_source, or neither. Send exactly one, plus a voice, and re-run.
- TTS 400 asking for voice.id or avatar_id / avatar_handle: the fix
The TTS call needs a voice: set voice.id, or a top-level avatar_id or avatar_handle. Without one the request is rejected before any audio is made.
- List Sume TTS Router models before you hardcode a Sonic id
GET /v1/tts-router/models returns the catalog of pass-through TTS models. Read it at startup instead of pinning an id that may change.
Written by Sume