Bots & MessagingIntermediate
12 min read

Build a WhatsApp Train Running Status & PNR Bot in Node.js

Step-by-step guide to building a production-ready WhatsApp bot for live Indian Railways GPS tracking, PNR verification, and seat availability using Baileys and the irctc-api SDK.

IRCTC Developer Relations
IRCTC Developer Relations
Platform Engineering
Updated 2026-10-25
Prerequisites
  • •Node.js 18+ LTS installed on your machine or VPS
  • •An active WhatsApp account on your mobile phone
  • •Free IRCTC API Key from https://irctc.dev/dashboard
Architecture Flow

User sends a WhatsApp message (!live 12558 or !pnr 2265248473) ➔ Baileys socket receives text ➔ Command router dispatches query to https://api.irctc.dev with caching ➔ Bot sends back a richly formatted WhatsApp message with delays and berth status.

Implementation Steps

(6 Steps)
1

Initialize Node.js Project & Install Dependencies

Create a new project directory and install @whiskeysockets/baileys for WhatsApp Web socket connectivity, qrcode-terminal for initial login scanning, irctc-api for railway data, and dotenv for environment variable security.

bash
mkdir irctc-whatsapp-bot && cd irctc-whatsapp-bot
npm init -y
npm install @whiskeysockets/baileys qrcode-terminal irctc-api dotenv
npm install -D typescript @types/node ts-node
npx tsc --init
Tip:Baileys connects directly via WhatsApp Web WebSocket protocol with zero recurring Twilio or Meta WhatsApp Business API fees.
2

Configure Environment Variables (.env)

Create a .env file in the root directory to store your secret IRCTC Developer API key securely. Never commit this file to version control.

bash
IRCTC_API_KEY=irctc_live_9a8f2c7e01d4
BOT_PREFIX=!
NODE_ENV=production
Tip:Get your free API key with 10,000 initial credits at https://irctc.dev/dashboard.
3

Initialize the IRCTC API Client & WhatsApp Socket

Create index.ts and initialize the Baileys socket with persistent multi-file authentication credentials so you only need to scan the QR code once.

typescript
import makeWASocket, {
DisconnectReason,
useMultiFileAuthState
} from '@whiskeysockets/baileys';
import { Boom } from '@hapi/boom';
import qrcode from 'qrcode-terminal';
import { IrctcApi } from 'irctc-api';
import dotenv from 'dotenv';
dotenv.config();
// 1. Initialize IRCTC Developer Client
const irctc = new IrctcApi({
apiKey: process.env.IRCTC_API_KEY || ''
});
// 2. Start WhatsApp Socket Connection
async function startBot() {
const { state, saveCreds } = await useMultiFileAuthState('auth_info_baileys');
const sock = makeWASocket({
auth: state,
printQRInTerminal: false
});
sock.ev.on('connection.update', (update) => {
const { connection, lastDisconnect, qr } = update;
if (qr) {
console.log('⚡ Scan QR code with WhatsApp on your phone:');
qrcode.generate(qr, { small: true });
}
if (connection === 'close') {
const shouldReconnect =
(lastDisconnect?.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut;
console.log('Connection closed. Reconnecting:', shouldReconnect);
if (shouldReconnect) startBot();
} else if (connection === 'open') {
console.log('✅ IRCTC WhatsApp Bot is online and listening!');
}
});
sock.ev.on('creds.update', saveCreds);
return sock;
}
startBot();
Tip:auth_info_baileys directory saves login tokens. Keep it secure and add it to your .gitignore.
4

Handle Incoming Messages & Command Parsing

Listen to incoming messages via sock.ev.on("messages.upsert") and parse commands: !live <trainNo>, !pnr <pnrNumber>, and !help.

typescript
// Inside startBot() after socket setup:
sock.ev.on('messages.upsert', async ({ messages, type }) => {
if (type !== 'notify') return;
const msg = messages[0];
if (!msg.message || msg.key.fromMe) return;
const sender = msg.key.remoteJid!;
const text =
msg.message.conversation ||
msg.message.extendedTextMessage?.text ||
'';
const trimmed = text.trim();
// 1. Help Menu Command
if (trimmed === '!help' || trimmed === '!start') {
const helpText = `🚆 *IRCTC Railway Assistant*
Available Commands:
• *!live <TrainNo>* — Live running status & delay
• *!pnr <10-Digit PNR>* — Passenger status & berths
• *!fare <TrainNo> <From> <To>* — Ticket fare breakup
*Example:* !live 12558`;
await sock.sendMessage(sender, { text: helpText });
return;
}
// 2. Live Train Status Command
if (trimmed.startsWith('!live ')) {
const trainNo = trimmed.replace('!live ', '').trim();
if (!/^\d{4,5}$/.test(trainNo)) {
await sock.sendMessage(sender, { text: '⚠️ Please provide a valid 4 or 5 digit train number.' });
return;
}
try {
await sock.sendMessage(sender, { text: `🔍 Fetching live running status for train ${trainNo}...` });
const status = await irctc.live.getRunningStatus(trainNo);
const reply = `🚆 *Train ${status.trainNumber} — ${status.trainName}*
📍 *Current Station:* ${status.currentStationName} (${status.currentStationCode})
⏱️ *Status:* ${status.delayMinutes > 0 ? `Delayed by ${status.delayMinutes} mins` : 'Running On Time 🟢'}
🎯 *Next Station:* ${status.nextStationName}
🏁 *Updated:* ${status.updatedAt || 'Just now'}`;
await sock.sendMessage(sender, { text: reply });
} catch (err: any) {
await sock.sendMessage(sender, { text: `❌ Error: ${err.message || 'Could not fetch status'}` });
}
}
// 3. PNR Status Command
if (trimmed.startsWith('!pnr ')) {
const pnr = trimmed.replace('!pnr ', '').trim();
if (!/^\d{10}$/.test(pnr)) {
await sock.sendMessage(sender, { text: '⚠️ Please provide a valid 10-digit PNR number.' });
return;
}
try {
await sock.sendMessage(sender, { text: `🔍 Verifying PNR ${pnr} with Indian Railways PRS...` });
const pnrData = await irctc.pnr.check(pnr);
let passengersList = pnrData.passengers
.map((p: any, i: number) => ` • Passenger ${i + 1}: *${p.currentStatus}* (Coach ${p.coach || 'N.A'}, Berth ${p.berthNumber || 'N.A'} ${p.berth ? `[${p.berth}]` : ''})`)
.join('\n');
const pnrReply = `📋 *PNR Status: ${pnrData.pnr}*
🚆 *Train:* ${pnrData.trainNumber} - ${pnrData.trainName}
🗓️ *Date of Journey:* ${pnrData.doj}
🛣️ *Route:* ${pnrData.from} ➔ ${pnrData.to}
📊 *Charting:* ${pnrData.chartStatus}
👥 *Passenger Manifest:*
${passengersList}`;
await sock.sendMessage(sender, { text: pnrReply });
} catch (err: any) {
await sock.sendMessage(sender, { text: `❌ Error: ${err.message || 'Failed to check PNR'}` });
}
}
});
Tip:Notice how we extract the normalized berth field (e.g. LOWER BERTH) so passengers know their exact seat location immediately.
5

Add In-Memory Request Caching to Protect Quotas

When hundreds of users query the same popular train (e.g. 12558 Sapt Kranti), caching the result for 60 seconds saves API credits and cuts response time to under 5ms.

typescript
// Simple in-memory LRU cache
const liveCache = new Map<string, { data: any; expiry: number }>();
async function getCachedLiveStatus(trainNo: string) {
const now = Date.now();
const cached = liveCache.get(trainNo);
if (cached && cached.expiry > now) {
return cached.data;
}
const live = await irctc.live.getRunningStatus(trainNo);
liveCache.set(trainNo, { data: live, expiry: now + 60_000 }); // 60s TTL
return live;
}
Tip:In production with multiple instances, you can swap this in-memory Map with Redis for distributed multi-process caching.
6

Run 24/7 in Background with PM2 on VPS

Compile TypeScript to JavaScript and configure PM2 process manager to ensure your bot auto-restarts on server reboots or unexpected crashes.

bash
npm run build # or npx tsc
npm install -g pm2
pm2 start dist/index.js --name "irctc-whatsapp-bot"
pm2 save
pm2 startup
Tip:Use pm2 logs irctc-whatsapp-bot to monitor live conversations, queries, and connection events in real time.
Production Endpoint

Live Train Running Status API

Test live queries and inspect sub-100ms response payloads directly in the browser sandbox.

Frequently Asked Questions

Related Tutorials

View All Guides →