Slack bot to generate video: a slash command with Sume's API

Ack Slack's slash command within 3000 ms, submit POST /v1/videos with a callback_url, then post the URL to response_url when Sume's webhook lands.

5 min readSume
All posts

To build a Slack bot that generates video, answer the slash command with HTTP 200 inside Slack's 3000 ms, then submit POST /v1/videos with a callback_url on your server. When Sume's signed job webhook arrives, post the media.sume.com video URL to the command's response_url, or with chat.postMessage once 30 minutes have passed.

Sume has no Slack app; this is your own Slack app's server calling Sume over HTTPS. The Sume facts come from the Video Generation and Webhooks docs, and the Slack facts from docs.slack.dev, all read on 2026-09-27. Sume's signing scheme is covered in Signed webhooks for Sume video runs.

Why must the bot answer Slack before it calls Sume?

Slack sends a slash command as an HTTP POST with application/x-www-form-urlencoded data, and your app must answer with HTTP 200 within 3000 milliseconds or the user sees an operation_timeout error. Anything the bot does before answering spends that budget, and the video itself takes far longer: Sume's docs say video generation typically takes 30 seconds to several minutes. So acknowledge first, submit next, and post the video when it is ready.

  • Verify the request before anything else. Slack signs v0:{timestamp}:{body} with your app's signing secret and sends it as X-Slack-Signature; reject any X-Slack-Request-Timestamp more than five minutes from local time.
  • The 200 body can carry a message such as “Rendering your video…”. ephemeral is the default response_type, so only the person who ran the command sees it.
  • If the submit fails later, report it as a message, not an HTTP 500. Slack says the status code only tells it whether the payload was received.

How does the bot start the video job?

After the acknowledgment, send the command's text as the prompt. Store the command's response_url and channel_id with the returned job id: Sume's webhook names the job, not the Slack conversation, so your store links the two.

  • An Idempotency-Key makes a retried submit safe, because a replay returns the original job. Slack's response_url is unique to each payload, so a hash of it is a stable key.
  • callback_url must be a public HTTPS URL. Localhost, private-network, and non-HTTPS URLs are rejected.
  • Each command starts a job billed to your workspace balance, and Sume's docs say to validate user input and enforce your own authorization before forwarding requests. Allow-list the user_id or channel_id values you accept.
import crypto from "node:crypto";

app.post("/slack/video", express.raw({ type: "application/x-www-form-urlencoded" }), async (req, res) => {
  if (!verifySlack(req)) return res.status(401).end(); // v0 signature, 5-minute window
  const cmd = Object.fromEntries(new URLSearchParams(req.body.toString("utf8")));
  res.json({ response_type: "ephemeral", text: "Rendering your video…" }); // inside 3000 ms
  const r = await fetch("https://api.sume.com/v1/videos", {
    method: "POST",
    headers: {
      Authorization: "Bearer " + process.env.SUME_API_KEY,
      "Content-Type": "application/json",
      "Idempotency-Key": "slack-" + crypto.createHash("sha256").update(cmd.response_url).digest("hex"),
    },
    body: JSON.stringify({
      model: "sume/auto",
      prompt: cmd.text,
      callback_url: "https://bot.example.com/hooks/sume",
    }),
  });
  const job = await r.json();
  if (!r.ok) return reply(cmd.response_url, "Sume refused the job: " + job.error.code);
  await saveJob(job.id, { responseUrl: cmd.response_url, channelId: cmd.channel_id, at: Date.now() });
});

How do I post the video when Sume's webhook arrives?

Sume POSTs one job webhook to callback_url when the job reaches a terminal state, and makes up to 10 attempts with a 10-second timeout each. Verify it on the raw body, store it, answer 2xx right away, and treat job_id as your idempotency key so a repeat delivery posts nothing twice.

  • Check x-sume-webhook-signature with verifyWebhook from @sume-com/sdk, which accepts any sume-v1= entry during a secret rotation. A hand-rolled check must split the header on commas.
  • On job.completed, take the payload.artifacts[] entry whose type is video. Its url is a public artifact under media.sume.com, so anyone in the channel can open it.
  • Within 30 minutes of the command, POST { "response_type": "in_channel", "text": url } to the stored response_url. Slack accepts up to 5 responses there in that window.
  • After 30 minutes, publish with `chat.postMessage`, a bot token with chat:write, and the stored channel_id. Slack generally allows 1 message per second per channel, and a new app needs chat:write.public to post in all public channels.
  • On job.failed (status: "ERROR"), post a short failure note the same way.

How do Slack's and Sume's signatures differ?

The bot verifies two inbound requests with two different secrets. Keep the verifiers separate.

From Slack's slash command and request verification pages and Sume's Webhooks page, read 2026-09-27.
PropertySlack slash commandSume job webhook
Signature headerX-Slack-Signature: v0=<hex>x-sume-webhook-signature: sume-v1=<hex>
Timestamp headerX-Slack-Request-Timestampx-sume-webhook-timestamp
Signed stringv0:{timestamp}:{body}{timestamp}.{raw_body}
SecretApp signing secretWorkspace webhook signing secret
Replay window5 minutes5 minutes, the suggested default
Answer within3000 ms10 s per attempt

What are the limits?

A few rules shape the bot beyond the two deadlines.

  • Slash commands created by developers cannot be invoked in message threads.
  • Sume sends terminal job events only, with no progress deliveries. For a “still rendering” update, poll GET /v1/jobs/{id}/status yourself.
  • A delivery that never lands does not change the job. Read the finished job from GET /v1/jobs/{id}/result and post from there.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume