Discord bot AI video generation: defer, then edit the reply

Defer the Discord interaction within 3 seconds, submit POST /v1/videos with a callback_url, then edit the reply when Sume's job webhook arrives.

5 min readSume
All posts

To build a Discord bot that makes AI video, answer the slash command's interaction with type 5, DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, within 3 seconds, then submit POST /v1/videos with a callback_url. When Sume's signed job webhook arrives, edit the deferred reply with the video URL while the 15-minute interaction token is still valid.

Sume has no Discord bot; this is your own app's HTTP interactions endpoint calling Sume over HTTPS. The Sume facts come from the Video Generation and Webhooks docs, and the Discord facts from docs.discord.com, all read on 2026-09-27. Sume's delivery and signing rules are covered in Signed webhooks for Sume video runs.

What must the interactions endpoint do within 3 seconds?

With an Interactions Endpoint URL set, Discord POSTs each interaction to your server, and you must send an initial response within 3 seconds or the interaction token is invalidated. A video job takes far longer: Sume's docs say video generation typically takes 30 seconds to several minutes.

  • Validate X-Signature-Ed25519 and X-Signature-Timestamp on every request with your app's public key, and answer 401 when validation fails. Discord sends invalid signatures on purpose as routine checks and removes the URL of an app that fails them.
  • Answer a PING (type: 1) with a PONG (type: 1).
  • Answer a command with { "type": 5 }. It acknowledges the interaction, and the user sees a loading state until you edit the response.

How does the bot start the video job?

After the deferred response, send the command option's value as the prompt. Store the interaction's application_id, token, and channel_id with the returned job id, because Sume's webhook names the job, not the interaction.

  • An Idempotency-Key built from the interaction id makes a retried submit return the original job instead of starting a second one.
  • 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. Sume's docs say to validate user input and enforce your own authorization before forwarding requests, so decide which servers and roles may run it.
app.post("/interactions", express.raw({ type: "application/json" }), async (req, res) => {
  if (!verifyDiscord(req)) return res.status(401).end("invalid request signature"); // Ed25519
  const i = JSON.parse(req.body);
  if (i.type === 1) return res.json({ type: 1 }); // PING -> PONG
  res.json({ type: 5 }); // DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, inside 3 seconds
  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": "discord-" + i.id,
    },
    body: JSON.stringify({
      model: "sume/auto",
      prompt: i.data.options[0].value,
      callback_url: "https://bot.example.com/hooks/sume",
    }),
  });
  const job = await r.json();
  if (!r.ok) return editReply(i.application_id, i.token, "Sume refused the job: " + job.error.code);
  await saveJob(job.id, { appId: i.application_id, token: i.token, channelId: i.channel_id, at: Date.now() });
});

How do I edit the reply when the video is ready?

Sume POSTs one job webhook when the job reaches a terminal state and makes up to 10 attempts, each with a 10-second timeout. Verify the raw body, store the event, answer 2xx right away, and treat job_id as your idempotency key.

  • PATCH /webhooks/{application_id}/{interaction_token}/messages/@original edits the deferred response in place. Interaction tokens are valid for 15 minutes.
  • A job that finishes after the token expires needs Discord's Create Message endpoint instead. In a server channel, that call needs the SEND_MESSAGES permission.
  • On job.completed, the payload.artifacts[] entry whose type is video carries the URL. It is a public artifact under media.sume.com, so anyone who can read the channel can open it.
  • verifyWebhook accepts any sume-v1= entry during a secret rotation. A hand-rolled check must split the header on commas.
import { verifyWebhook } from "@sume-com/sdk";

app.post("/hooks/sume", express.raw({ type: "application/json" }), async (req, res) => {
  const ok = await verifyWebhook({
    body: req.body,
    headers: req.headers,
    secret: process.env.SUME_COM_WEBHOOK_SIGNING_SECRET,
  });
  if (!ok) return res.status(401).end();
  const event = JSON.parse(req.body);
  const ctx = await claimJob(event.job_id); // your store; null if this job_id was handled
  res.status(204).end(); // Sume allows 10 s per attempt
  if (!ctx) return;
  const video = event.status === "OK" && event.payload.artifacts.find((a) => a.type === "video");
  const content = video ? video.url : "The video job failed.";
  // Tokens last 15 minutes; leave a margin, then fall back to Create Message.
  if (Date.now() - ctx.at > 14 * 60 * 1000) return createChannelMessage(ctx.channelId, content);
  await fetch("https://discord.com/api/v10/webhooks/" + ctx.appId + "/" + ctx.token + "/messages/@original", {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ content }),
  });
});

How do Discord's and Sume's checks differ?

The bot verifies two inbound requests with two different schemes. Keep them on separate routes.

From Discord's Interactions Overview and Receiving and Responding pages and Sume's Webhooks page, read 2026-09-27.
PropertyDiscord interactionSume job webhook
SchemeEd25519 signatureHMAC-SHA256
HeadersX-Signature-Ed25519, X-Signature-Timestampx-sume-webhook-signature, x-sume-webhook-timestamp
Signed bytesTimestamp followed by the body<timestamp>.<raw_body>
KeyYour app's public keyWorkspace webhook signing secret
Answer within3 seconds10 s per attempt

What are the limits?

Two of them come from the job side rather than from Discord.

  • Sume sends terminal job events only, with no progress deliveries. Between the defer and the edit, Discord's loading state is the only progress the user sees unless you poll GET /v1/jobs/{id}/status.
  • A delivery that never lands does not change the job. Read the finished job from GET /v1/jobs/{id}/result, then edit the reply or post a message.
  • Interaction tokens last 15 minutes, while video generation can take several minutes depending on the model, resolution, and server load. Keep the Create Message fallback.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume