Documentation API
API ERPplus
Generez des Etats des Risques et Pollutions par API. Authentifiez-vous avec votre cle API pour acceder a tous les endpoints.
Pour explorer la spec OpenAPI 3.0 de manière interactive (Try it out, génération SDK), voir la spec interactive Swagger UI. Le YAML brut est disponible sur /api-docs-ui/openapi.yaml pour les générateurs de SDK.
Authentification
Ajoutez votre cle API dans le header Authorization de chaque requete :
Authorization: Bearer erpplus_votre_cle_api
Versioning
L'API B2B ERPplus est versionnée. L'URL canonique est /api/v1/erp*. Cette URL est figée pour la durée de votre contrat et garantit la stabilité de votre intégration.
Tout breaking change passera par un nouveau préfixe /api/v2/ opt-in. Lors de l'annonce de la v2, l'API v1 recevra les headers Deprecation et Sunset (RFC 8594) avec un préavis minimum de 6 mois avant retrait. Vous aurez le temps de planifier votre migration sereinement.
Idempotency
Les requêtes POST /api/v1/erp et POST /api/v1/erp/batch acceptent un header Idempotency-Key qui permet de rejouer une requête sans risque de double création (panne réseau, retry client, redémarrage de worker).
Idempotency-Key: 5f7e8b9c-3a2d-4f1e-9b8a-c1d2e3f4a5b6
Format : 8 à 255 caractères dans [a-zA-Z0-9_-]. Recommandation : UUID v4 généré côté client. Scope per-user (deux clients distincts peuvent réutiliser la même clé sans collision).
Comportement :
- Première requête : la réponse est snapshottée (status + body) et stockée pendant 24h.
- Replay même clé même body : retour exact du snapshot initial avec le header
Idempotency-Replay: true. Le pipeline n'est pas relancé. - Replay même clé body différent : retour 422
idempotency_key_conflict. Pour soumettre un nouveau body, générer une nouvelle clé. - Header invalide ou trop court : retour 400
invalid_idempotency_key. - Header absent : comportement standard, aucune persistance.
- Erreur 5xx : la réponse n'est pas snapshottée, un retry avec la même clé est accepté (erreur transitoire serveur).
Rétention des Idempotency-Key
Les Idempotency-Key sont conservées 24h après leur création (aligné Stripe et standards de marché). Au-delà, la clé est purgée du serveur et toute requête utilisant la même clé sera traitée comme une nouvelle commande, ce qui consommera de nouveaux crédits.
Si vous rejouez une commande plus de 12h après le premier essai, régénérez la clé côté client pour éviter tout double-débit involontaire.
Exemple :
# Première requête curl -X POST https://votre-domaine/api/v1/erp \ -H "Authorization: Bearer erpplus_votre_cle_api" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 5f7e8b9c-3a2d-4f1e-9b8a-c1d2e3f4a5b6" \ -d '{"address": "21 Avenue Jean Moulin 75014 Paris", "vendeur": "SCI Dupont"}' # Retry réseau après panne, même clé même body : retour du snapshot initial curl -X POST https://votre-domaine/api/v1/erp \ -H "Authorization: Bearer erpplus_votre_cle_api" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 5f7e8b9c-3a2d-4f1e-9b8a-c1d2e3f4a5b6" \ -d '{"address": "21 Avenue Jean Moulin 75014 Paris", "vendeur": "SCI Dupont"}' # Headers de réponse : Idempotency-Replay: true
// Première requête const res = await fetch("https://votre-domaine/api/v1/erp", { method: "POST", headers: { Authorization: "Bearer erpplus_votre_cle_api", "Content-Type": "application/json", "Idempotency-Key": "5f7e8b9c-3a2d-4f1e-9b8a-c1d2e3f4a5b6" }, body: JSON.stringify({ address: "21 Avenue Jean Moulin 75014 Paris", vendeur: "SCI Dupont" }) }); // Retry réseau après panne, même clé même body : retour du snapshot initial const retry = await fetch("https://votre-domaine/api/v1/erp", { method: "POST", headers: { Authorization: "Bearer erpplus_votre_cle_api", "Content-Type": "application/json", "Idempotency-Key": "5f7e8b9c-3a2d-4f1e-9b8a-c1d2e3f4a5b6" }, body: JSON.stringify({ address: "21 Avenue Jean Moulin 75014 Paris", vendeur: "SCI Dupont" }) }); // Headers de réponse : retry.headers.get("Idempotency-Replay") === "true"
# Première requête import requests res = requests.post( "https://votre-domaine/api/v1/erp", headers={"Authorization": "Bearer erpplus_votre_cle_api", "Content-Type": "application/json", "Idempotency-Key": "5f7e8b9c-3a2d-4f1e-9b8a-c1d2e3f4a5b6"}, json={"address": "21 Avenue Jean Moulin 75014 Paris", "vendeur": "SCI Dupont"}, ) # Retry réseau après panne, même clé même body : retour du snapshot initial retry = requests.post( "https://votre-domaine/api/v1/erp", headers={"Authorization": "Bearer erpplus_votre_cle_api", "Content-Type": "application/json", "Idempotency-Key": "5f7e8b9c-3a2d-4f1e-9b8a-c1d2e3f4a5b6"}, json={"address": "21 Avenue Jean Moulin 75014 Paris", "vendeur": "SCI Dupont"}, ) # Headers de réponse : retry.headers["Idempotency-Replay"] == "true"
Endpoints
URL canonique : /api/v1/erp* (cf. section Versioning).
{
"address": "21 Avenue Jean Moulin 75014 Paris",
"parcelle": "CH42", // optionnel
"vendeur": "SCI Dupont", // obligatoire (max 200 chars)
"acquereur": "M Martin", // optionnel (max 200 chars)
"transaction_type": "vente" // obligatoire : "vente" ou "location"
}
Retourne { "job_id": "uuid", "status": "queued" }. Erreurs : 400 vendeur_required, vendeur_too_long, transaction_type_required, transaction_type_invalid.
La nature de la transaction est exigee depuis le lot P-10, et c’est une rupture de contrat : le champ etait optionnel et son absence stockait vente. Une commande qui en manque est refusee en 400, aucun credit debite. Un document dont le type n’est pas connu ecrit desormais Vendeur ou Bailleur et Acquereur ou Locataire, au lieu d’affirmer une vente.
Retourne { "job_id", "status", "address", "parcelle", "createdAt", "completedAt", "error", "expiresAt", "daysRemaining", "expiryStatus", "degraded", "unverifiedSections", "freeRenewalUntil" }
Statuts possibles : queued (en file), processing (en cours), completed (terminé), failed (échec).
degraded est vrai quand le document a été livré avec au moins une rubrique du formulaire non vérifiée, une source publique étant restée muette malgré les reprises ; unverifiedSections en porte les codes, dans l’ordre où le document présente ces rubriques (ppr_naturels_liste, ppr_naturels_exposition, ppr_miniers_liste, ppr_miniers_exposition, ppr_technologiques_liste, ppr_technologiques_exposition, sismicite, radon, rtc, sis, old, peb), vide sinon ; freeRenewalUntil est la date jusqu’à laquelle POST /api/v1/erp/{id}/renew est gratuit pour ce document (fin de sa validité), null quand le renouvellement se débite ou n’a pas d’objet. Les trois champs figurent aussi sur chaque élément de GET /api/v1/erp/list. Un client qui les ignore ne voit aucun changement.
Retourne le fichier PDF (Content-Type: application/pdf). Disponible uniquement quand status = "completed".
{
"commune": "Etampes",
"insee": "91223",
"parcelles": ["BL370", "BL312"],
"vendeur": "SCI Dupont", // obligatoire (max 200 chars)
"acquereur": "M Martin", // optionnel (max 200 chars)
"transaction_type": "vente" // obligatoire : "vente" ou "location", partage par tout le lot
}
Retourne { "batch_id": "uuid", "jobs": [{ "job_id", "parcelle", "status" }, ...], "credits" }. Maximum 20 parcelles par batch. Le vendeur s'applique a tous les jobs du batch.
Retourne { "batch_id", "total", "completed", "failed", "processing", "queued", "status", "jobs": [{ "job_id", ... }] }. Les compteurs completed, failed, processing, queued partitionnent les jobs du batch. Le status agrégé prend l'une des valeurs processing, completed, failed.
Retourne l'historique des jobs de l'utilisateur, au format pagine uniforme. Voir section Pagination pour le format de reponse, les query params et les bornes.
Supprime le job et son PDF. Retourne 409 si le job est en cours.
Retourne les informations de l'utilisateur, dont le solde de credits.
Pagination
Les endpoints de listing renvoient un format paginé uniforme. Query params ?limit=N&offset=M, valeurs par défaut limit=500, plafond limit=500. Une valeur non entière, négative ou supérieure au plafond renvoie une erreur 400 invalid_pagination.
Endpoints concernés :
GET /api/v1/erp/listGET /api/auth/ordersGET /api/account/invoicesGET /api/admin/usersGET /api/admin/users/{id}/jobs
Format de réponse :
{
"data": [ /* tableau d'éléments */ ],
"pagination": {
"total": 1234,
"limit": 500,
"offset": 0,
"has_more": true
}
}
Le header X-Total-Count est également émis et reflète pagination.total. Pour itérer sur toute la collection, incrémenter offset de limit tant que has_more est true.
Erreur 400 :
{
"error": "invalid_pagination",
"message": "limit must not exceed 500",
"field": "limit"
}
GET /api/admin/users/{id}/jobs retourne le format étendu {user, data, pagination} qui combine la fiche utilisateur et la pagination des jobs.Rate limiting
Les routes /api/v1/erp* sont protégées par 3 fenêtres en cascade par rôle. Le compteur le plus bas qui dépasse renvoie 429.
| Rôle | Soft (par sec) | Hard (par min) | Daily |
|---|---|---|---|
api_client | 10 | 100 | 100 000 |
diagnostiqueur | 5 | 100 | 50 000 |
agence | 5 | 100 | 50 000 |
particulier | 1 | 30 | 1 000 |
admin | exempt sur les 3 fenêtres | ||
Headers émis sur toutes les requêtes /api/v1/erp* :
X-RateLimit-Limit: 100 X-RateLimit-Remaining: 87 X-RateLimit-Reset: 1730000060
X-RateLimit-Limit et X-RateLimit-Remaining reflètent la fenêtre hard (par minute). X-RateLimit-Reset est un timestamp Unix indiquant le reset de cette fenêtre.
Réponse 429 sur dépassement :
HTTP/1.1 429 Too Many Requests
Retry-After: 42
{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded for window soft",
"retry_after_seconds": 42,
"limit_type": "soft"
}
Un compte formateur reçoit aussi un 429 quand son quota mensuel de générations est atteint (POST /api/v1/erp et renouvellement). Les deux cas se distinguent par le corps : le quota répond "code": "formation_quota_exhausted" avec un objet quota (restant, plafond, periode) et sans en-tête Retry-After ; il se rouvre au premier jour du mois suivant. Le limiteur répond "error": "rate_limit_exceeded" avec retry_after_seconds et limit_type.
Bonnes pratiques côté client : utiliser un backoff exponentiel respectant Retry-After. Délais recommandés : 1s, 2s, 4s, 8s, 16s, plafonné à 60s. Inutile de réessayer plus tôt que retry_after_seconds, le compteur ne baisse pas avant ce délai.
Exports CSV admin
Les routes GET /api/admin/export/{users,orders,jobs} sont limitées indépendamment à 10 requêtes / minute et 100 requêtes / jour. Cette limite n'est pas exemptée pour les administrateurs (protection contre compte compromis ou script qui boucle).
Export CSV compte client
La route GET /api/account/export/jobs.csv est limitée à 5 requêtes / minute et 30 requêtes / jour par utilisateur. Quota suffisant pour un usage manuel ou une intégration CSV ponctuelle.
Dashboard consommation client
Deux endpoints sont exposés aux comptes api_client, diagnostiqueur, agence et admin. Les comptes particulier reçoivent 403.
Historique de consommation sur les 12 mois calendaires glissants Europe/Paris. Mois courant flaggé is_partial: true. Jobs sandbox exclus.
{
"period": { "from": "2025-06-01", "to": "2026-05-11", "timezone": "Europe/Paris" },
"months": [
{ "month": "2025-06", "jobs_completed": 0, "jobs_failed": 0, "is_partial": false },
...
{ "month": "2026-05", "jobs_completed": 12, "jobs_failed": 1, "is_partial": true }
],
"totals": { "jobs_completed": 142, "jobs_failed": 3 },
"scope": { "includes_sandbox": false, "includes_admin_jobs": false }
}
Export CSV (UTF-8 BOM, séparateur ;, 17 colonnes) des jobs du compte. Période max 36 mois, défaut 12 derniers mois glissants. Rate-limit dédié 5 req/min, 30 req/jour.
Paramètres optionnels :
since(YYYY-MM-DD) borne basse incluseuntil(YYYY-MM-DD) borne haute inclusestatusparmicompleted,failed,processing,queuedinclude_sandboxbooléen (true,false,1,0), défautfalse
Colonnes (dans l'ordre) :
job_id: UUID v4 du jobbatch_id: UUID du batch parent (vide si job unitaire)order_number: numéro séquentiel global, commun à tous les comptes (référence client, sans portée comptable)created_at_paris,completed_at_paris:YYYY-MM-DD HH:mm:ssEurope/Parisduration_ms: durée de pipeline en millisecondesstatus: statut public (completed,failed,processing,queued)address: adresse postale saisieparcelle: saisie utilisateur bruteparcelle_canonical: forme canoniquecommune-prefix-section-numeroparcelles_sup: parcelles supplémentaires pipe-séparées (exBA12|BA13)insee: code INSEE 5 chiffrestransaction_type:vente,location, ou vide pour les travaux anterieurs au lot P-10 dont le client n’a jamais exprime le typeis_sandbox: 0 ou 1is_formation: 0 ou 1 (job pédagogique, jamais un document commercial)error: message d'erreur tronqué à 200 caractères (uniquement si status=failed)rubriques_non_verifiees: codes des rubriques du formulaire non vérifiées à la livraison, séparés par|, dans l’ordre deunverifiedSectionsde l’interface JSON (exrtc|sis) ; vide pour un document non dégradé ou non achevé
# Historique consommation JSON curl -H "Authorization: Bearer $API_KEY" \ https://votre-domaine/api/account/usage-history # Export CSV 12 derniers mois (défaut) curl -H "Authorization: Bearer $API_KEY" \ https://votre-domaine/api/account/export/jobs.csv -o jobs.csv # Export CSV période et statut custom curl -H "Authorization: Bearer $API_KEY" \ "https://votre-domaine/api/account/export/jobs.csv?since=2025-01-01&until=2025-12-31&status=completed" \ -o jobs_2025.csv
const fs = require("node:fs/promises"); const headers = { Authorization: `Bearer ${process.env.API_KEY}` }; // Historique consommation JSON const history = await fetch("https://votre-domaine/api/account/usage-history", { headers }); // Export CSV 12 derniers mois (défaut) const csv = await fetch("https://votre-domaine/api/account/export/jobs.csv", { headers }); await fs.writeFile("jobs.csv", Buffer.from(await csv.arrayBuffer())); // Export CSV période et statut custom const csv2025 = await fetch( "https://votre-domaine/api/account/export/jobs.csv?since=2025-01-01&until=2025-12-31&status=completed", { headers } ); await fs.writeFile("jobs_2025.csv", Buffer.from(await csv2025.arrayBuffer()));
import os
import requests
headers = {"Authorization": f"Bearer {os.environ['API_KEY']}"}
# Historique consommation JSON
history = requests.get("https://votre-domaine/api/account/usage-history", headers=headers)
# Export CSV 12 derniers mois (défaut)
csv = requests.get("https://votre-domaine/api/account/export/jobs.csv", headers=headers)
open("jobs.csv", "wb").write(csv.content)
# Export CSV période et statut custom
csv_2025 = requests.get(
"https://votre-domaine/api/account/export/jobs.csv",
params={"since": "2025-01-01", "until": "2025-12-31", "status": "completed"},
headers=headers,
)
open("jobs_2025.csv", "wb").write(csv_2025.content)
Exemples de requête
# Lancer une generation (URL canonique /api/v1/) curl -X POST https://votre-domaine/api/v1/erp \ -H "Authorization: Bearer erpplus_votre_cle_api" \ -H "Content-Type: application/json" \ -d '{"address": "21 Avenue Jean Moulin 75014 Paris", "vendeur": "SCI Dupont"}' # Verifier le statut curl https://votre-domaine/api/v1/erp/{id}/status \ -H "Authorization: Bearer erpplus_votre_cle_api" # Telecharger le PDF curl -o erp.pdf https://votre-domaine/api/v1/erp/{id}/download \ -H "Authorization: Bearer erpplus_votre_cle_api" # Consulter les credits curl https://votre-domaine/api/auth/me \ -H "Authorization: Bearer erpplus_votre_cle_api"
const fs = require("node:fs/promises"); const headers = { Authorization: "Bearer erpplus_votre_cle_api" }; // Lancer une generation (URL canonique /api/v1/) const job = await fetch("https://votre-domaine/api/v1/erp", { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ address: "21 Avenue Jean Moulin 75014 Paris", vendeur: "SCI Dupont" }) }); // Verifier le statut const status = await fetch(`https://votre-domaine/api/v1/erp/${id}/status`, { headers }); // Telecharger le PDF const pdf = await fetch(`https://votre-domaine/api/v1/erp/${id}/download`, { headers }); await fs.writeFile("erp.pdf", Buffer.from(await pdf.arrayBuffer())); // Consulter les credits const me = await fetch("https://votre-domaine/api/auth/me", { headers });
import requests
headers = {"Authorization": "Bearer erpplus_votre_cle_api"}
# Lancer une generation (URL canonique /api/v1/)
job = requests.post(
"https://votre-domaine/api/v1/erp",
headers=headers,
json={"address": "21 Avenue Jean Moulin 75014 Paris", "vendeur": "SCI Dupont"},
)
# Verifier le statut
status = requests.get(f"https://votre-domaine/api/v1/erp/{id}/status", headers=headers)
# Telecharger le PDF
pdf = requests.get(f"https://votre-domaine/api/v1/erp/{id}/download", headers=headers)
open("erp.pdf", "wb").write(pdf.content)
# Consulter les credits
me = requests.get("https://votre-domaine/api/auth/me", headers=headers)
Credits
Chaque generation ERP consomme 1 credit. Chaque parcelle d'un batch consomme 1 credit.
Consultez votre solde via GET /api/auth/me (champ credits).