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.

4 min readSume
All posts

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 delivery rules against the Sume submit (read 2026-10-03)
Linear ruleValueDesign consequence
Response deadline5 secondsAbort the Sume call at 3 seconds
RetriesUp to 3 (1 minute, 1 hour, 6 hours)Deterministic key per comment id
SignatureHex HMAC-SHA256 of raw body, Linear-Signature headerRead the body as text before parsing
Replay guardwebhookTimestamp in milliseconds, check within about a minuteReject stale deliveries
Disable policyUnresponsive endpoints may be disabledNever 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

All Integrations posts

Written by Sume