# How to collect feedback for a Discord bot

> A Discord bot collects feedback with a slash command that calls the Escuta Produto REST API from the bot process. Reply with an ephemeral message, store the server id as metadata, and pace requests with a cooldown and a queue, because all of the bot's requests share one IP address.

Source: https://escutaproduto.com/resources/collect-feedback-discord-bot
Last updated: 2026-10-11

## Why a Discord bot posts feedback through the REST API

A Discord bot runs as a server process that receives interactions from Discord, such as a slash command. That process can call any HTTPS API, so the bot sends feedback to the Escuta Produto REST API with `fetch`. The widget script does not run inside Discord, because Discord messages and embeds are not web pages. The public key is the only credential the bot needs, and it can create feedback but never read it.

The bot becomes the feedback channel for its community. Members type a command, the bot saves the report with the server id attached, and the team sees it in the inbox with the rest of the product feedback. The [REST API reference](/docs/api) lists every field, and the response is `201` with the new `id` when the report is saved.

## Register a /feedback command with a message option

Slash commands are registered with Discord before members can use them. The command needs one required string option for the message. Limit its length so a single report stays short:

```js
const { SlashCommandBuilder } = require("discord.js");

const feedbackCommand = new SlashCommandBuilder()
  .setName("feedback")
  .setDescription("Send feedback to the team")
  .addStringOption((option) =>
    option
      .setName("message")
      .setDescription("What should the team know?")
      .setRequired(true)
      .setMaxLength(1000)
  );

module.exports = feedbackCommand;
```

Register the command in your usual way, either for one development server while you test or globally once it works. Guild commands update almost immediately, so use them while you build.

## Defer the reply and send the report

Discord expects a reply within three seconds. A call to the REST API can take longer than that on a slow connection, so the handler defers the reply first, then edits it with the result. Make the reply ephemeral, so only the member who typed the command sees the confirmation.

```js
const { MessageFlags } = require("discord.js");

client.on("interactionCreate", async (interaction) => {
  if (!interaction.isChatInputCommand()) return;
  if (interaction.commandName !== "feedback") return;

  if (!interaction.guildId) {
    await interaction.reply({ content: "Use /feedback inside a server.", flags: MessageFlags.Ephemeral });
    return;
  }

  await interaction.deferReply({ flags: MessageFlags.Ephemeral });

  const result = await sendPaced(() =>
    sendToEscuta({
      message: interaction.options.getString("message"),
      serverId: interaction.guildId,
      channelId: interaction.channelId,
      memberId: interaction.user.id,
    })
  );

  await interaction.editReply(result.ok ? "Thanks, the team has your feedback." : "We could not save that. Please try again later.");
});
```

The helper that talks to Escuta Produto is short. It builds the JSON body, posts it and returns a result object:

```js
async function sendToEscuta({ message, serverId, channelId, memberId }) {
  const response = await fetch("https://escutaproduto.com/api/v1/feedback", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      key: process.env.ESCUTA_PUBLIC_KEY,
      message: message,
      kind: "other",
      metadata: { serverId: serverId, channelId: channelId, memberId: memberId },
    }),
  });

  if (response.status === 201) {
    const data = await response.json();
    return { ok: true, id: data.id };
  }
  return { ok: false, status: response.status };
}
```

Discord ids are snowflakes, which are large numbers. The discord.js library gives them to you as strings, so keep them as strings. Do not convert them to numbers, because the last digits can be lost.

## Store the server id as metadata

The server id, which Discord calls the guild id, tells you which community sent the report. A bot installed in many servers produces reports from many communities, and the server id is the only way to separate them later. The channel id shows where the command ran, which often explains the context of a bug.

Store the member id only if your team needs to reply to the person. Discord ids are personal identifiers, so treat them the way you treat an email address. The whole metadata object must stay under 4 KB. Server and channel ids are short, so there is plenty of room.

## How do you stop spam from one member or one busy server?

The REST API allows 10 requests per minute per IP address and product. Every report from your bot leaves from the same server, so the limit applies to the whole bot, not to each server. A large community that discovers the command on the same day can reach the limit within minutes, and the extra requests return 429.

Use two controls together. A cooldown per member stops one person from flooding the channel. A queue that sends requests at a steady pace keeps the bot under the rate limit. The helper below sends at most one request every six seconds, which matches 10 requests per minute:

```js
const cooldowns = new Map();
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let chain = Promise.resolve();

function sendPaced(task) {
  const result = chain.then(task);
  chain = result.then(() => sleep(6000), () => sleep(6000));
  return result;
}

function onCooldown(memberId) {
  const last = cooldowns.get(memberId) || 0;
  if (Date.now() - last < 60000) return true;
  cooldowns.set(memberId, Date.now());
  return false;
}
```

Check `onCooldown` before you defer the reply, and reply with a short ephemeral message when it returns true. For example, if a member sends a second report 20 seconds after the first, the bot can say to wait a minute. The map lives in memory, so it resets when the process restarts, which is acceptable for a cooldown.

When a 429 still comes back, wait a few seconds and retry once. If it fails again, tell the member that the report was not saved and ask them to try later. Do not retry in a loop, because that keeps the bot over the limit.

## What should the bot do when the API refuses a report?

Each error code has a clear action. Handle them in one place so the replies stay consistent:

| Status | Meaning | What the bot does |
| --- | --- | --- |
| 201 | Saved | Confirm to the member and show nothing else |
| 400 | Invalid body | Log the body size and check that the message has two or more characters |
| 404 | Unknown key | Check the environment variable and the product key in settings |
| 413 | Body over 16 KB | Shorten the message before sending |
| 429 | Rate limit | Wait, retry once, then ask the member to try later |

If the product has allowed origins set, a request from the bot can also return 403. Read the origin rules in the REST API reference before you change that list, because server-side requests do not behave like browser requests.

## Where do Discord reports show up for the team?

Escuta Produto posts alerts to one Discord incoming webhook per product. Each new item shows its type, the product, the rating if there is one, the sender, an excerpt of 500 characters and a dashboard link. Create a dedicated channel for these alerts, so they do not sit next to general chat. The [notifications guide](/docs/notifications) explains how to create the webhook.

The team then triages the items with statuses and internal notes. The [triage guide](/resources/how-to-triage-customer-feedback) describes a routine that fits a small team, and the article on [feedback in developer tools](/resources/collect-feedback-developer-tools) covers how to handle reports that point at errors.

## Doing it with Escuta Produto in ten minutes

Create a product for the bot, then copy its public key into the environment variable the bot reads. Register the command on your test server, send one report and confirm that it appears in the inbox with the server id in its metadata. Then add the cooldown and the queue before you register the command globally. Finally, connect the Discord webhook so the first real report reaches the team.

For a wider view of which questions to ask in a community, read [how to collect customer feedback](/resources/how-to-collect-customer-feedback). It covers the timing and wording that work well for communities that are still small.

## Frequently asked questions

### Can a Discord bot use the feedback widget?

No. The widget loads a script into a web page, and Discord does not run web pages from bots. Send reports through the REST API from the bot process, and let members use the slash command for quick messages.

### How do I stop one member from flooding the feedback channel?

Keep a cooldown per member id in the bot, for example one report per minute. Reply with a short ephemeral message when the cooldown is active, so the member knows to wait and the channel stays quiet.

### Should the server id be stored as a number?

No. Discord ids are large numbers that can lose digits when converted to a JavaScript number. Keep them as strings, which is how discord.js provides them.
