CompanyData — API
Service auto-hébergé d'enrichissement : vérification d'e-mail, qualification d'entreprise (par domaine ou SIREN), validation de téléphone, Email Finder, logo/avatar, et nettoyage de référentiel assisté par IA (Clean Review). Conçu pour fiabiliser vos envois, nettoyer vos listes et enrichir vos contacts — sans dépendance à un fournisseur SaaS tiers.
- Base URL :
https://companydata.scale2sell.company - Auth : en-tête
X-API-Key: <clé>(pages publiques sans clé :/,/health,/api-doc,/roadmap,/docs,/image/raw). - Format : JSON UTF-8, dates ISO 8601 UTC.
- Contrat machine :
GET /openapi.json· Swagger UI :/docs.
1. À quoi sert cette API — et pourquoi
Avant d'écrire à un contact ou de l'importer dans un CRM, on a besoin de savoir deux choses : l'adresse est-elle valide ? et quelle est l'entreprise derrière le domaine ? Les services du marché (Abstract, Clearbit, ZeroBounce…) répondent, mais :
- ils sont payants et externes : vos adresses (donnée personnelle) transitent chez un tiers ;
- ils sont opaques : un score sans explication ;
- ils créent une dépendance difficile à maîtriser (quota, prix, disponibilité).
CompanyData remplit le même rôle en interne, avec trois partis pris :
- Auto-hébergé, sans fournisseur tiers (la seule dépendance optionnelle est un LLM pour l'extraction difficile). Vos données ne sortent pas.
- Verdicts honnêtes et explicités : chaque réponse porte une raison et les signaux qui la fondent. On ne prétend jamais « délivrable » sans preuve.
- Zéro atteinte à la réputation d'envoi : le service n'envoie jamais de mail pour tester, et évite les sondes risquées (voir §4).
Ce qu'elle fait, concrètement
| Service | Question à laquelle il répond | Endpoint |
|---|---|---|
| Vérification d'e-mail | Cette adresse est-elle livrable ? risquée ? à jeter ? | GET /validate/email |
| Qualification d'entreprise | Qui est l'entreprise derrière ce domaine ? | GET /enrich |
| Image | Quel avatar/logo associer à ce contact/domaine ? | GET /image |
| Validation de téléphone | Ce numéro est-il valide ? mobile/fixe ? quel opérateur/pays ? | GET /validate/phone |
| Email Finder | Quel est l'e-mail pro probable de cette personne ? | GET /find/email |
| Enrichissement par SIREN | Quelle entreprise (+ domaine, logo) derrière ce SIREN ? | GET /enrich/siren |
| Clean Review | Détecter & corriger (IA) les incohérences du référentiel | POST /review/arbitrate |
Cas d'usage typiques : nettoyage de listes avant campagne, enrichissement CRM, validation de formulaire d'inscription, affichage d'un logo de société, normalisation de numéros de téléphone, résolution SIREN ↔ domaine.
2. Comment ça marche
2.1 Vérification d'e-mail — un pipeline en couches
L'adresse traverse 4 couches, de la moins chère à la plus coûteuse ; on s'arrête dès qu'une couche tranche :
- Syntaxe & normalisation — validité RFC, minuscule, domaine en IDNA/punycode.
Invalide →
invalid. - MX (DNS) — le domaine a-t-il des serveurs de messagerie ? Aucun MX →
undeliverable. On identifie au passage l'opérateur (mx_provider). - Classification — domaine jetable (liste embarquée), adresse générique/rôle
(
contact@,info@…), messagerie grand public. - Sonde SMTP (optionnelle,
smtp=true) — dialogueRCPT TOsans envoyer de mail, avec détection catch-all. Confirme (deliverable) ou infirme (undeliverable) la boîte.
Le résultat est un verdict scoré : status + score (0–100) + reason + signaux.
Identification de l'opérateur MX (mx_provider). On sait distinguer :
- les messageries (Google, Microsoft 365, Outlook grand public, Yahoo, iCloud,
Proton, Zoho, OVH, Gandi, Infomaniak, IONOS…) ;
- les passerelles anti-spam placées devant la vraie boîte (Mailinblack, Altospam,
Vade, MailCleaner, Proofpoint, Mimecast, Barracuda, Cisco IronPort, Trend Micro,
Symantec/MessageLabs, Hornetsecurity, SpamExperts, Libraesva, Retarus, SpamTitan…) ;
- les FAI français (Orange, Free, SFR, La Poste, Bouygues).
Cette distinction est décisive pour la fiabilité : voir §4.
Cache. Chaque verdict est mémorisé avec une date de péremption (expires_at) :
~30 jours pour un verdict stable (délivrable / sans MX / invalide), ~7 jours pour un
verdict incertain. Un nouvel appel resert le cache tant qu'il est frais ; refresh=true
force un recalcul.
2.2 Qualification d'entreprise — heuristique + registre + LLM
À partir d'un domaine (ou d'une URL/e-mail dont on extrait le domaine) :
- Lecture du site — page d'accueil + pages légales (mentions légales), avec repli HTTP et détection des pages « par défaut » (listing Apache, parking…).
- Extraction heuristique (hors ligne, déterministe) — JSON-LD, nom & baseline, adresse française décomposée (ville / code postal / pays), SIREN (validé par Luhn), e-mails, téléphones, personnes + rôles, réseaux sociaux (Twitter/X fusionnés), détection « à vendre ».
- Consolidation registre — via recherche-entreprises.api.gouv.fr (DINUM), par
SIREN ou par nom uniquement en présence d'un signal français (évite les
faux positifs sur des homonymes étrangers). Quand le SIREN n'a pas été trouvé sur
le site (recherche par nom, plus incertaine), le SIREN et la raison sociale ne
sont associés à la fiche que si l'adresse du registre est compatible avec celle du
site — même ville et même rue (numéro exclu), ou l'une incluse dans l'autre. En cas
d'incompatibilité (homonyme probable), le registre est écarté et la source
registry_rejected_addressest tracée dans le run. La provenance du SIREN/raison sociale (page du site vs fiche Annuaire des Entreprises) est conservée dans l'historique. - Repli LLM (optionnel,
llm=true) — meilleure lecture des pages difficiles, et repli « connaissance » quand le site est bloqué/injoignable ou trop maigre : le modèle identifie l'entreprise à partir du domaine (donnée tracée, non inventée). - Repli dichotomique de domaine — un sous-domaine stérile remonte vers le domaine
parent (
alias_of).
Normalisation d'adresse (BAN) : l'adresse scrapée est validée via la Base Adresse
Nationale (§3.10). Sur score fort (≥ 0.8, adresse précise), on adopte la ville / code
postal / citycode INSEE de la BAN (la voie scrapée est conservée avec ses accents) et
address_status="resolved". Sinon address_status="ambiguous" → une suggestion clean-review
« adresse à confirmer » est créée (on ne modifie rien tant qu'elle n'est pas validée).
Fusion non destructive : un rafraîchissement ne remplace jamais une donnée existante
par du vide ; les listes (contacts, réseaux…) sont unifiées. Péremption ~9 mois (infos
stables), raccourcie si données pauvres, DNS récemment modifié, ou domaine à vendre.
enrichment_level indique si un LLM est intervenu (basic vs llm).
2.3 Image — cascade avatar/logo
Pour une query (domaine ou e-mail), on cherche dans l'ordre, on s'arrête au premier hit : Gravatar (photo de personne, sur l'e-mail) → logo.dev (logo d'entreprise, sur le domaine) → BIMI (logo de marque publié en DNS) → favicon du site. Les octets sont téléchargés et mis en cache (la clé logo.dev n'est jamais exposée), avec péremption par source (Gravatar 2 mois, logo.dev/BIMI 3 mois, favicon 1 mois).
3. Référence des endpoints
Codes d'erreur transverses :
| Code | Signification |
|---|---|
| 401 | clé API absente ou incorrecte |
| 400 | requête invalide (domaine mal formé, batch > 50…) |
| 422 | e-mail grand public fourni là où un domaine d'entreprise est attendu |
3.1 GET /validate/email
| Param | Type | Défaut | Description |
|---|---|---|---|
email |
string | requis | adresse à vérifier |
refresh |
bool | false |
ignorer le cache |
smtp |
bool | false |
activer la sonde SMTP (RCPT TO, sans envoi) |
curl -H "X-API-Key: $KEY" \
"https://companydata.scale2sell.company/validate/email?email=jean@exemple.fr&smtp=true"
{
"email": "jean@exemple.fr",
"domain": "exemple.fr",
"status": "deliverable",
"score": 95,
"reason": "boite confirmee par le serveur SMTP",
"syntax_ok": true,
"has_mx": true,
"mx_hosts": ["mx1.exemple.fr"],
"mx_provider": "ovh",
"is_free": false,
"is_disposable": false,
"is_role": false,
"is_catch_all": false,
"smtp_checked": true,
"smtp_code": "250",
"expires_at": "2026-08-14T20:00:00Z",
"check_count": 1,
"from_database": false,
"image_url": "https://companydata.scale2sell.company/image/raw?query=jean%40exemple.fr",
"image_source": "gravatar",
"image_type": "person"
}
status et recommandation pour du nettoyage de liste :
status |
Sens | Action liste |
|---|---|---|
deliverable |
boîte confirmée (SMTP) | ✅ garder |
undeliverable |
pas de MX, ou boîte rejetée (550) | ❌ retirer |
invalid |
syntaxe invalide | ❌ retirer |
risky |
jetable, ou serveur catch-all | ⚠️ à revoir |
untestable |
MX présent, boîte non confirmée (opérateur non sondable) | ✅ garder par défaut |
Champs clés : score (0–100), mx_provider (voir §2.1), is_catch_all
(true/false/null), smtp_checked (la sonde a-t-elle réellement eu lieu),
is_disposable/is_role/is_free, image_* (voir §3.4).
3.2 POST /validate/batch
// requête (≤ 50) // réponse
{ "emails": ["a@x.fr"], { "results": [ <objet ci-dessus>, … ] }
"smtp": false }
3.3 GET /enrich
| Param | Type | Défaut | Description |
|---|---|---|---|
domain |
string | requis | domaine, URL ou e-mail |
refresh |
bool | false |
ignorer le cache |
llm |
bool | false |
activer l'extraction LLM |
curl -H "X-API-Key: $KEY" \
"https://companydata.scale2sell.company/enrich?domain=scale2sell.company"
{
"domain": "scale2sell.company",
"company_name": "Scale2Sell",
"baseline": "votre entreprise vaut plus que ce que vous croyez",
"legal_name": "SCALE2SELL SAS",
"siren": "912345678",
"address": "12 rue de la République 13001 Marseille",
"city": "Marseille", "postal_code": "13001", "country": "France",
"citycode": "13201", "address_status": "resolved", "address_score": 0.97,
"phone": "04 91 00 00 00", "phones": ["04 91 00 00 00"],
"emails": ["contact@scale2sell.company"],
"contacts": [{"name": "Jean Dupont", "role": "Président"}],
"description": "…",
"socials": {"linkedin": "https://www.linkedin.com/company/scale2sell"},
"linkedin_people": ["https://www.linkedin.com/in/jean-dupont"],
"linkedin_companies": [],
"registry": {"siren": "912345678", "legal_name": "SCALE2SELL SAS", "naf": "70.22Z",
"creation_date": "2018-03-01", "employees_range": "6 à 9 salariés"},
"finances": {"exercice": "2024-12-31", "ca": 302946, "resultat_net": 28299,
"ca_evolution_pct": 11.5, "tendance": "hausse",
"exercices": [{"annee": 2024, "ca": 302946, "resultat_net": 28299}]},
"sources": ["homepage", "legal_pages", "recherche_entreprises"],
"status": "enriched",
"enrichment_level": "basic",
"alias_of": null,
"expires_at": "2027-04-11T00:00:00Z",
"enrichment_count": 1,
"from_database": false,
"image_url": "https://companydata.scale2sell.company/image/raw?query=scale2sell.company",
"image_source": "logodev",
"image_type": "company"
}
Finances (finances, best-effort, clé par SIREN — source : ratios INPI/BCE via
data.economie.gouv.fr) : dernier exercice public connu (ca, resultat_net, exercice),
historique des 3 derniers exercices (exercices) et tendance de CA (tendance ∈
hausse|baisse|stable, ca_evolution_pct). null si l'entreprise ne dépose pas de comptes
(cas des entrepreneurs individuels / professions libérales). La date de création est dans
registry.creation_date. En cas d'échec transitoire (timeout de la source), un réessai
différé est programmé (~1 h) ; s'il aboutit, la fiche est mise à jour et un événement
company.updated est poussé aux webhooks (§3.8) — inutile de re-scraper.
Analyse technique (dans la réponse enrich) :
- tech_stack : [{name, category, evidence}] — CMS, framework, analytics, CDN, serveur… (détection statique HTML + en-têtes, sans rendu JS).
- hosting : {domain, name, asn, cdn} — hébergeur/CDN (DNS + ASN). Si le site est derrière un CDN, domain/name reflètent le CDN (origine masquée).
- agency : {domain, name, url} — agence créditée dans le pied de page / mentions légales (seulement si crédit explicite, sinon null).
Hébergeurs et agences sont aussi des entités référençables par leur domaine :
- GET /hosts · GET /hosts/{domaine} → l'hébergeur + les sites qu'il héberge.
- GET /agencies · GET /agencies/{domaine} → l'agence + les sites qu'elle a réalisés.
- Filtres croisés : GET /companies?hosting=<domaine> et ?agency=<domaine>.
Sémantique : status ∈ enriched | partial | unreachable ; enrichment_level ∈
basic | llm ; socials = dict plateforme→url (linkedin, facebook, instagram,
x, youtube, tiktok) ; contacts = [{name, role|null}] ; registry = données
registre FR ou null ; alias_of = domaine parent si alias ; query = echo de l'input
si e-mail/URL.
LinkedIn — socials.linkedin est le profil officiel de l'entreprise : la page
entreprise (/company/, /school/, /showcase/) la plus fréquente sur le site ; à défaut
(cabinet individuel sans page entreprise) le profil personnel (/in/, /pub/) le plus
fréquent. Deux champs complètent (rétrocompatibles, tableaux, vides par défaut) :
linkedin_people = profils personnels des associés/collaborateurs ; linkedin_companies =
pages entreprise supplémentaires (renseigné seulement si le site en cite plusieurs). Les URLs
sont canonicalisées (https://www.linkedin.com/<type>/<slug>, sans locale ni paramètres).
Entrées : une URL ou un e-mail sont acceptés (domaine extrait). E-mail grand
public → 422. Domaine mal formé → 400. POST /enrich/batch : { "domains": [...] } (≤ 50).
3.4 Image
Les réponses enrich/validate incluent (best-effort) :
| Champ | Description |
|---|---|
image_url |
URL absolue à mettre dans <img src> (ou null) |
image_source |
gravatar | logodev | bimi | favicon |
image_type |
person (avatar) | company (logo) |
GET /image/raw?query=<domaine|email>— public, renvoie les octets (image/*),404si absent.GET /image?query=<domaine|email>(authentifié) — résout + met en cache, renvoie les métadonnées.
3.5 Déclaration de bounces (adresse périmée)
Un bounce (rebond) est un signal négatif fiable : votre système d'envoi le
reçoit quand un message n'a pas pu être délivré. En le déclarant ici, l'adresse est
marquée périmée — verdict autoritaire qui prime sur toute revalidation future
(même une adresse Gmail « untestable » (ex-« unknown ») passe à undeliverable).
hard(permanent, ex. boîte inexistante550 5.1.1) → périme immédiatement.soft(temporaire, ex. boîte pleine) → comptabilisé ; périme au-delà d'un seuil (5).
curl -H "X-API-Key: $KEY" -H "Content-Type: application/json" -X POST \
https://companydata.scale2sell.company/bounces \
-d '{"email":"jean@exemple.fr","type":"hard","code":"550 5.1.1","source":"mailgun"}'
POST /bounces → renvoie la validation mise à jour (bounced: true, status: "undeliverable",
bounce_count, bounce_reason).
POST /bounces/batch → { "bounces": [ {email,type,…}, … ] } (≤ 500).
DELETE /bounces/{email} → annule le marquage (correction), force une revalidation.
Filtrer les adresses périmées : GET /validations?bounced=true.
Intégration type : brancher le webhook de bounce de votre outil d'envoi (ou un
traitement des NDR de votre boîte) sur POST /bounces. Les campagnes suivantes
consultent status/bounced et suppriment les périmées.
3.6 Déclaration de délivrabilité (engagement — miroir positif du bounce)
Symétrique du bounce : un engagement (ouverture ou clic) prouve que la boîte a accepté le message → l'adresse est marquée délivrable, verdict autoritaire positif qui prime sur une revalidation future (et se propage à tous les abonnés).
On notifie l'e-mail, l'heure d'envoi, le type d'événement (open/click) et
l'heure de l'événement :
curl -H "X-API-Key: $KEY" -H "Content-Type: application/json" -X POST \
https://companydata.scale2sell.company/deliverables \
-d '{"email":"jean@corp.com","event":"click",
"sent_at":"2026-07-16T09:00:00Z","event_at":"2026-07-16T11:30:00Z","source":"sendgrid"}'
Heuristique anti-spam : on calcule le délai event_at − sent_at. Un antispam ouvre/clique
en quelques secondes ; plus l'événement est éloigné de l'envoi, plus l'engagement est
probablement humain. Au-delà de engagement_human_delay_s (défaut 120 s) → confiance
maximale. La délivrabilité est confirmée dans tous les cas (le mail a bien été délivré), mais
le score est modulé :
| Événement | délai ≥ seuil (humain) | délai < seuil (possible bot) |
|---|---|---|
| clic | 99 | 90 |
| ouverture | 96 | 85 |
Résurrection : déclarer délivrable une adresse actuellement bounced la fait
« ressusciter » (bounced→false, resurrection_count += 1). Maximum 3 résurrections ;
au-delà, l'adresse est définitivement bounced et la déclaration est refusée (réponse
{"refused": true, …}, status reste undeliverable).
POST /deliverables→ validation mise à jour (deliverable_confirmed,engagement_event,engagement_delay_s,resurrection_count,status: "deliverable").POST /deliverables/batch→{ "deliverables": [ {email,event,sent_at,event_at}, … ] }(≤ 500).DELETE /deliverables/{email}→ annule la confirmation (force une revalidation ; le compteur de résurrections est conservé).
Intégration type : brancher les webhooks open/click de votre outil d'emailing
(SendGrid, Mailgun, Brevo…) sur POST /deliverables.
3.7 Édition manuelle (corriger une valeur scrapée)
Les données extraites automatiquement peuvent être corrigées à la main. Tout champ édité devient un « override » : il n'est plus jamais écrasé par un ré-enrichissement, et la modification est propagée aux webhooks (§3.8).
PATCH /companies/{domaine}— corps JSON avec les champs à modifier. Éditables :company_name, baseline, legal_name, siren, address, city, postal_code, country, phone, phones, emails, contacts, socials, linkedin_people, linkedin_companies, description, agency_name, agency_domain, hosting_name(+tags,notes). La réponse (CompanyDetail) inclutoverrides(liste des champs verrouillés). Émetcompany.updated.PATCH /validations/{email}—{status?, score?, reason?}. Verrouille ces champs. Émetemail.updated. ⚠️ Un bounce déclaré reste prioritaire sur un statut édité.
curl -H "X-API-Key: $KEY" -H "Content-Type: application/json" -X PATCH \
https://companydata.scale2sell.company/companies/exemple.fr \
-d '{"company_name":"Nom Corrigé","city":"Lyon"}'
Bloc registre (par SIREN). Deux gestes symétriques, hors du chemin de scraping :
POST /companies/{domaine}/registry/refresh[?siren=…]— recharge l'identité légale à partir du SIREN (raison sociale, siège, NAF, effectifs, dirigeants, TVA, finances) sans re-scraper le site. Sûr par construction : le registre exige la correspondance exacte du SIREN, aucun homonyme ne peut passer — là où l'étape registre du pipeline, qui cherche par nom faute de SIREN sur le site, se fait écarter par le garde-fou d'adresse (§2.2) et laisse le bloc vide.siren(9 ou 14 chiffres, SIRET toléré) rattache une personne morale quand la fiche n'en porte pas encore, ou en corrige une. C'est le seul chemin qui exploite un SIREN saisi à la main : le pipeline, lui, le redécouvre depuis le site à chaque passage. →422si aucun SIREN (ni en paramètre ni sur la fiche) ou mal formé,404si inconnu au registre.DELETE /companies/{domaine}/registry— videregistry,sirenetlegal_name(cas d'un SIREN détecté erroné).
Dans les deux cas ces trois champs sont verrouillés en override : un ré-enrichissement
ne défait pas la décision. Les deux émettent company.updated.
curl -H "X-API-Key: $KEY" -X POST \
"https://companydata.scale2sell.company/companies/exemple.fr/registry/refresh?siren=831823257"
3.8 Webhooks (propagation des modifications)
Vos applications s'abonnent ; à chaque modification d'une entreprise ou d'une
adresse, CompanyData pousse la donnée fraîche en POST signé. Ainsi, si vous
corrigez un nom ici, il se met à jour chez tous les abonnés ; si un e-mail bascule en
bounce, l'info est propagée à tous les clients.
Gérer les abonnements :
- POST /webhooks — { "url": "...", "events": ["*"], "secret"?: "...", "description"?: "..." }.
Le secret est généré s'il est absent (à conserver : il signe les payloads). Renvoie l'abonnement.
- GET /webhooks · PATCH /webhooks/{id} ({url?, events?, active?, description?}) · DELETE /webhooks/{id}.
- POST /webhooks/{id}/test — envoie un événement ping de test.
Événements : company.updated, email.updated, address.updated (ou ["*"] pour tout ; "company.*" supporté).
Requête reçue par votre endpoint :
POST /votre-webhook HTTP/1.1
Content-Type: application/json
X-CompanyData-Event: company.updated
X-CompanyData-Signature: sha256=3f9a… ← HMAC-SHA256(secret, corps brut)
{
"event": "company.updated",
"at": "2026-07-15T20:00:00Z",
"data": { "domain": "exemple.fr", "company_name": "Nom Corrigé", "city": "Lyon", ... }
}
datapourcompany.updated= la fiche entreprise (mêmes champs queGET /enrich, sansruns), à matcher pardomain.datapouremail.updated= la validation (mêmes champs queGET /validate/email), à matcher paremail; regarderstatus/bounced.datapouraddress.updated= la fiche adresse (mêmes champs queGET /normalise/address, §3.10), à matcher parkey(l'ID surrogate immuable). Émis quand la normalisation d'une adresse déjà connue change (résolution BAN tardive, correction, levée de doute) — c'est le canal qui rend les corrections d'adresse raccordables sans re-scraper.
Vérifier la signature (à faire systématiquement, sur le corps brut avant tout parsing) :
import hmac, hashlib
def verify(secret: str, raw_body: bytes, header_sig: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header_sig or "")
const crypto = require('crypto');
function verify(secret, rawBody, sig) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return sig && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}
Receveur minimal (FastAPI) :
@app.post("/webhook")
async def receive(request: Request):
raw = await request.body()
if not verify(SECRET, raw, request.headers.get("X-CompanyData-Signature", "")):
raise HTTPException(401, "signature invalide")
p = json.loads(raw)
if p["event"] == "company.updated":
upsert_company_by_domain(p["data"]) # met à jour votre base adhérents
elif p["event"] == "email.updated":
upsert_email(p["data"]) # p["data"]["bounced"], ["status"]…
elif p["event"] == "address.updated":
upsert_address_by_key(p["data"]) # join sur p["data"]["key"] (ID surrogate)
return {"ok": True} # répondre 2xx rapidement
Sémantique de livraison :
- Best-effort, concurrente, timeout ~5 s par abonné ; une modification n'échoue jamais
à cause d'un abonné KO (le compteur failure_count est exposé dans GET /webhooks).
- Pas de file de retry automatique pour l'instant : répondez vite en 2xx. Un même
événement peut être re-livré → traitez de façon idempotente (clé = domain/email/key,
en gardant le plus récent via at).
3.9 Import CSV — mode « scan » (détection dédupliquée en masse)
Deux modes d'import coexistent : le scan (ci-dessous) détecte et déduplique les e-mails/ domaines de tout le fichier ; l'import enrichi ligne à ligne (§3.11) mappe les colonnes et produit une sortie enrichie par ligne, exportable. Choisissez le scan pour dédoublonner un gros volume, l'enrichi pour repartir du fichier complet.
Envoyez un fichier CSV : le service détecte toutes les adresses e-mail et tous les
domaines présents (n'importe quelle colonne), en excluant les liens vers les réseaux
sociaux (linkedin.com, facebook.com, x.com…), les messageries grand public (gmail.com…)
et les domaines jetables. Les e-mails détectés sont validés (délivrabilité) et les domaines
(y compris celui de chaque e-mail professionnel) enrichis. Le traitement est asynchrone :
la création renvoie immédiatement, le travail se fait en tâche de fond, et l'IHM (onglet
« Imports CSV ») comme l'API permettent de suivre l'avancement.
POST /imports— le corps de la requête est le contenu CSV brut (Content-Type: text/csv). Paramètres de requête :filename(nom d'origine),smtp(bool, sonde SMTP à la validation, défautfalse),llm(bool, fallback LLM à l'enrichissement, défautfalse). Séparateur (,/;/tab) et encodage (UTF-8/Windows-1252) détectés automatiquement. Renvoie le job créé (id,status,emails_total,domains_total). Max 20 000 lignes.bash curl -X POST 'https://companydata.scale2sell.company/imports?filename=contacts.csv' \ -H 'X-API-Key: VOTRE_CLE' -H 'Content-Type: text/csv' --data-binary @contacts.csvGET /imports?page=&per_page=— liste des imports (récents d'abord) avec avancement (processed,stats).GET /imports/{id}?kind=&status=&page=&per_page=— le job + ses items détectés. Chaque item :{kind: email|domain, value, status, outcome}— pour un e-mailoutcome = {status, score, mx_provider}, pour un domaineoutcome = {status, company_name}. Filtreskind(email/domain) etstatus(pending/done/skipped/error).DELETE /imports/{id}— supprime l'import et ses items (les entreprises et validations sous-jacentes, elles, restent dans le référentiel).
3.10 Normalisation d'adresse (Base Adresse Nationale)
Service réutilisable de parsing + normalisation d'adresse, adossé à la BAN (API
Adresse, data.geopf.fr, gratuite et faisant autorité en France). Renvoie des composants
structurés, un score de confiance et une validation ville/CP. Repli sur un parseur FR
local si la BAN ne répond pas. Aucune exclusion : c'est un moteur générique (une adresse
d'hébergeur ou de médiateur est une adresse valide).
curl -H "X-API-Key: $KEY" \
"https://companydata.scale2sell.company/normalise/address?q=19+rue+de+la+republique+BP+418+13177+Marseille+Cedex+20"
{
"key": "b3f1c2a4d5e6478091a2b3c4d5e6f708",
"raw": "19 rue de la republique BP 418 13177 Marseille Cedex 20",
"raw_key": "19 rue de la republique bp 418 13177 marseille cedex 20",
"complement": "BP 418 · Cedex 20",
"status": "resolved",
"source": "ban",
"housenumber": "19", "street": "Rue de la République",
"postcode": "13002", "citycode": "13202", "city": "Marseille", "country": "France",
"score": 0.97, "ban_type": "housenumber",
"lat": 43.30, "lon": 5.37,
"city_matches_postal": true,
"alias_of": null,
"request_count": 1, "last_request_at": "2026-07-22T09:00:00Z",
"from_cache": false
}
Gating par confiance (status) : resolved si score ≥ 0.8 et ban_type=housenumber
(adresse précise) ; sinon ambiguous (à valider — ex. « rue de la paix » sans numéro sort à
score élevé mais type=street) ; parsed_local si repli parseur FR ; not_found sinon.
ID surrogate opaque (key) — ce que le consommateur stocke et joint, sans jamais le
recalculer. C'est un UUID opaque (32 hex), sans lien dérivable avec le contenu, attribué
une seule fois à la création de la fiche et immuable : une correction, une résolution BAN
tardive ou une levée de doute ne le change jamais. Ainsi une mise à jour (address.updated,
§3.8) reste toujours raccordable côté consommateur. Deux bruts qui résolvent à la même identité
(même id BAN, ou mêmes composants) partagent le même key — les variantes « … CS 80336 » /
sans CS, « … Cedex » / sans Cedex dédupliquent et le doublon est aussi lié en alias_of.
(raw_key = forme canonique du brut, interne au cache ; ne pas joindre dessus.)
Compléments d'adresse (complement) — les mentions hors géocodage (2ème étage, Bât C,
Apt 4, Escalier B, Porte 3, RDC, et les codes de distribution BP / CS / TSA /
CEDEX) sont séparées dans complement et retirées avant l'appel BAN (elles bruitent le
score). Elles ne sont jamais jetées (critiques pour la livraison postale) : conservez street
et complement pour une adresse d'envoi complète.
Levée de doute en cascade — si le géocodage du brut ne tranche pas franchement, le service
nettoie l'adresse (compléments retirés) et re-géocode :
- deux formulations → même id BAN ⇒ doute levé → resolved (corroboration) ;
- deux formulations → id différents ⇒ ambiguous (revue) ;
- le brut ne donnait rien de fiable, le nettoyé résout franchement ⇒ adopté (resolved).
Sur resolved, la BAN renvoie le vrai CP/citycode (ex. le Cedex 13177 devient 13002 /
13202).
Complétion au niveau rue — quand la BAN trouve la voie mais pas le numéro (réponse
type=street) et que la saisie contenait un numéro, le service pose le numéro détecté sur la
voie officielle BAN (nom complet + CP/citycode/coordonnées) → resolved. Il ne le fait que si
la voie BAN est compatible avec la voie saisie (« Avenue Saint Just » ↔ « Avenue Louis Antoine
Saint Just » : oui ; « Rue Jean Moulin » ↔ « Chemin du Moulin Rose » : non — reste ambiguous).
Traçage (request_count, last_request_at) — chaque appel sur une adresse est compté
(y compris les hits de cache), pour une facturation future du service.
GET /normalise/address?q=…— une adresse (curl-friendly).POST /normalise/address—{ "text": "…" }.POST /normalise/address/batch—{ "items": [ "…", … ] }(≤ 1000), via l'endpoint CSV de la BAN (une requête pour N adresses ; pas de cascade sur ce chemin — un seul géocodage par adresse).GET /normalise/addresses?status=&page=&per_page=— consulter le cache (filtrestatus).PATCH /normalise/address/{key}— édition manuelle (jumeau dePATCH /companies/{domain}, §3.7). Corps = champs à corriger parmihousenumber, street, complement, postcode, citycode, city, country, status. Les champs fournis deviennent des overrides verrouillés (exposés dansoverrides) : la cascade BAN ne les réécrit plus jamais. L'édition tranche l'adresse →resolved(saufstatusfourni), vaut pour toutes ses variantes, et émetaddress.updated.POST /normalise/address/{key}/confirm— valider une adresseambiguous→resolved(verrouillé), sans rien corriger (key= l'ID surrogate, la clé brute, ou l'adresse brute ; vaut pour toutes ses variantes).- Statut
ambiguous: on NE remplace PAS votre adresse par le point BAN douteux. La fiche garde l'adresse fournie (parsée) ; le point BAN incertain devient une proposition, stockée dans la suggestion de revue (current_value= votre adresse,suggested_value= la proposition BAN). C'est seulement enresolved(score fort +housenumber) que la BAN est adoptée d'office. - File de revue unifiée — une adresse
ambiguousremonte comme suggestion clean-review (§ Revue), à côté des entreprises (subject_type = "address",domain = "addr:<key>",rule = address_ambiguous). Quatre issues : POST /review/suggestions/{id}/accept?prefer=correction— garde votre voie + numéro, adopte le CP / ville / citycode de la BAN (corrige la cohérence postale) ; défaut ;…/accept?prefer=ban— adopte tout le point BAN (rue comprise) ;…/accept?prefer=keep(ouPOST /normalise/address/{key}/confirm) — conserve votre adresse inchangée →resolved;- corriger via le
PATCHci-dessus. Chaque issue verrouille les champs concernés (la BAN ne les réécrit plus) et émetaddress.updated. - Abonnez-vous à
address.updated(§3.8) pour recevoir les corrections d'adresse :data= la fiche ci-dessus, à joindre surkey.
3.11 Import enrichi ligne à ligne (mapping de colonnes + région)
Repart du fichier complet : on mappe les colonnes (le service propose un rôle par colonne, vous ajustez), puis chaque ligne ressort avec un verdict de délivrabilité sur son e-mail, un enrichissement entreprise, et son rattachement géographique (région + département). Résultat exportable en CSV (fichier d'origine + colonnes ajoutées).
Le flux est en deux temps (aperçu/confirmation, puis traitement) :
1. Aperçu + pré-matching — POST /imports/preview
- Corps = CSV brut (Content-Type: text/csv). Param filename. Max 20 000 lignes.
- La 1ʳᵉ ligne est l'en-tête. Le service déduit, par colonne, un type de champ et une
famille de traitement (modèle à 2 axes), et ne traite rien (statut draft).
- Type de champ (~23, groupés) : contact (email, phone, phone_mobile, phone_fixe) ·
entreprise (website, siren, siret, vat, naf, company_name) · personne (full_name,
first_name, last_name, civility, title) · adresse (address_full, address,
address_complement, postal_code, city, country) · divers (keep). full_name repère un
nom non séparé (« Nom complet », « Contact », « DUPONT Jean »).
- Famille de traitement (treatment) — ce qu'on fait de la colonne :
- 🔑 driver (entrée de travail) : déclenche un enrichissement externe → email
(délivrabilité), website/siren/siret (entreprise via site ou registre) ;
- 🧹 normalize (à traiter) : structure la valeur en place → full_name/title
(civilité §6), address*/postal_code/city (BAN §3.10), phone* (E.164), siren/siret/vat (validation) ;
- 📦 keep : conservée telle quelle.
Le treatment proposé = la famille du type ; surchargeable par colonne (ex. désactiver un
enrichissement coûteux en passant une colonne driver à keep).
- Réponse : { job, columns[], sample[], roles[] } où chaque colonne =
{index, header, role, group, family, treatment, source, confidence} et roles[] = le
catalogue {role, group, family, label}.
bash
curl -X POST 'https://companydata.scale2sell.company/imports/preview?filename=contacts.csv' \
-H 'X-API-Key: VOTRE_CLE' -H 'Content-Type: text/csv' --data-binary @contacts.csv
2. Lancement — POST /imports/{id}/run
- Corps JSON : { "mapping": [ {"index":0,"role":"email","treatment":"driver"}, … ], "smtp": true,
"llm": false }. Le mapping est celui confirmé ; treatment optionnel (défaut = famille du
rôle). smtp défaut true (délivrabilité).
- Aucune entrée n'est obligatoire : un import peut ne faire que normaliser (séparer des noms,
formater des téléphones) sans aucun enrichissement externe. L'import passe pending →
running → done. Relançable après un failed.
bash
curl -X POST 'https://companydata.scale2sell.company/imports/ID/run' \
-H 'X-API-Key: VOTRE_CLE' -H 'Content-Type: application/json' \
-d '{"mapping":[{"index":3,"role":"email"},{"index":4,"role":"website"},{"index":5,"role":"postal_code"}]}'
Traitement par ligne — pour chaque ligne : l'e-mail mappé est validé (§3.1, sonde SMTP
selon smtp) ; le domaine (colonne website, sinon le domaine de l'e-mail pro) est
enrichi (§3.3) ; la région/département est dérivée du code INSEE de l'entreprise
enrichie (le plus fiable — Corse 2A/2B, outre-mer), avec repli sur le code postal (colonne
postal_code du fichier si l'entreprise n'a pas été enrichie). Ainsi chaque ligne porte un
rattachement, même sans domaine d'entreprise.
Civilité (service §6) — si une colonne de nom non séparé (full_name) est détectée — ou
un seul champ nom contenant les deux —, la ligne est séparée de force en first_name /
last_name (avec détection d'inversion « NOM Prénom »), et le sexe est inféré (INSEE). Si une
colonne titre/fonction (title) est présente, une salutation intelligente est ajoutée en
plus de la salutation simple (« Avocate » → Maître, « Présidente » → Madame la Présidente).
Un prénom épicène (Dominique…) ressort UNDETERMINED sans salutation (à valider), jamais deviné.
Consultation & export
- GET /imports/{id}/rows?status=®ion=&page=&per_page= — les lignes traitées. Chaque ligne :
le socle typé (email, email_status, company_name, siren, city, postal_code, citycode, enriched,
first_name, last_name, gender, salutation, smart_salutation, region, department…) plus
outputs (bloc extensible des normaliseurs : phone* {value E.164, national, type, status},
adresse {value, street, postcode, city, citycode, complement, status resolved/ambiguous,
key}, siren/siret/tva {value, status}) et to_validate (nb de cellules à valider — F5).
- GET /imports/{id}/export.csv — fichier d'origine + colonnes ajoutées (prénom, nom, sexe,
salutation, salutation_intelligente, téléphone_e164, téléphone_type, adresse_normalisée,
adresse_statut, email_validé, email_statut, email_score, entreprise, siren, ville, code_postal,
région, département, département_code, enrichi, à_valider). UTF-8 + BOM (Excel).
- GET /imports liste les deux modes (mode = scan|rows) ; DELETE /imports/{id} supprime
l'import et ses lignes (entreprises/validations sous-jacentes conservées).
Complétude : à la fin, chaque ligne a un verdict e-mail (
deliverable/risky/undeliverable/untestable— rappel §4 : Gmail/Orange & co restentuntestablepar nature) et, si un domaine d'entreprise était présent, un enrichissement (enriched=true). Une ligne sans domaine pro (e-mail grand public seul) resteenriched=falsemais peut porter une région via son code postal.
3.12 GET /validate/phone — validation de téléphone (international)
Validation hors-ligne via libphonenumber (données Google embarquées, déterministe, tous pays, sans coût ni réseau) : validité, format E.164 / national / international, type de ligne, pays, indicatif, localisation, fuseau(x) horaire(s) et opérateur d'origine.
| Param | Type | Défaut | Description |
|---|---|---|---|
number |
string | requis | numéro (national ou +E.164) |
region |
string | FR |
pays par défaut si le numéro n'a pas d'indicatif |
hlr |
bool | false |
dip HLR/SS7 opt-in : opérateur actuel (portage) + état live de la ligne |
refresh |
bool | false |
ignorer le cache HLR |
curl -H "X-API-Key: $KEY" \
"https://companydata.scale2sell.company/validate/phone?number=06%2012%2034%2056%2078"
{
"input": "06 12 34 56 78", "valid": true, "possible": true,
"e164": "+33612345678", "national": "06 12 34 56 78", "international": "+33 6 12 34 56 78",
"type": "mobile", "country": "FR", "country_code": 33,
"location": "France", "timezones": ["Europe/Paris"], "carrier": "Orange France"
}
type ∈ mobile | fixe | fixe_ou_mobile | voip | gratuit | surtaxé | coût_partagé | personnel |
pager | uan | messagerie | unknown. Un numéro invalide est signalé (valid:false), jamais
« corrigé » au hasard.
Couche HLR (hlr=true, opt-in). Le carrier gratuit est l'opérateur d'origine (bloc de
numérotation) — faux si le numéro a été porté. Avec hlr=true, un dip IPQualityScore ajoute un
bloc hlr avec l'opérateur servant réel (dérivé du MCC/MNC, portage inclus, table mondiale
238 pays) et l'état de la ligne :
"hlr": {"available": true, "current_carrier": "SFR", "allocation_carrier": "Orange France",
"ported": true, "active": true, "active_status": "Active Line", "mcc": "208", "mnc": "10",
"cached": false, "source": "ipqs"}
Dormant si non configuré (available:false, reason), mis en cache 90 j (jamais re-payé),
aucun appel payant sur un numéro invalide.
POST /validate/phone/batch : { "items": [{number, region?}], "region"?, "hlr"? } (≤ 1000 ;
≤ 100 avec hlr).
3.13 GET /find/email — Email Finder (e-mail pro d'un contact)
Devine l'e-mail professionnel le plus probable d'une personne à partir de nom + domaine, avec une confiance explicite (jamais « délivrable » sans preuve).
| Param | Type | Défaut | Description |
|---|---|---|---|
domain |
string | requis | domaine de l'entreprise |
name |
string | — | nom complet (Jean Dupont) ou… |
first_name/last_name |
string | — | …champs séparés |
smtp |
bool | false |
vérifier en SMTP (RCPT, sans envoi) |
curl -H "X-API-Key: $KEY" \
"https://companydata.scale2sell.company/find/email?name=Jean%20Dupont&domain=acme.fr"
{
"found": true, "email": "jean.dupont@acme.fr", "confidence": "verified", "method": "feedback",
"pattern": "first.last", "provider_kind": "workspace", "has_mx": true, "mx_provider": "google",
"first_name": "Jean", "last_name": "Dupont", "domain": "acme.fr",
"alternatives": [{"pattern": "flast", "email": "jdupont@acme.fr"}]
}
Confiance (confidence) : verified (prouvé — soit un engagement réel remonté par la
boucle bounce/deliverable, soit une sonde SMTP concluante) > pattern (correspond au motif
appris du domaine) > guess. Un candidat déjà bouncé est écarté ; un candidat déjà
délivrable confirmé est verified sans sonde — seul moyen fiable pour Gmail / Google
Workspace (provider_kind distingue la boîte grand public du domaine d'entreprise). On ne
fabrique jamais d'envoi pour vérifier.
GET /companies/{domain}/email-pattern→ motif d'e-mail appris pour un domaine (déduit des couples e-mail↔contact connus).POST /find/email/batch:{ "items": [{domain, name | first_name+last_name}], "smtp"? }(≤ 200).
3.14 GET /enrich/siren — enrichissement par SIREN/SIRET
Entrée = SIREN (9) ou SIRET (14, tronqué au siège). Renvoie l'identité légale (registre
DINUM : raison sociale, adresse siège, NAF, effectif, création, dirigeants, finances, TVA,
état actif/radié) et, via un pont SIREN → domaine, le logo puis la fiche complète en option.
(Le sens inverse — domaine → SIREN — se fait via GET /enrich?domain=.)
| Param | Type | Défaut | Description |
|---|---|---|---|
siren |
string | requis | SIREN (9) ou SIRET (14) |
domain_hint |
string | — | domaine connu ; validé avant adoption |
resolve_domain |
enum | referential |
referential (rapide) · web (recherche, plus lent) · off |
logo |
bool | true |
résoudre le logo si un domaine est trouvé |
full |
bool | false |
enrichissement complet du domaine (écrit le référentiel) |
reinject |
bool | false |
(mode web) enrichir en fiches les domaines découverts absents de la base |
refresh |
bool | false |
ignorer le cache |
curl -H "X-API-Key: $KEY" \
"https://companydata.scale2sell.company/enrich/siren?siren=882000995&resolve_domain=web"
{
"siren": "882000995", "active": true,
"identity": {"legal_name": "SAINT ELOI HOLDING", "naf": "70.10Z", "city": "MARSEILLE",
"dirigeants": [{"name": "…", "role": "Président"}], "finances": {"…": "…"}},
"registry_url": "https://annuaire-entreprises.data.gouv.fr/entreprise/882000995",
"domain": "scale2sell.company", "domain_source": "referential", "domain_confidence": "high",
"domains": [
{"domain": "scale2sell.company", "source": "referential", "confidence": "high"},
{"domain": "we-upgrade.fr", "source": "search", "confidence": "medium"}
],
"image_url": "…", "image_source": "logodev", "candidates": [], "reinjected": []
}
Cascade de résolution du domaine. Le mode web fusionne toutes les sources : référentiel
(fiche déjà enrichie, high) + domain_hint validé (site_siren == siren) + recherche
« grounded » (métamoteur SearXNG auto-hébergé + le LLM identifie le domaine officiel parmi de
vrais résultats — anti-hallucination ; ex. OVH → ovhcloud.com) + repli slugs. Renvoie une
liste classée domains: [{domain, source, confidence}] (plusieurs domaines possibles — ex.
marque.fr + marque.com, marques d'un groupe) ; domain = tête de liste. Confiance : SIREN prouvé
par le site → high, choix LLM grounded → medium. domain:null (holding sans site) = réponse
nominale, pas une erreur. Le mode referential (défaut) reste un court-circuit rapide.
Réinjection (reinject=true, mode web) : les domaines découverts absents du référentiel sont
enrichis en fiches (les marques manquantes du groupe entrent dans la base) ; reinjected liste les
domaines créés.
3.15 Clean Review — nettoyage du référentiel (règles + arbitres IA)
Un moteur de règles détecte les incohérences des fiches (nom / SIREN / adresse / doublons / réseaux…), et des arbitres LLM (Sonnet) tranchent les cas ambigus : auto-application si la confiance est haute et l'action réversible, sinon reco pré-remplie (★ IA) pour un clic humain.
Scan & suggestions
- POST /review/scan?mode=incremental|full — (re)calcule les alertes du référentiel.
- GET /review/suggestions?status=&rule=&per_page= — liste les alertes (pending/accepted/
rejected/stale/obsolete).
- POST /review/suggestions/{id}/accept · /reject — trancher manuellement.
Arbitres IA (chacun : auto=true applique si confiance haute + réversible ; background=true
pour vider le stock) :
- POST /review/arbitrate — dispatcher unifié : arbitre en un appel adresse (+ normalisation)
+ SIREN + doublons.
- POST /companies/{domain}/siren/arbitrate · POST /review/siren/arbitrate — SIREN mal
attribué : compare l'identité de la fiche au nom légal du SIREN au registre → clear (vide
le SIREN faux, réversible) si l'entreprise du registre est sans rapport (homonyme / erreur), sinon
keep. Distingue « marque ≠ raison sociale » (normal) d'une vraie erreur.
- POST /review/duplicate/arbitrate — doublons (duplicate_name/duplicate_company) : même
entité (reco fusion + domaine canonique) vs homonymes distincts (classe l'alerte faux positif).
- POST /companies/{domain}/address/arbitrate · POST /review/address/arbitrate — adresse :
tranche site / registre / BAN à partir du texte du site ; POST /review/address/normalize-arbitrate
pour la file de normalisation BAN.
Chaque arbitrage trace sa décision (choix, confiance, rationale, modèle) sur l'alerte, pour audit et pré-remplissage de la revue.
4. Limites assumées (à connaître avant d'intégrer)
Ces limites sont volontaires : on préfère un untestable honnête à un faux deliverable.
- Google (Gmail/Workspace), Outlook/Hotmail grand public, Yahoo, iCloud, Proton :
non vérifiables en SMTP (ils acceptent tout / anti-abus). Verdict
untestable. Aucun outil sans envoi ne fait mieux. - Passerelles anti-spam (Mailinblack & co) : non sondées — elles greylistent les
sondes (anti-reconnaissance) et probing = risque pour votre réputation. La vraie boîte
est derrière ; verdict
untestable. - FAI français : idem, grand public non fiable.
untestable. - Microsoft 365 (entreprise) : sondé — il rejette souvent les inconnus (
550 5.1.10), donc exploitable. - On ne peut jamais garantir une boîte sans envoyer. Pour une certitude absolue : double opt-in (hors périmètre de ce service).
- Logo/avatar : couverture partielle (Gravatar surtout tech ; logo.dev/BIMI selon le domaine).
En pratique : pour du nettoyage, on retire les invalid/undeliverable/jetables (fiable
à 100 %) et on garde les untestable grand public (statistiquement réels).
5. Intégration
- Clé API côté serveur uniquement (jamais exposée côté client public).
- Par contact :
GET /validate/email(délivrabilité) etGET /enrichsur le domaine (identité + logo). Les deux mettent en cache. - Piloter sur
status/score(validation) etstatus/enrichment_level(enrichissement). - Afficher
image_urltelle quelle (URL absolue, endpoint public sans clé). - Respecter le cache : pas de
refresh=trueen boucle ; utiliser les endpointsbatch(≤ 50) pour du volume. - Générer un client depuis
GET /openapi.json(toujours à jour).
6. Service « Civilité & Identité » (/v1)
Service déterministe d'identité civile pour la personnalisation des communications : à partir
des données d'un contact — même quand nom et prénom ne sont pas séparés — il retourne prénom/nom
séparés, sexe estimé (INSEE), salutation simple et intelligente (par workspace). Aucun LLM dans
le chemin nominal (réservé aux titres rares, §6.5). Chaque valeur porte source/confidence, et la
referentialVersion du fichier prénoms.
6.1 POST /v1/civility — un contact
Corps : fullName ou firstName/lastName, plus optionnels email, jobTitle, salutation,
attributes (champs custom du workspace), workspaceId, configOverride (config inline).
curl -X POST -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{"workspaceId":"ws_123","fullName":"DUPONT-MARTIN Marie","jobTitle":"Avocate associée","attributes":{"is_avocat":"oui"}}' \
https://companydata.scale2sell.company/v1/civility
{
"firstName": "Marie", "lastName": "Dupont-Martin",
"detectedOrder": "NOM_PRENOM", "method": "CASING", "nameConfidence": 0.9,
"detectedTitle": null,
"gender": { "value": "FEMALE", "pFemale": 0.99, "confidence": 0.98, "source": "INFERE", "needsValidation": false },
"salutation": "Madame", "smartSalutation": "Maître",
"needsValidation": null, "referentialVersion": "insee-2024"
}
- Séparation (F1) —
method∈COMMA | CASING | INSEE | HEURISTIC | SINGLE_TOKEN | EMAIL. Gère « Jean DUPONT » et « DUPONT Jean » (casse), « Dupont, Jean » (virgule), particules (« Jean de La Fontaine »), prénoms/noms composés, dérivation depuis un e-mail.detectedOrder=PRENOM_NOM | NOM_PRENOM | INDETERMINE. - Sexe (F2) — inféré du prénom via le Fichier des prénoms de l'INSEE ;
confidence = |P(féminin) − 0,5| × 2.< 0,20(épicène) →UNDETERMINED;< 0,90→ hypothèse à valider. - Salutation (F3/F4) —
salutation= « Monsieur »/« Madame » (vide si sexe indéterminé) ;smartSalutation= 1ʳᵉ règle applicable : champ custom workspace > titre détecté (fonction/civilité) > salutation explicite > défaut selon le sexe. - Incertitude (F5) — jamais de devinette silencieuse : bloc
needsValidationstructuré (field=name|gender, message chiffré,options). La valeur confirmée revient ensource = CONFIRMEet déclenche un recalcul côté appelant.
6.2 POST /v1/civility/batch — lot (≤ 5000)
{ "workspaceId"?, "configOverride"?, "contacts": [ {…}, … ] } → { count, results[], referentialVersion }.
Une seule résolution de config pour tout le lot (un contact peut porter son propre configOverride).
6.3 PUT /v1/workspaces/{workspaceId}/salutation-config — config workspace
Pilote la salutation intelligente sans redéploiement, cloisonnée par workspace.
{
"rules": [
{ "fields": ["is_avocat"], "form": { "neutre": "Maître" } },
{ "field": "grade", "equals": "colonel", "form": { "homme": "Mon Colonel", "femme": "Mon Colonel" } }
],
"titleOverrides": { "colonel": { "homme": "Mon Colonel", "femme": "Mon Colonel" } }
}
- Règle :
fields/field+ condition (truthypar défaut — vrai = oui/true/1/o/vrai — ouequals) →form { homme, femme, neutre }(neutre l'emporte quel que soit le sexe). Priorité = ordre déclaré.titleOverridesétend/surcharge la table des titres protocolaires. GETcorrespondant renvoie la config (404si absente).- Mode inline : passer
configOverridedans l'appel/v1/civility(test/prévisualisation, non persisté). Isolation stricte : aucune règle ne fuit d'un workspace à l'autre.
6.4 Non-écrasement des sources
Chaque valeur porte une source ordonnée : DECLARE (saisi humain) > CONFIRME (validé) >
INFERE (calculé). Le service ne propose jamais d'écraser une source plus forte.
6.5 POST /v1/civility/suggest — titres rares (optionnel)
Proposition à la demande pour une fonction rare (jobTitle), via modèle de langage :
best-effort, jamais automatique, jamais pour le sexe (si le sexe est inconnu, renvoie
les deux formes en alternatives). Peut renvoyer { "suggestion": null, "alternatives": [] }.
6.6 Référentiel INSEE
Table prénom → P(féminin) construite depuis le Fichier des prénoms de l'INSEE (open data,
Licence Ouverte/Etalab) : national, toutes années, plancher d'effectif 100, ~11 600 prénoms.
Rafraîchissable indépendamment du code via scripts/build_prenoms_insee.py ; version exposée
dans chaque réponse (referentialVersion).