INTEGRATIONS

Reactions for your Discord agent.

Let your Discord agent react with standard or custom emoji. Emote chooses a reaction, and your bot attaches it to the user’s message.

Prerequisites

Start with a Discord bot that already receives messages through the Gateway, using a library such as discord.js or discord.py. Keep EMOTE_API_KEY and DISCORD_BOT_TOKEN on your server. Use a bot account, not a personal user token.

For server messages, subscribe to GUILD_MESSAGES and give the bot access to the channel. Receiving ordinary message text generally requires the privileged MESSAGE_CONTENT intent, enabled in the Developer Portal and in your client; approval may be required. DMs and messages mentioning your bot are among the exceptions. See Discord’s message content rules. Configure DIRECT_MESSAGES if your agent also serves DMs.

In server channels, grant View Channel and Read Message History. Add Reactions is also required when introducing an emoji that is not already on the message. Custom emoji must be usable by the bot in that channel; external server emoji may require Use External Emojis and remain subject to emoji restrictions.

Copy the TypeScript or Python example into your server code. TypeScript uses Node.js 20 or later; Python uses Python 3.10 or later with pip install httpx. Each example includes the Emote request function. Keep API keys in server environment variables.

Reaction options

Each option has an Emote id, a description, and the Discord emoji to send. Emote sees the IDs and descriptions; the helper maps its choice to the delivery value. This lets you mix standard emoji with custom server emoji.

For a custom emoji, add an option such as { id: "celebrate_parrot", description: "Celebrate good news.", emoji: "party_parrot:YOUR_EMOJI_ID" } to your palette. Replace the placeholder with an emoji the bot can use. Discord expects name:id for custom reactions, including animated emoji, rather than display markup like <:name:id> or <a:name:id>. The helper URL-encodes the value for you.

Pass your agent’s system prompt as agent, recent messages as context, and your palette as reactions. Set frequency to reserved, balanced, or expressive. If Emote chooses "none", no reaction is sent.

Integration example

import { createHash } from "node:crypto";

export const REACTIONS = [
  "👍", "❤️", "😂", "🎉", "😮", "😢",
  "🤔", "🔥", "💀", "🙌", "👀", "💯",
];

export type ReactionOption = string | { id: string; description: string };
export type ContextMessage = { role: "user" | "assistant"; content: string; reaction?: string };
export type ReactionInput = {
  eventId: string; message: string; agent?: string;
  context?: ContextMessage[]; reactions?: ReactionOption[];
  frequency?: "reserved" | "balanced" | "expressive";
};

// Server only. eventId must identify this message across all your conversations.
export async function chooseReaction({
  eventId, message, agent, context = [], reactions = REACTIONS, frequency = "balanced",
}: ReactionInput): Promise<string> {
  const key = process.env.EMOTE_API_KEY;
  if (!key) throw new Error("Set EMOTE_API_KEY on your server.");
  if (!eventId) throw new Error("A stable eventId is required.");

  const response = await fetch("https://useemote.com/v1/react", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${key}`,
      "Content-Type": "application/json",
      "Idempotency-Key": createHash("sha256").update(eventId).digest("hex"),
    },
    body: JSON.stringify({ message, agent, context, reactions, frequency }),
    signal: AbortSignal.timeout(3000),
  });
  if (!response.ok) throw new Error(`Emote: HTTP ${response.status}`);
  const reaction = await response.json();
  const allowed = reactions.map((option) => typeof option === "string" ? option : option.id);
  if (typeof reaction !== "string" || (reaction !== "none" && !allowed.includes(reaction))) {
    throw new Error("Unexpected Emote response.");
  }
  return reaction;
}

export type DiscordReaction = { id: string; description: string; emoji: string };
export const DISCORD_REACTIONS: DiscordReaction[] = [
  { id: "approve", description: "Agreement or acknowledgement.", emoji: "👍" },
  { id: "celebrate", description: "Celebrate an achievement or good news.", emoji: "🎉" },
  { id: "laugh", description: "Something funny, when laughter fits the context.", emoji: "😂" },
];

type DiscordInput = Omit<ReactionInput, "eventId" | "message" | "reactions"> & {
  botId: string; channelId: string; messageId: string; text: string;
  reactions?: DiscordReaction[];
};

// Pass a normal user message from your existing Discord bot's message handler.
export async function reactOnDiscord({
  botId, channelId, messageId, text, agent, context = [],
  reactions = DISCORD_REACTIONS, frequency = "balanced",
}: DiscordInput) {
  const token = process.env.DISCORD_BOT_TOKEN;
  if (!token) throw new Error("Set DISCORD_BOT_TOKEN on your server.");
  if (!reactions.length) return;
  const reaction = await chooseReaction({
    eventId: `discord:${botId}:${channelId}:${messageId}`,
    message: text, agent, context, frequency,
    reactions: reactions.map(({ id, description }) => ({ id, description })),
  });
  if (reaction === "none") return;
  const emoji = reactions.find(({ id }) => id === reaction)!.emoji;
  // Unicode emoji or custom name:id, not Discord's <:name:id> display markup.
  const response = await fetch(
    `https://discord.com/api/v10/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/reactions/${encodeURIComponent(emoji)}/@me`,
    {
      method: "PUT",
      headers: { Authorization: `Bot ${token}` },
      signal: AbortSignal.timeout(5000),
    },
  );
  if (!response.ok) throw new Error(`Discord: HTTP ${response.status}`);
  // Discord returns 204 with no JSON body.
}

The helper uses Discord’s Create Reaction endpoint and handles its empty HTTP 204 success response. If your bot already uses an SDK, you can keep the Emote call and use the SDK’s reaction method and rate-limit handling instead.

Message handler

This snippet goes inside your existing async worker and uses the raw MESSAGE_CREATE payload. If your SDK wraps messages, adapt its fields to this shape. botId is your authenticated bot user’s ID; personality, recent messages, and the reply function come from your application. Keep all Discord IDs as strings.

// Inside your existing MESSAGE_CREATE worker, using the raw message payload.
// botId is the authenticated bot user's ID; channel/message IDs stay strings.
if (message.author.bot || message.webhook_id || message.author.id === botId ||
    ![0, 19].includes(message.type) || !message.content?.trim()) return;

const reactionTask = reactOnDiscord({
  botId, channelId: message.channel_id, messageId: message.id,
  text: message.content, agent: agentPersonality, context: recentMessages,
  frequency: "balanced",
}).catch(() => console.error("Reaction failed; reply continues."));

const replyTask = Promise.resolve().then(() => replyToDiscord(message));
const [, reply] = await Promise.allSettled([reactionTask, replyTask]);
if (reply.status === "rejected") throw reply.reason;

The handler accepts normal messages and replies (types 0 and 19) and ignores bots, webhooks, system messages, and empty content. Route only channels and conversations your agent should serve. A reaction targets the incoming message’s channel_id and id, including in threads, not the bot’s new reply.

Delivery rules

Deduplicate messages using the bot, channel, and message IDs. Save the original Emote input and track delivery separately from your main reply. Repeating the same reaction PUT does not add another copy of that bot’s reaction, but choosing a different emoji can add another reaction. "none" leaves existing reactions unchanged.

The helper surfaces errors without retrying. For production, use your Discord SDK’s rate-limit handling or extend delivery to respect HTTP 429 responses and their retry_after value. Use bounded retries for transient failures and discard work for deleted or stale messages. Missing access or unavailable custom emoji should not stop your normal reply.

Verification

In a test server, confirm standard and custom emoji appear on the original message, including a message in a thread. Check "none", duplicate events, bot-message filtering, and a reaction permission failure. If Emote never receives any text, check Message Content intent and channel access before debugging reaction delivery.

API reference · Get an API key