Utiliser Normi dans une app Lovable
Ajoutez des estimations, l'historique des ventes DVF et les prix au m² de toute la France à une application générée avec Lovable, en une Edge Function Supabase et un prompt.
Ce que vous pouvez construire
- Estimateur en ligne : un formulaire adresse + surface qui renvoie une fourchette de prix.
- Outil de prospection : toutes les ventes récentes d'un secteur pour cibler les propriétaires.
- Carte des ventes : les transactions DVF géolocalisées autour d'un point.
- Tableau de bord de marché : prix au m², volumes et tendances par commune.
Toutes ces données passent par l'API REST Normi. Ce guide utilise /v1/estimate et /v1/transactions.
Prérequis
- Une clé API Normi : créez-la depuis le tableau de bord (500 crédits offerts).
- Un projet Lovable connecté à Supabase (onglet Supabase de Lovable).
1. Stocker la clé en secret
Dans Supabase, ajoutez un secret d'Edge Function nommé NORMI_API_KEY :
NORMI_API_KEY=normi_votre_token
2. Créer l'Edge Function
La fonction relaie les appels vers Normi, ajoute la clé et gère les limites de débit :
// supabase/functions/normi/index.ts
const NORMI = "https://mcp.normi.fr/v1";
const KEY = Deno.env.get("NORMI_API_KEY")!;
const CORS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, content-type, apikey, x-client-info",
};
async function normi(path: string, params: Record<string, string | number>) {
const qs = new URLSearchParams(
Object.entries(params).map(([k, v]) => [k, String(v)]),
);
for (let attempt = 0; attempt < 3; attempt++) {
const res = await fetch(`${NORMI}${path}?${qs}`, {
headers: { Authorization: `Bearer ${KEY}` },
});
// 429 : attendre le délai indiqué puis réessayer
if (res.status === 429) {
const wait = Number(res.headers.get("retry-after") ?? 5);
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
const body = await res.json();
if (!res.ok) throw new Error(`${body.error?.code}: ${body.error?.message}`);
return body;
}
throw new Error("rate_limited: trop de tentatives");
}
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: CORS });
try {
const { action, ...params } = await req.json();
const data =
action === "estimate"
? await normi("/estimate", params)
: await normi("/transactions", params);
return Response.json(data, { headers: CORS });
} catch (e) {
return Response.json({ error: String(e) }, { status: 400, headers: CORS });
}
});Côté interface, Lovable l'appelle avec le client Supabase :
import { supabase } from "@/integrations/supabase/client";
const { data } = await supabase.functions.invoke("normi", {
body: {
action: "estimate",
latitude: 43.6085,
longitude: 3.8797,
surface: 65,
pieces: 3,
type_local: "Appartement",
},
});
// data.estimate.price_range.mid -> estimation centrale en euros3. Le prompt à coller dans Lovable
Lovable devine parfois les noms de paramètres. Ce prompt lui donne les bons :
Ajoute une estimation immobilière basée sur l'API Normi (données DVF).
- Crée une Edge Function Supabase "normi" qui appelle https://mcp.normi.fr/v1
avec l'en-tête Authorization: Bearer <secret NORMI_API_KEY>.
Ne jamais appeler l'API Normi depuis le navigateur.
- Estimation : GET /v1/estimate avec latitude, longitude, surface (obligatoires),
pieces et type_local ("Appartement" ou "Maison").
- Ventes : GET /v1/transactions avec code_postal (5 chiffres), type_local,
date_debut (YYYY-MM-DD), limit (200 maximum) et offset.
Paginer avec next_cursor jusqu'à ce qu'il vaille null.
- N'invente aucun paramètre : "city" ou "postal_code" n'existent pas,
l'API renvoie une erreur 400 sur tout paramètre inconnu.
- Sur une réponse 429, attendre le nombre de secondes de l'en-tête retry-after.
- Sur une réponse 402, afficher "Crédits épuisés" au lieu d'un écran vide.
- Affiche price_range.low / mid / high et price_per_m2.median du résultat.Exemple : un T3 à Montpellier
Appartement de 65 m², 3 pièces, près de la place de la Comédie. Normi compare avec les ventes d'appartements à moins de 500 m sur les 18 derniers mois de données DVF : médiane autour de 3 760 €/m², soit environ 244 500 €. Réponse abrégée :
{
"estimate": {
"price_range": { "mid": 244500 },
"price_per_m2": { "median": 3762, "used_surface": 65 },
"confidence": "high"
},
"input": {
"latitude": 43.6085,
"longitude": 3.8797,
"surface": 65,
"type_local": "Appartement"
},
"_credits": { "used": 10, "remaining": 490 }
}La réponse complète inclut la fourchette basse et haute, le nombre de ventes comparables et les facteurs de confiance : référence /v1/estimate.
Récupérer toutes les ventes d'un secteur
/v1/transactions renvoie 200 ventes au plus par appel. Suivez next_cursor pour parcourir le reste :
// Toutes les ventes d'appartements à Montpellier 34000 depuis juillet 2025
const sales = [];
let offset = 0;
while (offset !== null) {
const page = await normi("/transactions", {
code_postal: "34000",
type_local: "Appartement",
date_debut: "2025-07-01",
limit: 200, // maximum autorisé
offset,
});
sales.push(...page.transactions);
offset = page.next_cursor; // null quand c'est la dernière page
}date_debut sur 12 mois plutôt que 5 ans divise la facture et le temps de chargement.Pièges fréquents
| Erreur | Cause | Solution |
|---|---|---|
missing_location (400) | Aucun filtre de lieu reconnu, souvent city ou postal_code au lieu de code_postal. | Utilisez code_postal, commune, code_departement, ou latitude + longitude. |
invalid_params (400) | limit au-delà de 200, paramètre inconnu, latitude manquante. | limit ≤ 200 et paginez avec offset. Seuls les paramètres documentés sont acceptés. |
rate_limited (429) | Trop d'appels par minute, typique d'une boucle de pagination sans pause. | Respectez l'en-tête retry-after. La fonction ci-dessus le fait déjà. |
insufficient_credits (402) | Solde à zéro. | Rechargez depuis le tableau de bord. Les crédits gratuits reviennent à 500 le 1er du mois. |
at_capacity (503) | Serveur saturé pendant quelques secondes. Rien n'est débité. | Réessayez après une courte pause. |
Coût en crédits
/v1/estimate: 10 crédits par estimation./v1/transactions: 5 crédits par page, quelle que soit sa taille. 1 000 ventes = 5 pages = 25 crédits.- Les appels en erreur ne sont pas débités.
Questions fréquentes
Faut-il un plan payant Normi pour tester ?
Non. Chaque compte reçoit 500 crédits gratuits, soit 50 estimations ou 100 pages de 200 ventes. Ils sont rechargés chaque mois.
Puis-je appeler l'API directement depuis le front Lovable ?
Non. Votre clé serait visible par tous les visiteurs et l'API n'accepte les appels navigateur que depuis une liste d'origines autorisées. Passez par une Edge Function Supabase.
Les données sont-elles à jour ?
Les ventes viennent des fichiers DVF publiés par la DGFiP, mis à jour une fois par an. Chaque réponse indique la date de la dernière vente disponible.