Back to blog
Tutorial·7 min read·1215 words

How to Build an AI-Powered Sentiment Analysis API with Python in 2026

A step-by-step guide to building a production-ready sentiment analysis API using Python, FastAPI, and the Qubax AI gateway.

How to Build an AI-Powered Sentiment Analysis API with Python in 2026 — illustration

Sentiment analysis is one of the highest-ROI projects in applied AI: automatically classifying text as positive, negative, or neutral at scale. Product teams use it to triage reviews, support teams use it to detect angry tickets before they churn, and traders use it to gauge market mood from news.

In 2026, you don't need to train your own NLP model to do this well. Large language models classify sentiment more accurately than purpose-built legacy models — especially on messy, real-world text full of sarcasm, slang, and mixed signals. In this tutorial, you'll build a production-ready sentiment analysis API using Python, FastAPI, and the Qubax AI gateway.

What We're Building

By the end of this tutorial you'll have:

  • A FastAPI service exposing POST /sentiment
  • LLM-powered classification with structured JSON output (label + confidence + reasoning)
  • Batch support for analyzing hundreds of texts in one request
  • Fallback routing so a provider outage doesn't take your API down

The complete project fits in ~150 lines of Python.

Prerequisites

  • Python 3.11+
  • A Qubax AI account and API key (grab one at qubax.ai)
  • Basic familiarity with Python and HTTP

Why Qubax instead of calling OpenAI or Anthropic directly? One API, dozens of models, and you can switch between them (e.g., GPT-5.6 Luna for cheap high-volume runs, Claude Sonnet 5 for nuanced analysis) by changing a single string — no re-integration. See the docs for full API reference.

Step 1: Project Setup

bash
mkdir sentiment-api && cd sentiment-api
python -m venv .venv && source .venv/bin/activate
pip install fastapi uvicorn httpx pydantic

Create the project structure:

code
sentiment-api/
├── main.py          # FastAPI app
├── analyzer.py      # LLM sentiment logic
└── .env             # QUBAX_API_KEY=...

Store your API key in .env — never hardcode it:

code
QUBAX_API_KEY=your-key-here

Step 2: The Sentiment Engine

The trick to reliable LLM classification is structured output: we'll demand strict JSON with a schema, so downstream code never has to parse prose.

Create analyzer.py:

python
import os
import json
import httpx

QUBAX_URL = "https://api.qubax.ai/v1/chat/completions"
API_KEY = os.environ["QUBAX_API_KEY"]

SYSTEM_PROMPT = """You are a sentiment classification engine.
Classify each text with EXACTLY one label: positive, negative, or neutral.
Respond with strict JSON, no markdown fences, matching this schema:
{"label": "<positive|negative|neutral>", "confidence": <0.0-1.0>, "reasoning": "<one short sentence>"}
Handle sarcasm, slang, and mixed sentiment. If genuinely mixed, pick the dominant tone and explain in reasoning."""

MODEL = "gpt-5.6-luna"  # cheap + fast; swap to "claude-sonnet-5" for nuance


async def classify(text: str) -> dict:
    payload = {
        "model": MODEL,
        "messages": [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": text},
        ],
        "temperature": 0.0,  # classification wants determinism
        "response_format": {"type": "json_object"},
    }
    async with httpx.AsyncClient(timeout=30) as client:
        resp = await client.post(
            QUBAX_URL,
            headers={"Authorization": f"Bearer {API_KEY}"},
            json=payload,
        )
        resp.raise_for_status()
        content = resp.json()["choices"][0]["message"]["content"]
        return json.loads(content)

Two details matter a lot here:

  • `temperature: 0.0` — for classification you want consistent, repeatable outputs, not creativity.
  • `response_format: json_object` — forces valid JSON, eliminating the "model added a chatty preamble" failure mode.

Step 3: The FastAPI Layer

Create main.py:

python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from analyzer import classify

app = FastAPI(title="Sentiment Analysis API", version="1.0.0")


class SentimentRequest(BaseModel):
    text: str = Field(..., min_length=1, max_length=10_000)


class SentimentResponse(BaseModel):
    label: str
    confidence: float
    reasoning: str


@app.post("/sentiment", response_model=SentimentResponse)
async def sentiment(req: SentimentRequest):
    try:
        result = await classify(req.text)
    except Exception as e:
        raise HTTPException(status_code=502, detail=f"upstream error: {e}")

    if result.get("label") not in {"positive", "negative", "neutral"}:
        raise HTTPException(status_code=502, detail="invalid model output")
    return result

Run it:

bash
export $(cat .env) && uvicorn main:app --reload

Test it:

bash
curl -s localhost:8000/sentiment -H 'Content-Type: application/json' \
  -d '{"text": "Well ANOTHER Monday. Just what my week needed. Amazing."}'

You'll get back something like:

json
{
  "label": "negative",
  "confidence": 0.93,
  "reasoning": "Sarcastic phrasing ('Well ANOTHER Monday') signals negativity despite surface-level positive words."
}

That sarcasm catch is exactly where LLMs demolish older keyword-based sentiment tools.

Step 4: Batch Analysis

Single-text calls are fine for demos, but real workloads come in bulk. Add a batch endpoint that fans out concurrently:

python
import asyncio

class BatchRequest(BaseModel):
    texts: list[str] = Field(..., min_length=1, max_length=500)


@app.post("/sentiment/batch")
async def batch(req: BatchRequest):
    results = await asyncio.gather(
        *(classify(t) for t in req.texts),
        return_exceptions=True,
    )
    out = []
    for text, r in zip(req.texts, results):
        if isinstance(r, Exception):
            out.append({"text": text, "error": str(r)})
        else:
            out.append({"text": text, **r})
    return {"count": len(out), "results": out}

asyncio.gather fires all LLM calls in parallel — 200 reviews analyze nearly as fast as one.

Step 5: Model Routing by Job

Here's where a multi-model gateway pays off. Different jobs deserve different models:

python
def pick_model(text: str) -> str:
    if len(text) > 5_000:
        return "deepseek-v4-flash"          # cheap long-context workhorse
    if "?" in text and "vs" in text.lower():
        return "claude-sonnet-5"            # nuanced comparative reasoning
    return "gpt-5.6-luna"                    # default: fast + inexpensive

Route in classify() by replacing the fixed MODEL with pick_model(text). A simple heuristic router like this routinely cuts costs 60–80% versus sending everything to a flagship model — and since every model lives behind the same Qubax endpoint, switching costs nothing.

Production Tips

Before shipping, add these:

  • Retries with backoff — wrap classify() with 3 retries on 429/5xx responses
  • Cost caps — track tokens per API key; a runaway loop can get expensive fast
  • Caching — identical texts (common with review dedup) should hit a cache, not the LLM
  • Logging labels only — avoid storing raw customer text unless you must, for privacy

Conclusion

You now have a working, honest-to-goodness sentiment analysis microservice: fast to build, cheap to run, and more accurate than the dedicated NLP tooling of a few years ago. From here you can extend to emotion taxonomies (angry/sad/frustrated), aspect-based sentiment (shipping vs. quality vs. support), or multilingual classification — it's all just prompt engineering on top of the same skeleton.

Ready to build? Grab an API key at Qubax AI and start classifying — full API documentation is at qubax.ai/docs.

FAQ

Why use an LLM for sentiment instead of a dedicated NLP library?

Legacy libraries match keywords and struggle with sarcasm, negation ("not bad at all"), and domain slang. LLMs read context the way people do and catch sarcastic or mixed-sentiment text far more reliably — and return reasoning you can audit.

Which model should I use for sentiment analysis?

For high-volume, straightforward text, a fast, cheap model like GPT-5.6 Luna or DeepSeek V4 Flash is ideal. For nuanced, sarcastic, or high-stakes text, Claude Sonnet 5 classifies more accurately. The Qubax model catalog lists live pricing for all of them.

How much does it cost to run this at scale?

Sentiment prompts are short (a few hundred tokens each), so per-call costs are tiny. With cheap models and caching, classifying 100,000 reviews typically costs just a few dollars through Qubax's discounted rates.

How do I force valid JSON from the model?

Use response_format: {"type": "json_object"} where supported, set temperature to 0, and include an explicit schema in the system prompt. The double belt-and-suspenders approach in this tutorial rarely fails.

Can this handle languages other than English?

Yes — modern LLMs are natively multilingual. The same prompt works for Spanish, German, Japanese, and dozens more, though you may want to add "respond with reasoning in English" for consistent logging.

🤖

Try Claude Sonnet 5 on Qubax

Best balance of speed and quality. Up to 62% off.

View pricing

Article tags

#Python#API#sentiment analysis#tutorial
Share:Post on XTelegramLinkedInYHacker NewsReddit
Qubax AI

Qubax AI

AI Models at up to 99% off · Pay with crypto

Reading about Claude Sonnet 5 and Claude? Access them — plus 340+ other models — through one API. Best balance of speed and quality. Up to 62% off.

Related articles