Linear webhook gives you 5 seconds: start a Sume job from a comment
Linear retries a webhook 3 times and may disable it if you keep failing. Verify Linear-Signature, key the Sume submit by comment id, and abort at 3 seconds.

To start a Sume job from Linear, subscribe a webhook to comments, verify the Linear-Signature header over the raw body, and submit with an Idempotency-Key built from the comment id. Linear gives your endpoint 5 seconds and retries a failed delivery at most 3 times, so the Sume call needs its own shorter timeout and a key that makes the retry harmless.
The example treats a comment that starts with /clip as the request, for instance /clip a paper plane over a city at dusk. That uses only fields Linear documents on a Comment event: action, type, data.id, data.body and data.issueId.
The delivery contract
Linear's page lists the signature as the hex-encoded HMAC-SHA256 of the raw body in the Linear-Signature header, and a webhookTimestamp field in milliseconds that it recommends checking against your own clock to within about a minute. A delivery fails if the server is unavailable, takes longer than 5 seconds, or answers anything other than 200. Retries come after 1 minute, 1 hour and 6 hours. A URL that stays unresponsive can be disabled by Linear and must be re-enabled by hand.
That last sentence is the operational risk. A slow Sume call that makes you miss the 5-second window looks like an unresponsive endpoint, so cap the outbound call below it and return a non-200 on timeout so Linear retries a minute later. Because the key is derived from the comment id, the retry returns the original job when the first call did reach Sume.
| Linear rule | Value | Design consequence |
|---|---|---|
| Response deadline | 5 seconds | Abort the Sume call at 3 seconds |
| Retries | Up to 3 (1 minute, 1 hour, 6 hours) | Deterministic key per comment id |
| Signature | Hex HMAC-SHA256 of raw body, Linear-Signature header | Read the body as text before parsing |
| Replay guard | webhookTimestamp in milliseconds, check within about a minute | Reject stale deliveries |
| Disable policy | Unresponsive endpoints may be disabled | Never block on generation |
Handler
The secret is checked for emptiness before it is used, the comparison is constant-time, and generation runs in webhook mode so the 202 comes back quickly. Finished-job handling belongs to a separate receiver that verifies the x-sume-webhook-signature header.
import crypto from "node:crypto";
export async function POST(req: Request) {
const secret = process.env.LINEAR_WEBHOOK_SECRET ?? "";
const raw = await req.text();
const want = crypto.createHmac("sha256", secret).update(raw).digest("hex");
const got = req.headers.get("linear-signature") ?? "";
const same = got.length === want.length &&
crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want));
if (!secret || !same) return new Response("bad signature", { status: 401 });
const evt = JSON.parse(raw);
if (Math.abs(Date.now() - evt.webhookTimestamp) > 60_000) return new Response("stale", { status: 401 });
if (evt.type !== "Comment" || evt.action !== "create") return new Response("ok");
const m = /^\/clip\s+(.+)/s.exec(evt.data.body ?? "");
if (!m) return new Response("ok");
try {
const res = await fetch("https://api.sume.com/v1/images", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.SUME_API_KEY}`, "Content-Type": "application/json",
"Idempotency-Key": `linear-comment-${evt.data.id}` },
body: JSON.stringify({ model: "sume/auto", prompt: m[1], mode: "webhook",
webhook_url: process.env.SUME_HOOK_URL }),
signal: AbortSignal.timeout(3000),
});
return new Response("ok", { status: res.ok ? 200 : 502 });
} catch { return new Response("retry", { status: 504 }); }
}What to do with a 4xx from Sume
A 400 for a bad prompt or a 402 for an empty balance will not improve on retry, and returning 502 for it burns Linear's three retries and risks the disable policy. Return 200 for those and record the failure somewhere you look. Reserve non-200 for the cases the retry can fix: a timeout, a 429 or a 5xx. The code above is intentionally blunt about this; add the split before it carries real traffic.
Sources
Related posts
More in Integrations
- LiteLLM mcp_servers config for Sume: static_headers and one credential
Register Sume's hosted MCP server in LiteLLM with auth_type and static_headers. Send one credential header, and keep write tools off a read-only team key.
- Make MCP scenario tool times out at 25 seconds: Sume job pattern
Make's MCP scenario tool call times out at 25s on OAuth while the run continues. Return a Sume job id and poll it; never resubmit the paid call.
- Make MCP token in URL, header or OAuth: which one for Sume workflows
Make's MCP server has three connection styles with different timeouts. Compare them for a Sume workflow and keep the Sume key out of URLs and prompts.
- Mastodon GIF under 1 megapixel, no sound: send a trimmed MP4 instead
Mastodon limits GIFs to 16 MB and under 1 megapixel (1280x720) and turns them into soundless MP4s. For sound, upload a trimmed video made with Sume instead.
Written by Sume