Build a WhatsApp Fact-Checking Bot with Twilio, OpenAI, and Node.js

May 04, 2026
Written by
Charles Oduk
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Build a WhatsApp Fact-Checking Bot with Twilio, OpenAI, and Node.js

A lot of news is spread on WhatsApp. Some of it is timely and useful. Some of it is half true. Some are obviously fake. And lately, some look polished enough that you pause for a second before deciding whether to trust them. This is especially harder for the older generation who don’t understand why anyone would intentionally spread fake news. I see this firsthand with my dad and that got me thinking about building him a bot to help verify some of the messages he receives.

In this tutorial, you'll build exactly that using Twilio, Node.js, and OpenAI. The bot will accept forwarded text, images, voice notes, and short videos. If the forwarded content contains audio, it gets transcribed first. Then the bot uses an LLM to analyze the claim and do a web verification pass before replying.

What we are building

By the end of this tutorial, you'll have a WhatsApp bot that can:

  • receive forwarded WhatsApp messages through a Twilio webhook
  • inspect plain text messages directly
  • analyze images with an LLM
  • transcribe voice notes and short videos before analysis
  • perform a lightweight web verification step for current or factual claims
  • send a short verdict back to the sender on WhatsApp
  • include a matched source when the bot finds a strong official or highly credible confirmation

Here is the end-to-end flow:

  • A user forwards a message to your WhatsApp number.
  • Twilio sends the incoming message to your webhook.
  • The app checks whether the message is text, image, audio, or video.
  • If the content is audio or video, the app transcribes it first.
  • The app sends the content to OpenAI for analysis.
  • If the claim looks factual or time-sensitive, the model can do a lightweight web search before deciding.
  • The app replies to the user through Twilio with a short credibility assessment.

Prerequisites

To follow along, you'll need:

Set up the project

Start by creating a new Node.js project, open up your preferred shell/terminal and navigate to where you want your project to be. Then, execute the following command to create your src folder where your project will be under and to initiate a Node.js project :

mkdir src
cd src
npm init -y

Then, install the needed dependencies:

npm install express twilio openai dotenv

Update your package.json to have these values:

"main": "src/index.js",
  "type": "module",
  "scripts": {
    "dev": "node --watch src/index.js"
  },

Create a .env file and add the following values:

PORT=3000
OPENAI_API_KEY=your_openai_api_key
OPENAI_MODEL=gpt-5.4-mini
OPENAI_TRANSCRIPTION_MODEL=gpt-4o-mini-transcribe
TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_AUTH_TOKEN=your_twilio_auth_token

For this tutorial, the app is split into three small pieces:

  • src/index.js for the Express server and Twilio webhook
  • src/services/media.js for downloading and classifying media
  • src/services/openai.js for transcription and verification

Build the WhatsApp webhook

Let's start with the Express app that receives inbound WhatsApp messages from Twilio.

Create a file named index.js within the src folder and add the following contents:

import "dotenv/config";
import express from "express";
import twilio from "twilio";
import { downloadTwilioMedia, getMediaKind, toDataUrl } from "./services/media.js";
import { transcribeMedia, verifyImageMessage, verifyTextMessage } from "./services/openai.js";
const app = express();
app.use(express.urlencoded({ extended: false }));
function twimlMessage(message) {
  const response = new twilio.twiml.MessagingResponse();
  response.message(message);
  return response.toString();
}

Twilio sends webhook parameters as form-encoded data, so express.urlencoded() is enough here.

Now add a /whatsapp route that handles incoming text and media. Add the following code to the end of src/index.js, after the twimlMessage function:

app.post("/whatsapp", async (request, response) => {
  const body = request.body.Body?.trim() ?? "";
  const numMedia = Number(request.body.NumMedia ?? 0);
  if (!body && numMedia === 0) {
    response.type("text/xml").send(
      twimlMessage("Please forward a message, image, voice note, or short video for me to review.")
    );
    return;
  }
  if (numMedia === 0) {
    const verdict = await verifyTextMessage({ messageText: body });
    response.type("text/xml").send(twimlMessage(verdict));
    return;
  }
  const mediaUrl = request.body.MediaUrl0;
  const contentType = request.body.MediaContentType0;
  const mediaKind = getMediaKind(contentType);
  // We'll handle media types in the next section.
});
const port = Number(process.env.PORT ?? 3000);
app.listen(port, () => {
    console.log(`Server listening on http://localhost:${port}`);
});

Handle images, voice notes, and videos

Next, create a helper file named src/services/media.js for classifying and downloading media and add the following code:

const SUPPORTED_IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp"]);
const SUPPORTED_AUDIO_TYPES = new Set([
  "audio/mpeg",
  "audio/mp3",
  "audio/mp4",
  "audio/m4a",
  "audio/wav",
  "audio/webm",
  "audio/ogg"
]);
const SUPPORTED_VIDEO_TYPES = new Set([
  "video/mp4",
  "video/quicktime",
  "video/webm",
  "video/ogg"
]);
export function getMediaKind(contentType = "") {
  if (SUPPORTED_IMAGE_TYPES.has(contentType)) return "image";
  if (SUPPORTED_AUDIO_TYPES.has(contentType)) return "audio";
  if (SUPPORTED_VIDEO_TYPES.has(contentType)) return "video";
  return "unsupported";
}

Twilio stores inbound media at a URL that requires authentication. That means you need to download the file with your Account SID and Auth Token before sending it to OpenAI. Add the following code to the end of src/services/media.js:

export async function downloadTwilioMedia(mediaUrl, contentType, credentials) {
  const response = await fetch(mediaUrl, {
    headers: {
      Authorization: `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString("base64")}`
    }
  });
  if (!response.ok) {
    throw new Error(`Failed to download media: ${response.status} ${response.statusText}`);
  }
  const arrayBuffer = await response.arrayBuffer();
  return {
    buffer: Buffer.from(arrayBuffer),
    filename: `incoming.${contentType.split("/")[1] ?? "bin"}`
  };
}

Once the media is downloaded in a Buffer, you base64-encode and wrap it in a data: URL so the vision API can consume the image.

export function toDataUrl(buffer, contentType) {
  return `data:${contentType};base64,${buffer.toString("base64")}`;
}

Back in src/index.js, you can now complete the webhook by adding the media handling code. Replace the comment line // We'll handle media types in the next section with the following code:

const media = await downloadTwilioMedia(mediaUrl, contentType, {
  username: process.env.TWILIO_ACCOUNT_SID,
  password: process.env.TWILIO_AUTH_TOKEN
});
if (mediaKind === "image") {
  const verdict = await verifyImageMessage({
    messageText: body,
    imageDataUrl: toDataUrl(media.buffer, contentType)
  });
  response.type("text/xml").send(twimlMessage(verdict));
  return;
}
const transcript = await transcribeMedia({
  buffer: media.buffer,
  filename: media.filename,
  contentType
});
const verdict = await verifyTextMessage({
  messageText: body,
  transcript
});
response.type("text/xml").send(twimlMessage(verdict));

Images go straight into a multimodal model. Audio and short video clips go through transcription first.

OpenAI's speech-to-text tooling supports common audio and video file types including mp3, mp4, m4a, wav, and webm, which makes it a good fit for WhatsApp voice notes and short forwarded clips.

Add transcription and verification with OpenAI

Now, create the OpenAI service.

Create src/services/openai.js and add the follwoing code to it:

import OpenAI from "openai";
import { toFile } from "openai/uploads";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

You'll use one function for transcription and two for verification: one for text-plus-transcript, and one for images.

Transcribe audio or video

Add the following to src/services/openai.js to generate a transcription from the audio or video provided:

export async function transcribeMedia({ buffer, filename, contentType }) {
  const file = await toFile(buffer, filename, { type: contentType });
  const transcript = await client.audio.transcriptions.create({
    file,
    model: process.env.OPENAI_TRANSCRIPTION_MODEL || "gpt-4o-mini-transcribe"
  });
  return transcript.text;
}

Create a verification prompt

The bot can make mistakes and it is important that it doesn’t sound overly confident. "Unclear" is a perfectly valid answer. Add the following function to src/services/openai.js:

function buildInstructions() {
  return [
    "You verify forwarded WhatsApp content for everyday users.",
    "Be careful, skeptical, and easy to understand.",
    "When the content makes factual, current, or time-sensitive claims, search the web before deciding.",
    "Prefer official, primary, or highly credible sources when available.",
    "If a reliable source confirms the content, say that clearly.",
    "If reliable sources contradict it, say that clearly.",
    "If the search results are weak, missing, or inconclusive, say so instead of guessing.",
    "Use exactly this format:",
    "Verdict: <Likely legitimate | Unclear | Likely misleading>",
    "Reason: <one or two short sentences>",
    "Check next: <one practical verification step>",
    "Confidence: <Low | Medium | High>"
  ].join(" ");
}

Let the model use lightweight web verification

If someone sends a government notice, or breaking news claim, the model shouldn't rely only on what it sees in the screenshot. It should try to find a matching official or reputable source first. Add the following code to src/services/openai.js:

function buildResponseOptions() {
  return {
    model: process.env.OPENAI_MODEL || "gpt-5.4-mini",
    instructions: buildInstructions(),
    tools: [
      {
        type: "web_search",
        search_context_size: "medium"
      }
    ],
    include: ["web_search_call.action.sources"]
  };
}

OpenAI's Responses API supports built-in tool use for web search, which allows you to add a verification step without building your own crawler or search layer. It also gives you a way to pull out a supporting source and include that in the WhatsApp reply when the match looks strong.

Verify text and transcripts

Add the following code to src/services/openai.js to verify messages and transcripts:

export async function verifyTextMessage({ messageText, transcript }) {
  const content = [
    transcript ? `Forwarded media transcript:\n${transcript}` : null,
    messageText ? `Forwarded message text:\n${messageText}` : null,
    "Assess whether this looks legitimate, misleading, or too uncertain to trust."
  ]
    .filter(Boolean)
    .join("\n\n");
  const response = await client.responses.create({
    ...buildResponseOptions(),
    input: content
  });
  return response.output_text.trim();
}

Verify images

Then, add the following to verify images:

export async function verifyImageMessage({ messageText, imageDataUrl }) {
  const response = await client.responses.create({
    ...buildResponseOptions(),
    input: [
      {
        role: "user",
        content: [
          {
            type: "input_text",
            text: [
              "This image was forwarded in WhatsApp.",
              messageText ? `Caption or extra text:\n${messageText}` : "No caption was included.",
              "If the image appears to show an official notice, advisory, flyer, or screenshot, search the web to see whether matching official information exists."
            ].join("\n\n")
          },
          {
            type: "input_image",
            image_url: imageDataUrl
          }
        ]
      }
    ]
  });
  return response.output_text.trim();
}

OpenAI's Responses API supports image inputs, which makes it possible to evaluate screenshots, flyers, and forwarded graphics without adding OCR or an external vision pipeline.

Configure Twilio WhatsApp Sandbox

Now it's time to connect your local app to Twilio.

Start your server:

npm run dev

Expose it publicly:

ngrok http 3000

Copy the HTTPS forwarding URL from ngrok and append /whatsapp, for example:

https://1234abcd.ngrok-free.app/whatsapp

Then open the Twilio Console, go to your WhatsApp Sandbox settings, and paste that URL into the incoming message webhook field.

Screenshot of the Twilio Console WhatsApp Sandbox settings. A URL (https://1234abcd.ngrok-free.app/whatsapp) is visible in the incoming message webhook field.
Screenshot of the Twilio Console WhatsApp Sandbox settings. A URL (https://1234abcd.ngrok-free.app/whatsapp) is visible in the incoming message webhook field.

After that, send the sandbox join code from your phone to the Twilio WhatsApp number. Once your number is connected, you can start forwarding real messages to the bot.

Test the bot

At this point, try forwarding a few different message types:

  • a chain message with a dramatic claim and no source
  • a screenshot of an official looking notice
  • a voice note describing a current event
  • a short video clip with a caption making a factual claim

The bot should respond in a format like this:

Verdict: Unclear
Reason: This message makes a factual claim but does not provide a reliable source, and I could not confidently verify it from the content alone.
Check next: Look for the same claim on an official government website or a reputable news source.
Confidence: Medium

And when the bot finds a good match online, the reply can be a little more useful:

Verdict: Likely legitimate
Reason: This advisory matches information published by an official meteorological source, including the timing and warning details.
Check next: Open the matched source and confirm the advisory number or date.
Confidence: High
Matched source: Kenya Meteorological Department - https://www.meteo.go.ke/...
Screenshot of a successful WhatsApp interaction showing the bot's detailed response.
Screenshot of a successful WhatsApp interaction showing the bot's detailed response.

Wrapping up

In this tutorial, you built a WhatsApp bot that can review forwarded content in multiple formats and return a practical second opinion.

Using Twilio for messaging, Express for the webhook, and OpenAI for transcription, multimodal analysis, and lightweight web verification, you now have a compact project that solves a real everyday problem: helping someone decide whether a forwarded message is worth trusting.

If you wanted to evolve this beyond a tutorial, here are some possible additions:

  • Validate requests before processing webhooks
  • Add asynchronous handling for longer audio and video files
  • Store requests and verdicts for analytics or review
  • Create a database of known fake messages

I look forward to seeing what you build! You can reach me on:

Email: odukjr@gmail.com

Discord: @charlieoduk

Github: charlieoduk