DEV Community

Cover image for Sentiment Analysis: Read Customer Feedback at Scale
APIVerve
APIVerve

Posted on • Originally published at blog.apiverve.com

Sentiment Analysis: Read Customer Feedback at Scale

Last year, a SaaS company I advised ignored their support tickets for too long. Not individual tickets—they responded to those. They ignored patterns in the tickets.

Over three months, 47 customers mentioned the same frustrating bug in their feedback. Different words, different levels of anger, but the same root problem. Nobody connected the dots because nobody was reading all the tickets. The support team closed each one individually. Product never saw the pattern.

By the time they realized, they'd lost 23 of those 47 customers. The bug took two hours to fix.

Reading every piece of customer feedback is impossible at scale. But ignoring it is expensive. Sentiment analysis sits in the middle: it processes everything automatically and surfaces what actually matters.

What Sentiment Analysis Actually Does

At its core, sentiment analysis answers one question: Is this positive, negative, or neutral?

const response = await fetch('https://api.apiverve.com/v1/sentimentanalysis', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': process.env.APIVERVE_KEY
  },
  body: JSON.stringify({
    text: "Your product is fantastic! Best purchase I've made all year."
  })
});

const { data } = await response.json();
// {
//   "sentiment": "positive",
//   "score": 0.92,
//   "magnitude": 0.85
// }
Enter fullscreen mode Exit fullscreen mode

The sentiment is the classification. The score is confidence (0-1). The magnitude indicates intensity—a calm positive vs. an enthusiastic rave.

Compare these two:

  • "The product works as expected." → positive, 0.65 score, 0.3 magnitude
  • "THIS IS AMAZING! Exceeded every expectation!!!" → positive, 0.95 score, 0.9 magnitude

Both positive. Very different levels of enthusiasm.

Building a Feedback Analysis Pipeline

Here's how to process feedback at scale:

async function analyzeFeedback(feedbackItems) {
  const results = await Promise.all(feedbackItems.map(async (item) => {
    const [sentiment, keywords] = await Promise.all([
      // Get sentiment
      fetch('https://api.apiverve.com/v1/sentimentanalysis', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': process.env.APIVERVE_KEY
        },
        body: JSON.stringify({ text: item.text })
      }).then(r => r.json()).then(r => r.data),

      // Extract keywords
      fetch('https://api.apiverve.com/v1/keywordextractor', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': process.env.APIVERVE_KEY
        },
        body: JSON.stringify({ text: item.text })
      }).then(r => r.json()).then(r => r.data)
    ]);

    return {
      id: item.id,
      text: item.text,
      source: item.source,
      timestamp: item.timestamp,
      sentiment: sentiment.sentiment,
      sentimentScore: sentiment.score,
      magnitude: sentiment.magnitude,
      keywords: keywords.keywords || [],
      priority: calculatePriority(sentiment)
    };
  }));

  return results;
}

function calculatePriority(sentiment) {
  // High priority: negative with high confidence and magnitude
  if (sentiment.sentiment === 'negative' && sentiment.score > 0.8 && sentiment.magnitude > 0.7) {
    return 'urgent';
  }
  if (sentiment.sentiment === 'negative') {
    return 'high';
  }
  if (sentiment.sentiment === 'positive' && sentiment.magnitude > 0.8) {
    return 'opportunity'; // Very happy customer - potential testimonial
  }
  return 'normal';
}
Enter fullscreen mode Exit fullscreen mode

Now every piece of feedback has:

  • Sentiment classification
  • Priority level
  • Keywords that tell you what it's about

The urgent ones bubble up. The raves get flagged for marketing to follow up on. The neutral stuff gets logged and checked periodically.

Real-Time Support Ticket Triage

Want to route angry customers to senior support reps? Here's how:

async function triageSupportTicket(ticket) {
  const { data } = await fetch('https://api.apiverve.com/v1/sentimentanalysis', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': process.env.APIVERVE_KEY
    },
    body: JSON.stringify({ text: ticket.description })
  }).then(r => r.json());

  // Determine routing
  let assignTo, priority;

  if (data.sentiment === 'negative' && data.magnitude > 0.7) {
    // Very angry - senior rep, high priority
    assignTo = 'tier2';
    priority = 'high';
  } else if (data.sentiment === 'negative') {
    // Mildly frustrated - standard but prioritized
    assignTo = 'tier1';
    priority = 'medium';
  } else {
    // Neutral/positive - standard queue
    assignTo = 'tier1';
    priority = 'normal';
  }

  return {
    ticketId: ticket.id,
    assignedTeam: assignTo,
    priority,
    sentiment: data.sentiment,
    sentimentScore: data.score,
    autoTagged: true
  };
}
Enter fullscreen mode Exit fullscreen mode

Now when someone opens a ticket saying "I've been trying to cancel for THREE WEEKS and nobody is helping me!!!", they don't wait in the same queue as "Quick question about feature X."

Aggregating Sentiment Over Time

Individual feedback is useful. Trends are more valuable.

async function getSentimentTrends(feedbackByDay) {
  const trends = [];

  for (const [date, items] of Object.entries(feedbackByDay)) {
    const sentiments = await Promise.all(
      items.map(item =>
        fetch('https://api.apiverve.com/v1/sentimentanalysis', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'x-api-key': process.env.APIVERVE_KEY
          },
          body: JSON.stringify({ text: item.text })
        }).then(r => r.json()).then(r => r.data)
      )
    );

    const positive = sentiments.filter(s => s.sentiment === 'positive').length;
    const negative = sentiments.filter(s => s.sentiment === 'negative').length;
    const neutral = sentiments.filter(s => s.sentiment === 'neutral').length;

    trends.push({
      date,
      total: items.length,
      positive,
      negative,
      neutral,
      positiveRatio: positive / items.length,
      avgMagnitude: sentiments.reduce((sum, s) => sum + s.magnitude, 0) / sentiments.length
    });
  }

  return trends;
}
Enter fullscreen mode Exit fullscreen mode

Now you can see: "Last Tuesday our negative sentiment spiked 40%. What happened?" Check the keywords from that day's negative feedback, and you'll probably find the answer.

Finding Patterns with Keywords

Sentiment tells you how people feel. Keywords tell you about what.

async function findTopIssues(negativeFeedback) {
  // Extract keywords from all negative feedback
  const allKeywords = await Promise.all(
    negativeFeedback.map(async item => {
      const { data } = await fetch('https://api.apiverve.com/v1/keywordextractor', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': process.env.APIVERVE_KEY
        },
        body: JSON.stringify({ text: item.text })
      }).then(r => r.json());

      return data.keywords || [];
    })
  );

  // Count keyword frequency
  const keywordCounts = {};
  allKeywords.flat().forEach(keyword => {
    keywordCounts[keyword] = (keywordCounts[keyword] || 0) + 1;
  });

  // Sort by frequency
  const topIssues = Object.entries(keywordCounts)
    .sort((a, b) => b[1] - a[1])
    .slice(0, 10)
    .map(([keyword, count]) => ({ keyword, count }));

  return topIssues;
}

// Output example:
// [
//   { keyword: "payment", count: 47 },
//   { keyword: "loading", count: 34 },
//   { keyword: "error", count: 28 },
//   { keyword: "cancel", count: 23 },
//   { keyword: "refund", count: 19 }
// ]
Enter fullscreen mode Exit fullscreen mode

If "payment" and "error" both appear in the top issues, you have a payments bug. If "loading" and "slow" are trending, you have a performance problem.

The company I mentioned earlier? If they'd been running this analysis, they would have seen their specific bug keyword spike in week one, not month three.

Handling Multiple Languages

Your customers don't all speak English. Detect language first, then analyze:

async function analyzeMultilingual(text) {
  // Detect language
  const langRes = await fetch(
    `https://api.apiverve.com/v1/languagedetector?text=${encodeURIComponent(text)}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );
  const { data: langData } = await langRes.json();

  // Sentiment analysis works best with English
  // For other languages, you might want to translate first
  // or accept that accuracy may vary

  const sentimentRes = await fetch('https://api.apiverve.com/v1/sentimentanalysis', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': process.env.APIVERVE_KEY
    },
    body: JSON.stringify({ text })
  });
  const { data: sentimentData } = await sentimentRes.json();

  return {
    language: langData.detectedLanguage,
    languageConfidence: langData.confidence,
    sentiment: sentimentData.sentiment,
    sentimentScore: sentimentData.score,
    magnitude: sentimentData.magnitude,
    note: langData.detectedLanguage !== 'english'
      ? 'Non-English text - sentiment accuracy may vary'
      : null
  };
}
Enter fullscreen mode Exit fullscreen mode

Batch Processing Historical Data

Have a backlog of feedback you've never analyzed? Process it in batches:

async function processBacklog(feedback, batchSize = 50) {
  const results = [];

  for (let i = 0; i < feedback.length; i += batchSize) {
    const batch = feedback.slice(i, i + batchSize);

    const batchResults = await Promise.all(
      batch.map(async item => {
        try {
          const { data } = await fetch('https://api.apiverve.com/v1/sentimentanalysis', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'x-api-key': process.env.APIVERVE_KEY
            },
            body: JSON.stringify({ text: item.text })
          }).then(r => r.json());

          return {
            id: item.id,
            sentiment: data.sentiment,
            score: data.score,
            magnitude: data.magnitude,
            processed: true
          };
        } catch (err) {
          return {
            id: item.id,
            processed: false,
            error: err.message
          };
        }
      })
    );

    results.push(...batchResults);

    // Rate limiting pause
    await new Promise(r => setTimeout(r, 200));

    console.log(`Processed ${Math.min(i + batchSize, feedback.length)}/${feedback.length}`);
  }

  return results;
}

// Run overnight
const allFeedback = await db.query('SELECT id, text FROM feedback WHERE sentiment IS NULL');
const analyzed = await processBacklog(allFeedback);
await db.batchUpdate(analyzed);
Enter fullscreen mode Exit fullscreen mode

Process 50 at a time, brief pause between batches to respect rate limits. Let it run overnight and wake up with your entire feedback history analyzed.

What to Do with the Insights

Analysis without action is just expensive note-taking. Here's how to actually use sentiment data:

1. Alert on negative spikes

async function checkSentimentAlerts() {
  const lastHour = await getSentimentStats('1h');
  const baseline = await getSentimentStats('7d');

  if (lastHour.negativeRatio > baseline.negativeRatio * 1.5) {
    await sendSlackAlert({
      channel: '#support',
      message: `⚠️ Negative feedback up ${Math.round((lastHour.negativeRatio / baseline.negativeRatio - 1) * 100)}% in the last hour. Top keywords: ${lastHour.topKeywords.join(', ')}`
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Weekly digest to product team

async function generateWeeklyDigest() {
  const feedback = await getWeekFeedback();
  const analyzed = await analyzeFeedback(feedback);

  const negatives = analyzed.filter(f => f.sentiment === 'negative');
  const topIssues = await findTopIssues(negatives);
  const promoters = analyzed.filter(f => f.sentiment === 'positive' && f.magnitude > 0.8);

  return {
    totalFeedback: analyzed.length,
    sentimentBreakdown: {
      positive: analyzed.filter(f => f.sentiment === 'positive').length,
      neutral: analyzed.filter(f => f.sentiment === 'neutral').length,
      negative: negatives.length
    },
    topComplaints: topIssues.slice(0, 5),
    potentialTestimonials: promoters.slice(0, 10),
    urgentItems: analyzed.filter(f => f.priority === 'urgent')
  };
}
Enter fullscreen mode Exit fullscreen mode

3. NPS correlation
If you track NPS, correlate it with sentiment:

async function analyzeNPSResponses(responses) {
  const analyzed = await Promise.all(
    responses.map(async r => {
      const { data } = await fetch('https://api.apiverve.com/v1/sentimentanalysis', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': process.env.APIVERVE_KEY
        },
        body: JSON.stringify({ text: r.comment })
      }).then(r => r.json());

      return {
        npsScore: r.score,
        sentiment: data.sentiment,
        magnitude: data.magnitude
      };
    })
  );

  // Detractors with high-magnitude negative sentiment = churn risk
  const churnRisk = analyzed.filter(
    r => r.npsScore <= 6 && r.sentiment === 'negative' && r.magnitude > 0.7
  );

  return churnRisk;
}
Enter fullscreen mode Exit fullscreen mode

The Cost

Sentiment analysis: 1 credit per request
Keyword extraction: 1 credit per request

If you're analyzing support tickets monthly with both APIs, the Starter plan ({{plan.starter.price}}/month) covers thousands of analyses.

Compare to:

  • Hiring someone to read 10,000 tickets: Not happening
  • Missing a pattern that costs you 23 customers: Much more than $29

You can't read everything your customers write. But you can't afford to ignore it either.

Sentiment analysis bridges the gap. It reads everything, flags what matters, and shows you patterns you'd never see manually. The company that lost 23 customers to an unnoticed bug? They now run sentiment analysis on every support ticket, every review, every NPS response. They've caught three similar patterns in the last six months—each fixed before it became a retention problem.

The Sentiment Analysis, Keyword Extractor, and Language Detector APIs work together. Same API key, consistent responses.

Get your API key and start understanding what your customers are actually telling you.


Originally published at APIVerve Blog

Top comments (0)