Mistral AI dans un SaaS B2B : 5 use-cases concrets en TypeScript
Intégrer une IA française dans votre SaaS : extraction de données, résumés, classification d'emails, génération de contenu et recherche sémantique.
Pourquoi Mistral plutôt qu'OpenAI ?
Trois raisons concrètes pour un SaaS B2B français :
- Souveraineté des données : vos données clients ne sortent pas de l'UE. Argument RGPD béton face à un acheteur grand compte.
- Prix :
mistral-small-latestcoûte ~10x moins cher que GPT-4o pour des tâches d'extraction structurée. - Latence en EU : les serveurs Mistral sont à Paris, pas en Virginie. Vos webhooks répondent en 200ms au lieu de 600ms.
Setup minimal :
// 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 : Extraction de données depuis un PDF
Le client envoie une facture fournisseur en PDF. Vous voulez extraire automatiquement le montant, la date, le numéro.
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:
"Tu extrais les données structurées de factures françaises. Réponds uniquement en JSON valide.",
},
{
role: "user",
content: `Extrait ces champs de la facture : invoiceNumber, date (YYYY-MM-DD), totalHT, totalTTC, vatRate, supplierName.
Facture :
${text}`,
},
],
});
const raw = response.choices[0]?.message.content;
if (typeof raw !== "string") throw new Error("Empty response");
return InvoiceSchema.parse(JSON.parse(raw));
}Astuce critique
Utilisez responseFormat: { type: "json_object" } et validez avec Zod. Sinon vous allez JSON.parse du markdown un jour sur deux.
Use case 2 : Résumé d'historique client pour le CRM
Quand un commercial ouvre une fiche client, il veut comprendre en 5 secondes ce qui s'est passé : derniers échanges, signaux d'achat, points de friction.
// 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:
"Tu es un assistant CRM. Résume en 3 puces : statut actuel, dernier signal d'achat, prochaine action recommandée.",
},
{ role: "user", content: transcript },
],
});
return response.choices[0]?.message.content ?? null;
}),Cache obligatoire
Un résumé Mistral coûte ~0.01€ : multiplié par 200 utilisateurs qui ouvrent 50 fiches/jour, ça fait 100€/jour. Cachez la réponse 24h dans Redis avec invalidation sur nouvelle interaction.
Use case 3 : Classification d'emails entrants
Vous recevez les emails clients dans une boîte support. Vous voulez les router automatiquement vers le bon 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: `Tu classifies des emails de support. Réponds en JSON : {"category": "...", "urgent": boolean}.
Catégories valides : BUG_REPORT, FEATURE_REQUEST, BILLING, ONBOARDING, CHURN_RISK, OTHER.`,
},
{ role: "user", content: `Sujet : ${subject}\n\n${body}` },
],
});
const result = z
.object({ category: Category, urgent: z.boolean() })
.parse(JSON.parse(response.choices[0]?.message.content ?? "{}"));
return result;
}Performance typique : 92% de précision après 200 emails labellisés à la main pour few-shot prompting.
Use case 4 : Génération de contenu (descriptions produits)
Un utilisateur ajoute un produit dans son catalogue. Il rentre le nom et 3 caractéristiques. Vous générez la description marketing.
generateProductDescription: requirePermission("catalog:write")
.input(
z.object({
productName: z.string(),
keyFeatures: z.array(z.string()).min(1).max(5),
tone: z.enum(["pro", "casual", "premium"]).default("pro"),
}),
)
.mutation(async ({ ctx, input }) => {
// Freemium guard avant l'appel
const guard = await checkFreemiumLimit(ctx, "aiGenerations");
if (!guard.allowed) {
throw new TRPCError({
code: "FORBIDDEN",
message: `Quota IA atteint (${guard.used}/${guard.limit})`,
});
}
const response = await mistral.chat.complete({
model: "mistral-small-latest",
maxTokens: 250,
messages: [
{
role: "system",
content: `Tu rédiges des descriptions produit en français, ton ${input.tone}. 80 mots max.`,
},
{
role: "user",
content: `Produit : ${input.productName}\nCaractéristiques :\n- ${input.keyFeatures.join("\n- ")}`,
},
],
});
// Incrémenter le compteur APRÈS le succès
await ctx.db.subscription.update({
where: { organizationId: ctx.session.user.organizationId },
data: { aiGenerationsUsed: { increment: 1 } },
});
return response.choices[0]?.message.content ?? "";
}),Pattern critique
Check-before, increment-after. Si l'API Mistral fail, le quota n'est pas décrémenté.
Use case 5 : Search sémantique avec embeddings
L'utilisateur tape "facture impayée Dupont" et vous voulez retrouver des résultats même si le mot exact n'est pas dans la base.
// 1. À la création d'une ressource, calculer son 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");
// Stockage avec pgvector
await db.$executeRaw`
UPDATE "Document"
SET embedding = ${embedding}::vector
WHERE id = ${id}
`;
}
// 2. Au moment de la recherche
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
`;
}Coût : mistral-embed est à 0.10€ / 1M tokens. Indexer 10 000 documents de 500 mots = ~50 centimes. La recherche elle-même coûte des millièmes de centime.
Le pattern de coût global
| Use case | Coût/appel | Fréquence typique | Coût mensuel (1000 users) |
|---|---|---|---|
| Extraction PDF | ~0.005€ | 5/user/mois | 25€ |
| Résumé CRM | ~0.002€ | 30/user/mois (cached) | 60€ |
| Classification email | ~0.0003€ | 50/user/mois | 15€ |
| Génération produit | ~0.003€ | 10/user/mois | 30€ |
| Embedding (index + search) | ~0.0001€ | 200/user/mois | 20€ |
| Total | ~150€/mois |
Vous facturez vos plans 30-150€/mois. La marge sur l'IA est confortable.
Erreurs à éviter
- Appeler Mistral depuis un Server Component sans cache : un refresh = un appel API. Toujours wrapper avec
unstable_cacheou Redis. - Pas de retry sur erreur réseau : Mistral a un SLA de 99.9% mais des spikes. Implémentez 3 retries avec backoff exponentiel.
- Streamer dans une Edge Function de plus de 25s : Vercel coupe à 30s. Pour les longs streams, utilisez un endpoint Node.js classique.
- Oublier le freemium guard avant l'appel : sans ça, un utilisateur du plan gratuit peut consommer 1000€ d'IA en 1 nuit.
Conclusion
Mistral n'est pas "OpenAI mais français". C'est une stack adaptée aux SaaS européens : RGPD-friendly, économique, et avec des modèles assez bons pour 90% des cas d'usage B2B. Pour les 10% restants (raisonnement complexe, code generation), gardez un fallback vers Claude ou GPT-4o, mais commencez par Mistral.
Dans HeartCo, l'intégration Mistral est pré-câblée : freemium guard, retry, cache Redis, et exemples dans src/lib/ai/. Vous ajoutez votre use case en 30 lignes.
Pour aller plus loin
Articles connexes
Cadrer Claude Code avec CLAUDE.md sur un SaaS multi-tenant
Les vraies règles qui gouvernent ce SaaS avec Claude Code : ce qui va dans CLAUDE.md, ce qui n'y va pas, et ce qui est réellement livré à l'achat.
LireLancer un SaaS sans coder en 2026, la stack hybride no-code + code
Comment passer de l'idée au SaaS en production sans écrire une ligne de Stripe ou de NextAuth, avec un assistant no-code qui génère le code pour vous.
LireFacturation électronique 2026 pour un SaaS B2B
Calendrier officiel, vocabulaire PDP et e-reporting, générer un Factur-X, et le rôle d'un raccordement comme iopole pour un SaaS B2B français.
LirePrêt à lancer ton SaaS ?
HeartCo Starter inclut tout ce dont tu as besoin : auth, paiements, IA, mobile, sécurité auditée. À partir de 199 €.