INTEGRATIONS

Reactions for your WhatsApp agent.

Let your agent react to messages or stay quiet. Emote chooses the emoji; your WhatsApp Business account sends it as a reaction to the original message.

Prerequisites

An existing WhatsApp Cloud API integration with a working, signature-verified webhook, a business phone number ID, and an access token permitted to send messages. This example uses Meta’s Cloud API directly; if you use another provider, keep the Emote call and use that provider’s reaction API.

Set EMOTE_API_KEY, WHATSAPP_ACCESS_TOKEN, and WHATSAPP_GRAPH_VERSION (for example, v26.0, if supported by your Meta app). Keep both keys server-side.

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

Pass the original incoming message ID and sender phone number from your verified webhook. The helper skips sending anything when Emote chooses "none".

You can choose your own emoji palette. The example mixes built-in emoji strings with a custom 🙏 option. For custom options, use the actual emoji as the ID so the returned string can go directly into WhatsApp’s emoji field. Pass reactions to override this sample palette.

Set frequency to reserved, balanced, or expressive. Emote can return "none" with any setting.

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;
}

type WhatsAppInput = Omit<ReactionInput, "eventId" | "message"> & {
  phoneNumberId: string; messageId: string; from: string; text: string;
};

// A sample palette. WhatsApp accepts emoji, not application-specific labels.
export const WHATSAPP_REACTIONS = [
  "👍", "❤️", "😂", "😮", "😢",
  { id: "🙏", description: "Gratitude, thanks, or a hopeful wish, when appropriate in context." },
];

// Pass a verified inbound text message from your WhatsApp webhook worker.
export async function reactOnWhatsApp({
  phoneNumberId, messageId, from, text, agent, context = [], reactions = WHATSAPP_REACTIONS, frequency = "balanced",
}: WhatsAppInput) {
  const token = process.env.WHATSAPP_ACCESS_TOKEN;
  const version = process.env.WHATSAPP_GRAPH_VERSION;
  if (!token || !version) {
    throw new Error("Set WHATSAPP_ACCESS_TOKEN and WHATSAPP_GRAPH_VERSION.");
  }

  const reaction = await chooseReaction({
    eventId: `whatsapp:${phoneNumberId}:${messageId}`,
    message: text, agent, context, reactions, frequency,
  });
  if (reaction === "none") return;

  const response = await fetch(
    `https://graph.facebook.com/${version}/${encodeURIComponent(phoneNumberId)}/messages`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        messaging_product: "whatsapp",
        recipient_type: "individual",
        to: from,
        type: "reaction",
        reaction: { message_id: messageId, emoji: reaction },
      }),
      signal: AbortSignal.timeout(5000),
    },
  );
  if (!response.ok) throw new Error(`WhatsApp: HTTP ${response.status}`);
  return response.json(); // Accepted by Meta; observe status webhooks for errors.
}

WhatsApp expects one supported emoji per reaction, not a label like love. If you prefer named IDs, map them to an emoji before calling Meta. Custom options let Emote choose additional reactions; they do not change what WhatsApp or your messaging provider accepts. See custom reaction options for limits and response format.

Message handler

Use the function above in your existing bot. The inbound message, conversation context, and reply function below come from your application; the snippet belongs inside your async worker. Normalize and validate inbound text messages before this point; skip status notifications, reaction events, and unsupported media.

// Inside your existing verified, deduplicated webhook worker.
// inbound is your normalized text-message event.
const reactionTask = reactOnWhatsApp({
  phoneNumberId: inbound.phoneNumberId,
  messageId: inbound.messageId,
  from: inbound.sender,
  text: inbound.text,
  agent: agentPersonality,
  context: recentMessages,
}).catch(() => console.error("Reaction failed; reply continues."));

// Your existing reply function runs independently.
const replyTask = Promise.resolve().then(() => replyToWhatsApp(inbound));
const [, reply] = await Promise.allSettled([reactionTask, replyTask]);
if (reply.status === "rejected") throw reply.reason;

Acknowledge the webhook promptly using your existing queue or worker setup. Deduplicate using the business phone number ID and message ID. Store the original Emote input for retries, and track reaction delivery separately so webhook retries don’t send duplicate reactions.

Delivery rules

According to Meta’s reaction message documentation, the target must be a message received from the user. It cannot be older than 30 days, deleted, absent from the conversation, or itself a reaction message. Invalid targets can produce webhook error 131009.

A successful HTTP response means Meta accepted the request. Monitor status webhooks for delivery errors. Reactions receive a sent status, rather than delivered and read statuses. Process reactions while the conversation is active; discard stale queued work.

Verification

Send a text to your test business number and confirm any chosen emoji is attached to that exact message. Then verify "none" sends nothing, a repeated webhook does not resend the reaction, and an Emote error does not prevent your usual reply. Use Meta’s test number before connecting production conversations.

API reference · Get an API key