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.

4 min readSume
All posts

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.

QStash behavior against Sume response classes (read 2026-10-03)
Sume responseQStash treats it asResult with a forwarded key
202 acceptedDelivered (2xx)One job
429 queue_full or rate_limitedFailed delivery; backs off, honors Retry-AfterRetry adopts the same key once capacity opens
503 provider_capacity_exceededFailed delivery, retriedSame key, safe
400 invalid_request or 402 insufficient_creditsFailed delivery, retried up to the default 3Three wasted calls; no job is created
Lost response after Sume acceptedFailed delivery, retriedReplay 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

All Integrations posts

Written by Sume