Back to Blog
API & Developer Guides

How to Build a Slack Bot That Finds Business Emails

Published February 7, 2026

Why a Slack Bot for Lead Generation?

Your sales team lives in Slack. Instead of making them switch to a separate tool every time they need a business email, you can bring the Easy Email Finder API directly into Slack. In this tutorial, we build a Slack bot that responds to slash commands and finds business emails on demand.

What We Are Building

The bot supports two slash commands:

  • /findleads plumbers in Austin, TX - Searches for businesses and returns enriched results with emails
  • /checkcredits - Shows the remaining credit balance

Prerequisites

  • Node.js 18 or later
  • An Easy Email Finder API key from the Developer Dashboard
  • A Slack workspace where you can install custom apps

Step 1: Create a Slack App

Go to api.slack.com/apps and click "Create New App." Choose "From scratch," name it "Email Finder," and select your workspace. Under "Slash Commands," create two commands: /findleads and /checkcredits. Under "OAuth & Permissions," add the commands and chat:write scopes. Install the app to your workspace and note the Bot Token and Signing Secret.

Step 2: Set Up the Project

mkdir slack-email-finder && cd slack-email-finder
npm init -y
npm install @slack/bolt dotenv

Create your .env file:

SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_SIGNING_SECRET=your-signing-secret
EEF_API_KEY=eef_live_your_api_key_here
PORT=3000

Step 3: Build the Bot

import 'dotenv/config';
import { App } from '@slack/bolt';

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
});

const EEF_KEY = process.env.EEF_API_KEY;
const EEF_BASE = "https://easyemailfinder.com/api/v1";

function parseQuery(text: string): { query: string; location: string } | null {
  // Parse "plumbers in Austin, TX" format
  const match = text.match(/^(.+?)\s+in\s+(.+)$/i);
  if (!match) return null;
  return { query: match[1].trim(), location: match[2].trim() };
}

// /findleads command handler
app.command('/findleads', async ({ command, ack, respond }) => {
  await ack();

  const parsed = parseQuery(command.text);
  if (!parsed) {
    await respond({
      text: 'Usage: /findleads [business type] in [city, state]\nExample: /findleads plumbers in Austin, TX'
    });
    return;
  }

  await respond({ text: `Searching for ${parsed.query} in ${parsed.location}...` });

  try {
    const resp = await fetch(`${EEF_BASE}/search-and-enrich`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${EEF_KEY}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        query: parsed.query,
        location: parsed.location,
        mode: "local"
      })
    });

    const data = await resp.json();
    const leads = data.results?.filter((r: any) => r.email) || [];

    if (leads.length === 0) {
      await respond({ text: 'No emails found for that search. Try a different query.' });
      return;
    }

    // Format results as a Slack message
    const blocks: any[] = [
      {
        type: "header",
        text: {
          type: "plain_text",
          text: `Found ${leads.length} emails for "${parsed.query}" in ${parsed.location}`
        }
      }
    ];

    for (const lead of leads.slice(0, 10)) {
      blocks.push({
        type: "section",
        text: {
          type: "mrkdwn",
          text: `*${lead.name || 'Unknown Business'}*\n`
            + `Email: ${lead.email}\n`
            + `Phone: ${lead.phone || 'N/A'}\n`
            + `Website: ${lead.website || 'N/A'}`
        }
      });
      blocks.push({ type: "divider" });
    }

    if (leads.length > 10) {
      blocks.push({
        type: "context",
        elements: [{
          type: "mrkdwn",
          text: `Showing 10 of ${leads.length} results. Credits used: ${data.creditsUsed}`
        }]
      });
    }

    await respond({ blocks });
  } catch (err) {
    console.error(err);
    await respond({ text: 'An error occurred while searching. Please try again.' });
  }
});

// /checkcredits command handler
app.command('/checkcredits', async ({ command, ack, respond }) => {
  await ack();

  try {
    const resp = await fetch(`${EEF_BASE}/balance`, {
      headers: { "Authorization": `Bearer ${EEF_KEY}` }
    });
    const data = await resp.json();
    const dollars = (data.credits * 0.25).toFixed(2);
    await respond({
      text: `You have *${data.credits} credits* remaining (\$${dollars} worth of emails).`
    });
  } catch (err) {
    await respond({ text: 'Could not fetch credit balance.' });
  }
});

(async () => {
  await app.start(Number(process.env.PORT) || 3000);
  console.log('Slack bot is running');
})();

Step 4: Deploy and Test

Run the bot locally with npx tsx app.ts and use ngrok to expose it to Slack during development. For production, deploy to any Node.js hosting platform and update your Slack app's request URL.

Usage in Slack

/findleads dentists in Portland, OR
/findleads web design agencies in Chicago, IL
/checkcredits

The bot will return formatted results with business names, emails, phone numbers, and websites directly in the Slack channel.

Team Access Control

Since the bot uses a shared API key, all usage draws from the same credit pool. Consider adding a daily credit limit per user or channel to prevent runaway costs. You can track per-user usage by logging command invocations with the user ID from command.user_id.

Next Steps

You can extend this bot with interactive buttons to save leads to a CRM, add reaction-based workflows to mark high-quality leads, or integrate with a spreadsheet. For more API integration patterns, check out our CRM integration guide or learn about Zapier automation. Full API docs are at easyemailfinder.com/developer/docs.

Ready to find business emails?

Try Easy Email Finder free — get 5 credits to start.

Start Finding Emails

Related Posts