QStash to Sume: forward headers, 3 default retries, pinned key
QStash can publish straight to api.sume.com with Upstash-Forward headers and retries 3 times. Pin an Idempotency-Key; the API key sits in the stored message.

Yes, you can have QStash call the Sume API directly: publish to https://qstash.upstash.io/v2/publish/https://api.sume.com/v1/images, forward the Authorization and Idempotency-Key headers with the Upstash-Forward- prefix, and QStash will retry failures for you. Set the Idempotency-Key, because QStash retries any non-2xx response three times by default and each retry would otherwise be a new paid request.
The trade is that the Sume API key travels inside a message QStash stores durably. If that is not acceptable, publish to a route on your own domain that holds the key, and keep the same retry behavior.
What QStash does with the call
QStash's publish page says a message is durably stored in an Upstash Redis database and then delivered to the destination, with headers forwarded when prefixed Upstash-Forward-. The retry page sets the default at three retries per failed delivery, a backoff of min(86400, e ** (2.5*n)) seconds, and says it honors Retry-After and the X-RateLimit-Reset family of headers, capped at one day.
That lines up with Sume's own rules for transient failures. Per the admission guide, queue_full and rate_limited are 429 responses you retry with the same key, retry-after when present, and a 503 provider_capacity_exceeded is retried later. The mismatch is on permanent errors: a 400 or 402 from Sume is still a non-2xx, so QStash retries it. The documented way to stop that, status 489 with Upstash-NonRetryable-Error: true, has to come from a receiver you control; Sume will not send it.
| Sume response | QStash treats it as | Result with a forwarded key |
|---|---|---|
| 202 accepted | Delivered (2xx) | One job |
| 429 queue_full or rate_limited | Failed delivery; backs off, honors Retry-After | Retry adopts the same key once capacity opens |
| 503 provider_capacity_exceeded | Failed delivery, retried | Same key, safe |
| 400 invalid_request or 402 insufficient_credits | Failed delivery, retried up to the default 3 | Three wasted calls; no job is created |
| Lost response after Sume accepted | Failed delivery, retried | Replay returns the original job, no second charge |
The publish call
Pick the webhook mode so the 202 returns at once and the finished image arrives at your receiver; the receiver verifies the signature as shown in the Webhooks guide. The key below is fixed per intent, here one banner for one date.
const target = "https://api.sume.com/v1/images";
const res = await fetch(`https://qstash.upstash.io/v2/publish/${target}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.QSTASH_TOKEN}`,
"Content-Type": "application/json",
"Upstash-Forward-Authorization": `Bearer ${process.env.SUME_API_KEY}`,
"Upstash-Forward-Content-Type": "application/json",
"Upstash-Forward-Idempotency-Key": "banner-2026-10-03",
"Upstash-Retries": "3",
},
body: JSON.stringify({
model: "sume/auto",
prompt: "Autumn sale banner, warm palette, no text",
mode: "webhook",
webhook_url: "https://example.com/hooks/sume",
}),
});
console.log(res.status, await res.text());When to put your own route in between
Direct publish fits one-off scheduled prompts you wrote yourself. Anything that interpolates user input should go through a route you own, for three reasons: you can validate the prompt before it costs money, the Sume key stays in your environment, and you can answer 489 for a permanent Sume error so QStash stops retrying. The route then submits with Idempotency-Key derived from your own record id, and QStash's job is reduced to what it is good at, durable timed delivery.
Sources
Related posts
More in Integrations
- Vercel Cron can fire twice: key the Sume submit by schedule slot
Vercel cron delivery is best effort and never retried. Derive the Sume Idempotency-Key from the schedule slot so a repeat adopts the first job.
- Vimeo embed captions on by default: texttrack and cc parameters
Add texttrack=en to a Vimeo embed URL to open with captions on. What cc does, when texttrack is ignored, and when a burned-in caption is the safer fix.
- VS Code --add-mcp for a remote server: add Sume with mcp.json
VS Code documents code --add-mcp only with a local command. For a remote server like Sume, put a type http entry in mcp.json and sign in with OAuth.
- VS Code Agent Host skips .vscode/mcp.json inputs: place Sume's entry
VS Code's Agent Host forwards your MCP config but drops entries that need input variables. Where to put Sume's entry and how to pass an API key without them.
Written by Sume