Mistral AI dans un SaaS B2B : 5 use-cases concrets en TypeScript
Intégrer une IA souveraine française dans votre SaaS, extraction de données, résumés clients, classification d'emails, génération de contenu et search 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));
}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;
}),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 ?? "";
}),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.
Articles connexes
Lancer 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.
LireRGPD pour SaaS B2B français, checklist complète sans mythes
Ce que vous devez vraiment faire pour être conforme RGPD en tant que SaaS B2B français, et ce que les agences de conseil vous vendent inutilement.
LireShipFast vs HeartCo : quel boilerplate SaaS choisir en 2026 ?
Comparaison directe et honnête entre ShipFast et HeartCo, stack technique, features, prix, et pour quel type de projet chaque solution convient vraiment.
Lire