Skip to main content
All articles
6 min read

Mistral AI in a B2B SaaS: 5 Concrete TypeScript Use Cases

Integrating a European AI provider into your SaaS: data extraction, summaries, email classification, content generation, and semantic search.

Why Mistral over OpenAI?

Three concrete reasons for a SaaS selling into the EU market:

  • Data sovereignty: your customers' data never leaves the EU. A solid GDPR argument in front of an enterprise buyer.
  • Price: mistral-small-latest costs roughly 10x less than GPT-4o for structured extraction tasks.
  • EU latency: Mistral's servers are in Paris, not in Virginia. Your webhooks respond in 200ms instead of 600ms.

Minimal setup:

// src/lib/ai/mistral.ts
import { Mistral } from "@mistralai/mistralai";
import { env } from "~/env";
 
export const mistral = new Mistral({ apiKey: env.MISTRAL_API_KEY });

Use case 1: extracting data from a PDF

A customer sends a supplier invoice as a PDF. You want to automatically extract the amount, date, and invoice number.

import { z } from "zod";
 
const InvoiceSchema = z.object({
  invoiceNumber: z.string(),
  date: z.string(),
  totalHT: z.number(),
  totalTTC: z.number(),
  vatRate: z.number(),
  supplierName: z.string(),
});
 
export async function extractInvoiceFromText(text: string) {
  const response = await mistral.chat.complete({
    model: "mistral-small-latest",
    responseFormat: { type: "json_object" },
    messages: [
      {
        role: "system",
        content:
          "You extract structured data from invoices. Respond only with valid JSON.",
      },
      {
        role: "user",
        content: `Extract these fields from the invoice: invoiceNumber, date (YYYY-MM-DD), totalHT, totalTTC, vatRate, supplierName.
 
Invoice:
${text}`,
      },
    ],
  });
 
  const raw = response.choices[0]?.message.content;
  if (typeof raw !== "string") throw new Error("Empty response");
 
  return InvoiceSchema.parse(JSON.parse(raw));
}

Critical tip

Use responseFormat: { type: "json_object" } and validate with Zod. Otherwise you'll be JSON.parse-ing markdown one day out of two.

Use case 2: summarizing customer history for the CRM

When a sales rep opens a customer record, they want to understand in 5 seconds what's been happening: recent exchanges, buying signals, friction points.

// src/server/api/routers/crm.ts
import { mistral } from "~/lib/ai/mistral";
 
summarizeClientHistory: requirePermission("crm:read")
  .input(z.object({ clientId: z.string() }))
  .query(async ({ ctx, input }) => {
    const interactions = await ctx.orgDb.clientInteraction.findMany({
      where: { clientId: input.clientId },
      orderBy: { createdAt: "desc" },
      take: 20,
    });
 
    if (interactions.length === 0) return null;
 
    const transcript = interactions
      .map((i) => `[${i.type}] ${i.createdAt.toISOString()}: ${i.content}`)
      .join("\n");
 
    const response = await mistral.chat.complete({
      model: "mistral-small-latest",
      maxTokens: 200,
      messages: [
        {
          role: "system",
          content:
            "You are a CRM assistant. Summarize in 3 bullets: current status, latest buying signal, recommended next action.",
        },
        { role: "user", content: transcript },
      ],
    });
 
    return response.choices[0]?.message.content ?? null;
  }),

Caching is mandatory

A single Mistral summary costs roughly $0.01. Multiply that by 200 users opening 50 records a day, and you're at $100/day. Cache the response for 24h in Redis, invalidated on the next interaction.

Use case 3: classifying incoming emails

You receive customer emails in a support inbox. You want to route them automatically to the right agent.

const Category = z.enum([
  "BUG_REPORT",
  "FEATURE_REQUEST",
  "BILLING",
  "ONBOARDING",
  "CHURN_RISK",
  "OTHER",
]);
 
export async function classifyEmail(subject: string, body: string) {
  const response = await mistral.chat.complete({
    model: "mistral-small-latest",
    responseFormat: { type: "json_object" },
    maxTokens: 50,
    messages: [
      {
        role: "system",
        content: `You classify support emails. Respond in JSON: {"category": "...", "urgent": boolean}.
Valid categories: BUG_REPORT, FEATURE_REQUEST, BILLING, ONBOARDING, CHURN_RISK, OTHER.`,
      },
      { role: "user", content: `Subject: ${subject}\n\n${body}` },
    ],
  });
 
  const result = z
    .object({ category: Category, urgent: z.boolean() })
    .parse(JSON.parse(response.choices[0]?.message.content ?? "{}"));
 
  return result;
}

Typical performance: 92% accuracy after hand-labeling 200 emails for few-shot prompting.

Use case 4: generating content (product descriptions)

A user adds a product to their catalog. They enter the name and 3 key features. You generate the marketing description.

generateProductDescription: requirePermission("catalog:write")
  .input(
    z.object({
      productName: z.string(),
      keyFeatures: z.array(z.string()).min(1).max(5),
      tone: z.enum(["professional", "casual", "premium"]).default("professional"),
    }),
  )
  .mutation(async ({ ctx, input }) => {
    // Freemium guard before the call
    const guard = await checkFreemiumLimit(ctx, "aiGenerations");
    if (!guard.allowed) {
      throw new TRPCError({
        code: "FORBIDDEN",
        message: `AI quota reached (${guard.used}/${guard.limit})`,
      });
    }
 
    const response = await mistral.chat.complete({
      model: "mistral-small-latest",
      maxTokens: 250,
      messages: [
        {
          role: "system",
          content: `You write product descriptions, ${input.tone} tone. 80 words max.`,
        },
        {
          role: "user",
          content: `Product: ${input.productName}\nFeatures:\n- ${input.keyFeatures.join("\n- ")}`,
        },
      ],
    });
 
    // Increment the counter AFTER success
    await ctx.db.subscription.update({
      where: { organizationId: ctx.session.user.organizationId },
      data: { aiGenerationsUsed: { increment: 1 } },
    });
 
    return response.choices[0]?.message.content ?? "";
  }),

Critical pattern

Check-before, increment-after. If the Mistral call fails, the quota isn't decremented.

Use case 5: semantic search with embeddings

A user types "unpaid invoice Smith" and you want to surface results even when the exact words aren't in the database.

// 1. When a resource is created, compute its embedding
export async function indexDocument(id: string, text: string) {
  const response = await mistral.embeddings.create({
    model: "mistral-embed",
    inputs: [text],
  });
 
  const embedding = response.data[0]?.embedding;
  if (!embedding) throw new Error("Embedding failed");
 
  // Stored via pgvector
  await db.$executeRaw`
    UPDATE "Document"
    SET embedding = ${embedding}::vector
    WHERE id = ${id}
  `;
}
 
// 2. At search time
export async function semanticSearch(query: string, orgId: string) {
  const queryEmbedding = (
    await mistral.embeddings.create({
      model: "mistral-embed",
      inputs: [query],
    })
  ).data[0]?.embedding;
 
  return db.$queryRaw`
    SELECT id, title, 1 - (embedding <=> ${queryEmbedding}::vector) AS similarity
    FROM "Document"
    WHERE "organizationId" = ${orgId}
    ORDER BY embedding <=> ${queryEmbedding}::vector
    LIMIT 10
  `;
}

Cost: mistral-embed runs $0.10 per 1M tokens. Indexing 10,000 documents of 500 words costs about $0.50. The search query itself costs a fraction of a cent.

The overall cost pattern

Use caseCost/callTypical frequencyMonthly cost (1,000 users)
PDF extraction~$0.0055/user/month$25
CRM summary~$0.00230/user/month (cached)$60
Email classification~$0.000350/user/month$15
Product generation~$0.00310/user/month$30
Embeddings (index+search)~$0.0001200/user/month$20
Total~$150/month

You're charging $30-150/month per plan. The margin on AI is comfortable.

Mistakes to avoid

  1. Calling Mistral from a Server Component with no cache: a page refresh becomes an API call. Always wrap it with unstable_cache or Redis.
  2. No retry on network errors: Mistral has a 99.9% SLA but real spikes happen. Implement 3 retries with exponential backoff.
  3. Streaming from an Edge Function over 25s: Vercel cuts off at 30s. For long streams, use a regular Node.js endpoint instead.
  4. Forgetting the freemium guard before the call: without it, a free-plan user can burn $1,000 of AI usage in one night.

Conclusion

Mistral isn't "OpenAI but French." It's a stack suited to European SaaS: GDPR-friendly, cheap, and good enough for 90% of B2B use cases. For the remaining 10% (complex reasoning, code generation), keep a fallback to Claude or GPT-4o, but start with Mistral.

In HeartCo, the Mistral integration is pre-wired: freemium guard, retry, Redis cache, and examples in src/lib/ai/. You add your own use case in about 30 lines.

Go further

Share

Ready to launch your SaaS?

HeartCo Starter includes everything you need: auth, payments, AI, mobile, audited security. Starting at $219.

30-day money-back guarantee
Mistral AI in a B2B SaaS: 5 Use Cases | HeartCo