{"openapi":"3.1.0","info":{"title":"Listar API","version":"1.0.0","description":"# API Listar\n\nDonne des informations de contact (téléphone, email, profils sociaux…) sur une\npersonne, à partir du peu que vous connaissez d'elle (nom, email, slug LinkedIn,\ntéléphone…).\n\n## Authentification\n\nToutes les requêtes s'authentifient avec une **clé API** dans l'en-tête :\n\n```\nAuthorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxx\n```\n\nLa clé est créée via `POST /api-keys` (ou fournie par l'équipe Listar). Elle est\naffichée **une seule fois** - stockez-la en lieu sûr.\n\n## Démarrer un test\n\n1. L'équipe Listar crée votre compte et **crédite votre solde de test**.\n2. Vous récupérez une clé API.\n3. Vous appelez `POST /search/unified`. Quand des données facturables sont\n   trouvées, elles sont **payées automatiquement avec votre crédit** et\n   renvoyées directement.\n\nVérifiez votre solde à tout moment via `GET /credit/balance`.\n\n## Enrichissement asynchrone ⏱️\n\nC'est le point clé. Un enrichissement n'est **pas toujours instantané** : selon\nles cas, des données complémentaires sont récupérées en arrière-plan. La réponse\nde `POST /search/unified` peut donc être immédiate **ou** différée :\n\n```\nPOST /search/unified\n   │\n   ├─ 200  Résultat disponible immédiatement\n   │        (payé automatiquement par crédit, ou résultat gratuit)\n   │\n   ├─ 402  Résultat prêt mais paiement requis\n   │        → POST /search/pay-with-credit { id } → 200 avec le résultat\n   │\n   └─ 202  Enrichissement EN COURS (réponse { id, ... })\n            Deux façons de récupérer le résultat final :\n\n            ┌─ Mode A - WEBHOOK (recommandé) ─────────────────────────────┐\n            │ Passez \"callbackUrl\" dans le corps de POST /search/unified. │\n            │ Dès que c'est prêt, on POST l'évènement \"enrichment.completed\" │\n            │ sur cette URL (signé HMAC). Aucun polling nécessaire.        │\n            └─────────────────────────────────────────────────────────────┘\n\n            ┌─ Mode B - POLLING (fallback) ───────────────────────────────┐\n            │ Appelez GET /search/results/{id} toutes les ~3-5 s :         │\n            │   202 → encore en cours, on re-poll                          │\n            │   425 → prêt, paiement en cours de résolution, on réessaie   │\n            │   402 → prêt, paiement requis                                │\n            │   200 → résultat complet                                     │\n            └─────────────────────────────────────────────────────────────┘\n```\n\n### Webhook - payload & signature\n\nOn POST sur votre `callbackUrl` (https obligatoire) un corps JSON\n`enrichment.completed` (voir la section **Webhooks**). Chaque requête porte\nun en-tête de signature, style Stripe :\n\n```\nX-Listar-Signature: t=<timestamp>,v1=<hmac_sha256>\n```\n\nVérification (récupérez / régénérez votre secret dans l'app - Paramètres →\nClés API, section Webhook - ou via `GET /webhooks/signing-secret`) :\n\n```js\nconst crypto = require('crypto');\nfunction verify(rawBody, header, secret) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));\n  if (!parts.v1) return false;\n  const expected = crypto\n    .createHmac('sha256', secret)\n    .update(parts.t + '.' + rawBody)\n    .digest('hex');\n  const a = Buffer.from(expected);\n  const b = Buffer.from(parts.v1);\n  return a.length === b.length && crypto.timingSafeEqual(a, b);\n}\n```\n\nRépondez `2xx` pour acquitter. En cas d'échec de livraison, le polling reste\ndisponible en fallback. Un même événement peut arriver plus d'une fois (par\nexemple après un redémarrage de notre côté) : dédupliquez sur son `id`.\n\n## Enrichissement en masse 📦\n\nPour enrichir plusieurs contacts en un seul appel, utilisez `POST /search/bulk`\n(jusqu'à **10 000 contacts** par requête). Vous recevez immédiatement un `202`\navec un `id` de batch ; les contacts sont enrichis en arrière-plan. À la fin du\nbatch, on POST **une seule fois** l'évènement `batch_enrichment.completed` sur\nvotre `callbackUrl` (même signature HMAC que ci-dessus), avec le résultat complet\nembarqué (`results[]`, un objet par contact). Aucun polling nécessaire.\nFacturé à la donnée trouvée, débité automatiquement de votre crédit à la fin.\n\n## Connecteur Claude (MCP) 🔌\n\nListar est disponible comme serveur MCP distant à l'adresse\n`https://api.listar.fr/mcp` : Claude (et tout client MCP) peut chercher les\ndirigeants d'une entreprise et enrichir des contacts directement depuis une\nconversation ou une tâche planifiée.\n\n- **claude.ai** : Paramètres › Connecteurs › Ajouter un connecteur personnalisé,\n  collez l'URL, puis « Se connecter ». Vous vous connectez à Listar (ou créez\n  votre compte) et autorisez l'accès : aucune clé à copier.\n- **Claude Code, Cursor et autres** : passez une clé API en en-tête,\n  `Authorization: Bearer sk_live_...`.\n\nOutils : `find_company_contacts`, `enrich_person`, `get_result`,\n`get_credit_balance`, `get_top_up_link`. Chaque appel est facturé sur votre\ncrédit exactement comme l'API. La connexion crée une clé « MCP connector » visible\ndans Paramètres › Clés API : la révoquer coupe l'accès.\n\n## Ligne pro ou perso 💼\n\nLes téléphones livrés portent `usage` : `professional` (ligne pro),\n`personal` (ligne perso) ou `unknown` quand les indices manquent ou se\ncontredisent. Le champ est absent quand l'analyse n'a pas pu tourner (numéro\nécarté, délai dépassé) : traitez-le comme `unknown`. `usageConfidence` (`high`, `medium`, `low`) donne la force du\nverdict, `null` quand `usage` vaut `unknown`. Le verdict s'appuie sur la façon\ndont le numéro se présente (compte WhatsApp Business, annuaire d'entreprise,\nphoto de profil) : il est indépendant de la vérification d'appartenance\n(`phoneValidation`). En masse, les champs s'appellent `phoneUsage`\n(`professional` ou `personal`, absent sinon) et `phoneUsageConfidence` ;\nce sont aussi les colonnes `enriched_phone_usage` et\n`enriched_phone_usage_confidence` des exports CSV. Aucun surcoût.\n\n## Limites de débit\n\nLes appels par clé API sont comptés **par clé**, jamais par adresse IP : plusieurs\nserveurs, ou une IP mutualisée, ne changent rien à votre budget.\n\n| Endpoint | Limite par clé |\n|----------|----------------|\n| `POST /search/unified` | 300 / min |\n| `POST /search/bulk` | 60 / min |\n| `POST /search/verify-emails` | 300 / min |\n| `POST /search/verify-whatsapp` | 60 / min |\n| `POST /search/verify-phone-ownership` | 60 / min |\n| `POST /company-enrichment/search` | 120 / min, et 20 recherches en cours |\n| Autres endpoints d'enrichissement | 100 / min |\n\nL'historique est compté **par compte**, toutes clés et sessions confondues :\n`GET /history` 30 / min, `GET /history/{type}/{id}`\n120 / min. Pour le parcourir en entier, paginez avec\n`paginate=cursor` : chaque réponse donne `nextCursor`, à repasser dans\n`before` pour la page suivante, tant que `hasMore` vaut `true`. Les pages\nnumérotées (`page`, par défaut) s'arrêtent au 2000e élément. Pour\nvos recherches de personne, l'export `GET /history/searches.csv` donne tout en\nun seul appel (12 mois au plus).\n\nLe polling des résultats (`GET /search/results/{id}`,\n`GET /company-enrichment/search/{id}`) n'est pas limité. Pendant le règlement\nd'un paiement (juste après une recharge), il peut répondre `202` avec\n`settlementPending: true` et un `Retry-After` : interrogez de nouveau après ce\ndélai.\n\nAu-delà, la réponse est un `429` avec un en-tête `Retry-After` en secondes :\nattendez ce délai avant de réessayer. Les en-têtes `RateLimit-*` indiquent à\ntout moment ce qu'il vous reste.\n\nUne clé invalide ou révoquée renvoie un `401`. Si ces `401` se répètent, ils\nsont bloqués par adresse IP : corrigez la clé plutôt que de réessayer.\n\n## Tarification\n\nFacturation **à la donnée trouvée** (en centimes d'euro), pas au contact :\n\n| Donnée | Prix de base |\n|--------|--------------|\n| Téléphone | 50¢ |\n| Email | 10¢ |\n\nVérifications unitaires, facturées par élément :\n\n| Endpoint | Prix |\n|----------|------|\n| `POST /search/verify-emails` | 1¢ par email |\n| `POST /search/verify-whatsapp` | 2¢ par numéro, remboursé sans réponse |\n| `POST /search/verify-phone-ownership` | 5¢ par numéro tranché (`owned` / `not_owned`), remboursé sinon |\n\nDes remises de fidélité s'appliquent selon le volume cumulé. Le détail du prix\nest renvoyé dans la réponse `402` (`breakdown`, `priceInCents`,\n`currentBalanceCents`…).\n\n## Support\n\nChaque réponse porte un en-tête `X-Request-Id`. Citez-le, avec l'horodatage de\nl'appel, dans toute demande de support : c'est ce qui nous permet de retrouver\nla requête exacte. L'identifiant renvoyé est toujours généré par Listar. Si\nvous envoyez votre propre `X-Request-Id` (jusqu'à 128 caractères parmi lettres,\nchiffres, `_`, `.` et `-`), nous l'enregistrons à côté du nôtre : vous\npouvez alors nous citer l'un ou l'autre.","contact":{"name":"Listar","url":"https://listar.fr"}},"servers":[{"url":"https://api.listar.fr","description":"Production"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"Person Enrichment","description":"Enrichir une personne (téléphone, email, profils). Cœur de l’API - voir le flux async dans la description en haut."},{"name":"Company Enrichment","description":"Enrichir une entreprise : firmographics + contacts. Flux async (lancer puis poller), même logique que l’enrichissement personne."},{"name":"Credit","description":"Solde de crédit et historique."},{"name":"API Keys","description":"Création et révocation des clés API."},{"name":"Webhooks","description":"Secret de signature des webhooks sortants (récupération / rotation).\n\nChaque webhook envoyé sur votre `callbackUrl` porte un en-tête de signature `X-Listar-Signature: t=<timestamp>,v1=<hmac_sha256>`. La signature = HMAC-SHA256 de `timestamp + \".\" + corps brut`, avec votre secret comme clé :\n\n```js\nconst crypto = require('crypto');\nfunction verify(rawBody, header, secret) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));\n  if (!parts.v1) return false;\n  const expected = crypto\n    .createHmac('sha256', secret)\n    .update(parts.t + '.' + rawBody)\n    .digest('hex');\n  const a = Buffer.from(expected);\n  const b = Buffer.from(parts.v1);\n  return a.length === b.length && crypto.timingSafeEqual(a, b);\n}\n```"}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","description":"Clé API Listar. En-tête : `Authorization: Bearer sk_live_...`"}},"schemas":{"Error":{"type":"object","properties":{"message":{"type":"string"},"status":{"type":"integer"}}},"UnifiedSearchQuery":{"type":"object","description":"Au moins un critère identifiant est requis : `firstName`, `phone`, `email`, `linkedinSlug`, `facebookSlug`, `facebookId`, `doctolibSlug` ou `maltSlug`. Plus l’identifiant est fort (slug LinkedIn fourni, RPPS, fiche d’annuaire), plus la livraison est ancrée : un simple nom + zone ne suffit pas à livrer un datapoint.","properties":{"firstName":{"type":"string","minLength":1,"maxLength":100},"lastName":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","format":"email","maxLength":200},"phone":{"type":"string","minLength":6,"maxLength":20,"description":"Format souple : `+33 6 12 34 56 78`, `0612345678`…"},"linkedinSlug":{"type":"string","maxLength":200,"description":"Slug du profil LinkedIn (la partie après /in/)."},"facebookSlug":{"type":"string","maxLength":200},"facebookId":{"type":"string","maxLength":50},"doctolibSlug":{"type":"string","maxLength":300,"description":"Chemin de fiche Doctolib (`<specialite>/<ville>/<praticien>`, ex. `medecin-generaliste/paris/jean-dupont`) ou URL complète de la fiche. Son portrait sert de photo de RÉFÉRENCE au face match."},"maltSlug":{"type":"string","maxLength":200},"company":{"type":"string","maxLength":200},"jobTitle":{"type":"string","maxLength":300,"description":"Intitulé de poste / spécialité - sert de contexte anti-homonyme."},"city":{"type":"string","maxLength":100},"postalCode":{"type":"string","maxLength":20,"description":"Code postal EXACT du contact - requis pour activer l'ancre `acceptGeoExactUnique` (livraison mobile ou email sur nom + code postal exact)."},"country":{"type":"string","maxLength":100},"birthYear":{"type":"integer","minimum":1900,"example":1985},"gender":{"type":"string","enum":["male","female","M","F","m","f"]},"rppsId":{"type":"string","pattern":"^\\d{11}$","description":"Identifiant national RPPS (11 chiffres) du professionnel de santé. ANCRE FORTE : une fiche de l'annuaire officiel partageant ce RPPS désigne la personne sans ambiguïté d'homonyme, indépendamment de la géo ou de la société."},"industry":{"type":"string","maxLength":200,"description":"Secteur / activité - contexte anti-homonyme quand la raison sociale est opaque."},"website":{"type":"string","maxLength":500,"description":"Site de l'entreprise : son domaine corrobore les emails professionnels."},"enrichmentType":{"type":"string","enum":["both","phone","email"],"default":"both","description":"Limiter l’enrichissement au téléphone ou à l’email. Seul le canal demandé est livré et facturé."},"searchProEmail":{"type":"boolean","default":true,"description":"Livrer les emails PROFESSIONNELS. `enrichmentType` choisit le canal (tél / email) ; celui-ci choisit quel email dans ce canal."},"searchPersoEmail":{"type":"boolean","default":true,"description":"Livrer les emails PERSONNELS (gmail, orange…). Avec `enrichmentType: email`, `searchProEmail` et `searchPersoEmail` ne peuvent pas être tous deux à `false` (400)."},"requirePersonalEmailProof":{"type":"boolean","default":false,"description":"Ne livrer (et ne facturer) un email PERSONNEL que sur PREUVE d'appartenance : face match entre la photo du compte (Gravatar / GitHub / Google / Teams) et le portrait de référence du contact, ou ancre qui désigne la personne elle-même (RPPS, slug LinkedIn fourni, email ou téléphone d'entrée partagé). Une adresse perso n'est rattachée ni à une société ni à une ville : sans cette option, elle peut être livrée sur une simple corroboration de l'entourage. **Désactivé par défaut** (opt-in) - l'activer réduit le volume d'emails perso livrés."},"includeSharedEmails":{"type":"boolean","default":false,"description":"Livrer aussi les boîtes PARTAGÉES d'entreprise (`contact@`, `accueil@`, `pharmacieX@…`) en plus des emails nominatifs. Désactivé par défaut (précision d'abord)."},"acceptGeoExactUnique":{"type":"boolean","default":false,"description":"Livrer les mobiles ET les emails corroborés par `nom + CODE POSTAL EXACT` quand un SEUL est trouvé à ce code postal. L’unicité est comptée canal par canal : ≥2 mobiles distincts au même CP = homonymes → mobile non livré, sans effet sur l’email, et réciproquement. Les fixes en restent exclus. Élargit la couverture sur les listes géo-précises (gérants, professions libérales) ; la géo lâche (ville / rayon) reste insuffisante. **Désactivé par défaut** (opt-in) - risque de fuite homonyme si le code postal est PARTAGÉ (siège / arrondissement) et que la cible n’a pas la donnée en base. Passer `true` pour l’activer sur des listes où le CP est le domicile/commune d’exercice réel."},"callbackUrl":{"type":"string","format":"uri","maxLength":2000,"description":"URL HTTPS notifiée (POST `enrichment.completed`, signé HMAC) quand un enrichissement async se termine. Recommandé pour éviter le polling."},"respondAsync":{"type":"boolean","default":false,"description":"Répond tout de suite `202` avec l’`id`, sans attendre le résultat. Utile pour lancer beaucoup de recherches d’affilée et les récupérer ensuite via `GET /search/results/{id}` (ou par webhook). Facturation identique."}}},"FaceMatch":{"type":"object","nullable":true,"description":"Résultat de comparaison faciale (validation d’identité).","properties":{"isMatch":{"type":"boolean"},"confidence":{"type":"string","enum":["high","medium","low"]}}},"PhoneInfo":{"type":"object","properties":{"number":{"type":"string","example":"+33612345678"},"type":{"type":"string","enum":["mobile","fixed","special","unknown"]},"verified":{"type":"boolean","nullable":true},"verifiedAt":{"type":"string","format":"date-time","nullable":true},"carrier":{"type":"string","nullable":true,"description":"Opérateur auquel l'ARCEP a attribué le bloc du numéro (mobiles français uniquement, `null` ailleurs). Ce n'est pas l'opérateur actuel d'un numéro porté."},"isVirtual":{"type":"boolean","nullable":true,"example":true,"description":"Numéro virtuel (application de second numéro, téléphonie cloud, tranche VoIP) : `true`, ou `null` quand rien ne l'indique. Jamais `false` : un numéro porté vers un opérateur virtuel garde le préfixe de son opérateur d'origine. Mobiles français : bloc ARCEP attribué à un opérateur de numéros virtuels. Ailleurs : tranche que le plan national réserve à la VoIP."},"isWhatsApp":{"type":"boolean","nullable":true},"isWhatsAppBusiness":{"example":false,"type":"boolean","nullable":true,"description":"Compte WhatsApp Business."},"pushname":{"type":"string","nullable":true,"description":"Nom affiché sur WhatsApp."},"usage":{"type":"string","enum":["professional","personal","unknown"],"description":"Usage probable de la ligne : professionnelle ou personnelle. `unknown` quand les indices manquent ou se contredisent ; absent quand l’analyse n’a pas tourné."},"usageConfidence":{"type":"string","nullable":true,"enum":["high","medium","low"],"description":"Confiance dans `usage`, null quand il vaut `unknown`."},"faceMatch":{"$ref":"#/components/schemas/FaceMatch"}}},"EmailInfo":{"type":"object","properties":{"address":{"type":"string","format":"email"},"verified":{"type":"string","nullable":true,"enum":["verified","smtp_valid","invalid","disposable","unknown"]},"verifiedAt":{"type":"string","format":"date-time","nullable":true},"type":{"type":"string","enum":["personal","professional"],"example":"professional"},"stale":{"example":false,"type":"boolean","description":"Email pro dont le domaine ne correspond plus à la société courante."},"domainUncorroborated":{"example":false,"type":"boolean","description":"Domaine de l'email non corroboré par la société demandée ni par son domaine officiel. L'adresse est livrée telle quelle : à vérifier avant prospection."}}},"EnrichedProfile":{"type":"object","properties":{"firstName":{"type":"string","nullable":true},"lastName":{"type":"string","nullable":true},"gender":{"type":"string","nullable":true},"birthDate":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true,"description":"Meilleur téléphone (raccourci de phones[0])."},"phones":{"type":"array","items":{"$ref":"#/components/schemas/PhoneInfo"}},"email":{"type":"string","nullable":true,"description":"Meilleur email (raccourci de emails[0])."},"emails":{"type":"array","items":{"$ref":"#/components/schemas/EmailInfo"}},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"company":{"type":"string","nullable":true},"jobTitle":{"type":"string","nullable":true},"linkedinUrl":{"type":"string","nullable":true},"facebookUrl":{"type":"string","nullable":true},"twitterUrl":{"type":"string","nullable":true}}},"UnifiedSearchResponse":{"type":"object","description":"Résultat complet d’un enrichissement (après paiement).","properties":{"query":{"$ref":"#/components/schemas/UnifiedSearchQuery"},"totalResults":{"type":"integer","example":1},"profiles":{"type":"array","description":"Profil retenu pour la personne recherchée. Les homonymes non vérifiés ne sont jamais renvoyés à une clé API.","items":{"$ref":"#/components/schemas/EnrichedProfile"}},"linkedinParsed":{"type":"object","properties":{"jobTitle":{"type":"string"},"company":{"type":"string"}}},"bestPhone":{"$ref":"#/components/schemas/PhoneInfo"},"bestEmail":{"$ref":"#/components/schemas/EmailInfo"},"excluded":{"type":"boolean","description":"Présent et `true` quand la personne a exercé ses droits RGPD (effacement ou opposition) : `profiles` est vide et rien n’est facturé."},"paidWithCredit":{"type":"boolean","example":true},"creditDeducted":{"example":60,"type":"integer","description":"Montant débité en centimes."},"freeResult":{"type":"boolean","example":false},"id":{"type":"string","description":"ID de la recherche, **présent dans toutes les réponses** (sync 200 comme async 202/402). Ré-interrogeable à tout moment via `GET /search/results/{id}` - vous pouvez donc traiter sync et async avec le même code."},"searchResultId":{"type":"string","deprecated":true,"description":"Alias historique de `id` (même valeur). Conservé pour compatibilité - préférez `id`."}}},"PendingResponse":{"type":"object","description":"Enrichissement async en cours. Récupérez le résultat via webhook (si callbackUrl fourni) ou en pollant GET /search/results/{id}.","properties":{"success":{"type":"boolean","example":true},"id":{"type":"string","description":"ID à utiliser pour le polling."},"pendingLinkedin":{"type":"boolean","example":false,"description":"Récupération du profil LinkedIn en cours."},"pendingAsyncProvider":{"type":"boolean","example":true,"description":"Provider d’enrichissement async en cours."},"message":{"type":"string"}}},"PaymentRequiredResponse":{"type":"object","description":"Résultat prêt, paiement requis.","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","example":"Payment required to view the results"},"requiresPayment":{"type":"boolean","example":true},"id":{"type":"string","example":"6a1f3c9e2b7d4a0012ab34cd"},"contactsCount":{"type":"integer","example":1},"priceInCents":{"type":"integer","example":60},"priceFormatted":{"type":"string","example":"0.60 €"},"hasPhone":{"type":"boolean","example":true},"hasEmail":{"type":"boolean","example":true},"breakdown":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["phone","email"]},"label":{"type":"string"},"basePriceCents":{"type":"integer"},"priceCents":{"type":"integer"}}},"example":[{"type":"phone","label":"Phone","basePriceCents":50,"priceCents":50},{"type":"email","label":"Email","basePriceCents":10,"priceCents":10}]},"currentBalanceCents":{"type":"integer","example":20,"description":"Solde actuel (centimes)."},"topUpNeededCents":{"type":"integer","example":1000,"description":"Recharge suggérée (centimes) : le manque arrondi à l’euro, au moins `minTopUpCents`."},"minTopUpCents":{"type":"integer","example":1000}}},"InsufficientCreditResponse":{"type":"object","description":"Crédit insuffisant : rien n’a été débité.","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","example":"Insufficient credit"},"requiresPayment":{"type":"boolean","example":true},"requiresTopUp":{"type":"boolean","description":"Alias historique de `requiresPayment` (enrichissement entreprise)."},"priceInCents":{"type":"integer","description":"Prix de la requête entière (centimes)."},"priceFormatted":{"type":"string"},"currentBalanceCents":{"type":"integer"},"topUpNeededCents":{"type":"integer","example":1000},"minTopUpCents":{"type":"integer","example":1000}}},"PreflightPaymentRequired":{"type":"object","description":"Crédit insuffisant pour LANCER un enrichissement : son coût maximal dépasse le crédit disponible (solde moins ce que vos enrichissements en cours ont déjà engagé). Rien n’a été lancé ni débité.","properties":{"success":{"type":"boolean","example":false},"error":{"type":"string","example":"Insufficient credit to start this enrichment"},"requiresPayment":{"type":"boolean","example":true},"requiresTopUp":{"type":"boolean","example":true},"estimatedMaxCostCents":{"type":"integer","example":6000},"estimatedMaxCostFormatted":{"type":"string","example":"60.00 €"},"currentBalanceCents":{"type":"integer","example":2000},"reservedCents":{"type":"integer","example":500,"description":"Crédit déjà engagé par vos enrichissements en cours."},"inFlightCount":{"type":"integer","example":1},"availableBalanceCents":{"type":"integer","example":1500},"topUpNeededCents":{"type":"integer","example":4500},"minTopUpCents":{"type":"integer","example":1000}}},"CreditBalance":{"type":"object","properties":{"success":{"type":"boolean","example":true},"balanceCents":{"type":"integer","example":5000},"balanceFormatted":{"type":"string","example":"50.00 €"},"totalTopUpCents":{"type":"integer","example":10000},"totalUsedCents":{"type":"integer","example":5000},"hasCredit":{"type":"boolean","example":true},"minTopUpCents":{"type":"integer","example":1000}}},"WebhookPayload":{"type":"object","description":"Corps POST sur votre callbackUrl quand un enrichissement async se termine.","properties":{"event":{"type":"string","enum":["enrichment.completed"]},"id":{"type":"string","description":"ID de la recherche (= GET /search/results/{id})."},"status":{"type":"string","enum":["paid","free","pending_payment"],"description":"`paid`/`free` → result rempli. `pending_payment` → result null, payez via /search/pay-with-credit."},"query":{"$ref":"#/components/schemas/UnifiedSearchQuery"},"result":{"oneOf":[{"$ref":"#/components/schemas/UnifiedSearchResponse"},{"type":"null"}]},"completedAt":{"type":"string","format":"date-time"}}},"BatchContactResult":{"type":"object","description":"Un contact enrichi du batch. Forme **aplatie** (comme les colonnes d’export CSV) - PAS de `bestPhone`/`bestEmail` ni de `profiles[]` (ceux-là sont réservés au webhook 1-contact `enrichment.completed`).","properties":{"ref":{"type":"string","description":"Votre identifiant, ré-échoué tel quel."},"input":{"type":"object","description":"Les champs du contact tels que soumis."},"status":{"type":"string","enum":["success","partial","not_found","pending","excluded"],"description":"`excluded` : la personne a exercé ses droits RGPD (effacement ou opposition). Rien n’est livré ni facturé."},"phone":{"type":"string","nullable":true},"phoneStatus":{"type":"string","enum":["verified","not_verified"],"nullable":true},"phoneUsage":{"type":"string","enum":["professional","personal"],"nullable":true,"description":"Usage probable du téléphone : ligne professionnelle ou personnelle. Absent quand les indices manquent ou se contredisent."},"phoneUsageConfidence":{"type":"string","enum":["high","medium","low"],"nullable":true,"description":"Confiance dans `phoneUsage`. `low` = verdict probable (un seul indice modéré, ou compte WhatsApp Business sans autre indice)."},"proEmail":{"type":"string","nullable":true},"proEmailStatus":{"type":"string","enum":["verified","not_verified"],"nullable":true},"proEmailDomainUncorroborated":{"type":"boolean","nullable":true,"description":"Domaine de l'email pro non corroboré par la société demandée ni par son domaine officiel. L'adresse est livrée telle quelle : à vérifier avant prospection."},"persoEmail":{"type":"string","nullable":true},"persoEmailStatus":{"type":"string","enum":["verified","not_verified"],"nullable":true}}},"BatchWebhookPayload":{"type":"object","description":"Corps POST sur votre callbackUrl quand un batch (POST /search/bulk) se termine. Le résultat est aplati par contact (`results[]`), pas au format `UnifiedSearchResponse` du webhook 1-contact.","properties":{"event":{"type":"string","enum":["batch_enrichment.completed"]},"id":{"type":"string","description":"ID du batch."},"status":{"type":"string","enum":["completed","pending_payment"],"description":"`completed` → `results` rempli. `pending_payment` → données facturables trouvées mais crédit insuffisant : `results` omis, rechargez votre crédit."},"stats":{"type":"object","properties":{"total":{"type":"integer"},"success":{"type":"integer"},"partial":{"type":"integer"},"notFound":{"type":"integer"},"phonesFound":{"type":"integer"},"emailsFound":{"type":"integer"}}},"pricing":{"type":"object","properties":{"priceInCents":{"type":"integer"},"paidWithCredit":{"type":"boolean"}}},"results":{"type":"array","description":"Un objet par contact soumis. Présent uniquement si `status:\"completed\"`.","items":{"$ref":"#/components/schemas/BatchContactResult"}},"submittedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"}}},"CompanyContactInfo":{"type":"object","properties":{"fullName":{"type":"string","nullable":true},"firstName":{"type":"string","nullable":true},"lastName":{"type":"string","nullable":true},"jobTitle":{"type":"string","nullable":true},"mandateRole":{"type":"string","nullable":true,"description":"Rôle de mandat déclaré au registre, en anglais (ex: `Managing director`, `President`). Contexte secondaire quand `jobTitle` porte un intitulé opérationnel distinct."},"type":{"type":"string","enum":["individual","entity"],"description":"`entity` = mandataire personne morale (holding / société représentante) - pas de LinkedIn/email."},"linkedinUrl":{"type":"string","nullable":true},"validated":{"type":"boolean","description":"Contact validé (identité confirmée)."},"confidence":{"type":"number","minimum":0,"maximum":100,"example":92}}},"CompanyEnrichmentResult":{"type":"object","description":"Entreprise enrichie : firmographics + contacts. Les libellés (`legalForm`, `apeLabel`, rôles de mandat) sont renvoyés en anglais ; villes, pays et noms de personnes sont mis en casse lisible, l’adresse reste celle publiée par le registre. Le champ `activity` (objet social en texte libre, dans la langue du registre) n’est PAS exposé sur cette API : utilisez `apeCode` / `apeLabel` pour le secteur.","properties":{"companyName":{"type":"string","nullable":true},"tradeName":{"type":"string","nullable":true},"domain":{"type":"string","nullable":true},"siren":{"type":"string","nullable":true},"siret":{"type":"string","nullable":true},"legalForm":{"type":"string","nullable":true,"description":"Forme juridique en anglais (ex: `Simplified joint-stock company (SAS)`)."},"legalFormCode":{"type":"string","nullable":true},"apeCode":{"type":"string","nullable":true},"apeLabel":{"type":"string","nullable":true,"description":"Secteur en anglais, résolu depuis `apeCode` (ex: `Computer programming, consultancy and related activities`)."},"isActive":{"type":"boolean","nullable":true,"example":true},"creationDate":{"type":"string","nullable":true},"capital":{"type":"number","nullable":true,"example":10000},"industry":{"type":"string","nullable":true},"employees":{"type":"integer","nullable":true,"example":25},"revenue":{"type":"number","nullable":true,"example":2500000},"foundedYear":{"type":"integer","nullable":true,"example":2015},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"country":{"type":"string","nullable":true,"description":"Pays en anglais (`France`, `Germany`) ; un code ISO fourni à la requête est renvoyé tel quel en majuscules."},"establishmentSiret":{"type":"string","nullable":true,"description":"Renseigné quand le match porte sur un ÉTABLISSEMENT différent du siège (chaînes, multi-sites). Dans ce cas `establishment*` est l’adresse du site matché et `address`/`city`/`postalCode` celle du siège."},"establishmentAddress":{"type":"string","nullable":true},"establishmentCity":{"type":"string","nullable":true},"establishmentPostalCode":{"type":"string","nullable":true},"establishmentsCount":{"type":"integer","nullable":true,"example":2,"description":"Nombre d’établissements ouverts."},"industries":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Secteurs du fournisseur de firmographics (vocabulaire distinct de `apeLabel`)."},"technologies":{"type":"array","nullable":true,"items":{"type":"string"}},"website":{"type":"string","nullable":true},"linkedinUrl":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"directors":{"type":"array","nullable":true,"description":"Représentants légaux issus des registres officiels : personnes physiques, et mandataires personnes morales (holdings, `type: \"entity\"`) lorsque le registre les publie. Indépendant de `withContacts`, mais **`withFirmographics: false` met ce tableau à `null`** (comme `address`/`city`/`country`) puisque le bloc société n’est alors ni facturé ni renvoyé. Les employés des mandataires personnes morales ne sont jamais recherchés.","items":{"$ref":"#/components/schemas/CompanyContactInfo"}},"contactsRequested":{"type":"integer","nullable":true,"example":5},"contactsFound":{"type":"integer","example":3},"contacts":{"type":"array","nullable":true,"items":{"$ref":"#/components/schemas/CompanyContactInfo"}}}},"CompanyWebhookPayload":{"type":"object","description":"Corps POST sur votre callbackUrl quand la recherche entreprise se termine.","properties":{"event":{"type":"string","enum":["company_enrichment.completed"]},"id":{"type":"string"},"status":{"type":"string","enum":["completed","pending_payment"],"description":"`completed` → result rempli. `pending_payment` → result null, le crédit était insuffisant."},"result":{"oneOf":[{"$ref":"#/components/schemas/CompanyEnrichmentResult"},{"type":"null"}]},"completedAt":{"type":"string","format":"date-time"}}}}},"paths":{"/search/unified":{"post":{"tags":["Person Enrichment"],"operationId":"searchUnified","summary":"Enrichir une personne","description":"Lance un enrichissement. Réponse **synchrone** (200/402) ou **asynchrone** (202) - voir le flux async dans la description de l’API. Passez `callbackUrl` pour être notifié par webhook au lieu de poller.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnifiedSearchQuery"},"examples":{"byLinkedin":{"summary":"Par slug LinkedIn","value":{"linkedinSlug":"alexandre-brel-5b090227","callbackUrl":"https://example.com/webhooks/listar"}},"byNameCompany":{"summary":"Par nom + société","value":{"firstName":"Alexandre","lastName":"Brel","company":"Alacaza","callbackUrl":"https://example.com/webhooks/listar"}},"byEmail":{"summary":"Par email (téléphone uniquement)","value":{"email":"alex@alacaza.fr","enrichmentType":"phone","callbackUrl":"https://example.com/webhooks/listar"}}}}}},"responses":{"200":{"description":"Résultat disponible immédiatement (payé par crédit ou gratuit). La réponse contient toujours `searchResultId` - même id que le flux async - ré-interrogeable via `GET /search/results/{id}`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnifiedSearchResponse"}}}},"202":{"description":"Enrichissement async en cours - résultat via webhook ou polling.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PendingResponse"}}}},"402":{"description":"Résultat prêt, paiement requis.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRequiredResponse"}}}},"429":{"description":"Trop de requêtes (300 / min par clé API). `Retry-After` donne le délai d’attente."}}}},"/search/bulk":{"post":{"tags":["Person Enrichment"],"operationId":"searchBulk","summary":"Enrichir en masse (jusqu’à 10 000 contacts)","description":"Soumet jusqu’à 10 000 contacts en un seul appel. Réponse **202** immédiate avec un `id` de batch ; les contacts sont enrichis en arrière-plan. Le résultat complet est poussé sur votre `callbackUrl` (webhook signé `batch_enrichment.completed`) - aucun polling requis. Facturé par donnée trouvée sur votre solde crédit, débité à la fin du batch.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["contacts","callbackUrl"],"properties":{"contacts":{"type":"array","minItems":1,"maxItems":10000,"description":"Liste de contacts (1 à 10 000). Chaque contact requiert au moins un critère identifiant (firstName, lastName, fullName, email, phone, linkedinUrl, linkedinSlug, doctolibSlug ou rppsId). Plus l’identifiant est fort, plus la livraison est ancrée : un simple nom + zone ne suffit pas à livrer un datapoint.","items":{"type":"object","properties":{"ref":{"type":"string","description":"Identifiant client, ré-échoué dans la sortie."},"firstName":{"type":"string"},"lastName":{"type":"string"},"fullName":{"type":"string"},"company":{"type":"string"},"city":{"type":"string"},"postalCode":{"type":"string","description":"Code postal EXACT - active l'ancre `acceptGeoExactUnique` (mobile ou email sur nom + code postal exact)."},"country":{"type":"string"},"jobTitle":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"linkedinUrl":{"type":"string"},"linkedinSlug":{"type":"string"},"doctolibSlug":{"type":"string","description":"Chemin de fiche Doctolib (`<specialite>/<ville>/<praticien>`, ex. `medecin-generaliste/paris/jean-dupont`) ou URL complète de la fiche. Son portrait sert de photo de RÉFÉRENCE au face match - souvent la seule ancre disponible sur un praticien libéral sans LinkedIn. Sa spécialité sert aussi de contexte anti-homonyme quand `jobTitle` est absent."},"rppsId":{"type":"string","pattern":"^\\d{11}$","description":"Identifiant national RPPS (11 chiffres). ANCRE FORTE : une fiche de l'annuaire officiel partageant ce RPPS désigne la personne sans ambiguïté d'homonyme."},"industry":{"type":"string"},"website":{"type":"string"}}}},"options":{"type":"object","description":"Au moins un canal parmi `searchPhone`, `searchProEmail` et `searchPersoEmail` doit rester à `true` (400 sinon).","properties":{"searchPhone":{"type":"boolean","default":true,"description":"**Activé par défaut** : sans `options`, les téléphones trouvés sont livrés ET facturés. Passer `false` pour un batch email seul."},"searchProEmail":{"type":"boolean","default":true},"searchPersoEmail":{"type":"boolean","default":true},"includeSharedEmails":{"type":"boolean","default":false,"description":"Livrer aussi les boîtes PARTAGÉES d'entreprise (`contact@`, `accueil@`, `pharmacieX@…`) en plus des emails nominatifs. Désactivé par défaut (précision d'abord) : n'activez que si les adresses d'établissement vous conviennent."},"acceptGeoExactUnique":{"type":"boolean","default":false,"description":"Livrer les mobiles ET les emails corroborés par `nom + CODE POSTAL EXACT` quand un SEUL est trouvé à ce code postal. L’unicité est comptée canal par canal : ≥2 mobiles distincts au même CP = homonymes → mobile non livré, sans effet sur l’email, et réciproquement. Les fixes en restent exclus. Élargit la couverture sur les listes géo-précises (gérants, professions libérales) ; la géo lâche (ville / rayon) reste insuffisante. **Désactivé par défaut** (opt-in) - risque de fuite homonyme si le code postal est PARTAGÉ (siège / arrondissement) et que la cible n’a pas la donnée en base. Passer `true` pour l’activer sur des listes où le CP est le domicile/commune d’exercice réel."},"requirePersonalEmailProof":{"type":"boolean","default":false,"description":"Ne livrer (et ne facturer) un email PERSONNEL que sur PREUVE d'appartenance : face match entre la photo du compte (Gravatar / GitHub / Google / Teams) et le portrait de référence du contact, ou ancre qui désigne la personne elle-même (RPPS, slug LinkedIn fourni, email ou téléphone d'entrée partagé). Une adresse perso n'est rattachée ni à une société ni à une ville : sans cette option, elle peut être livrée sur une simple corroboration de l'entourage. **Désactivé par défaut** (opt-in) - l'activer réduit le volume d'emails perso livrés."}}},"callbackUrl":{"type":"string","format":"uri","description":"URL https notifiée à la fin du batch (webhook signé). **Requis** - unique canal de restitution du résultat."}}},"examples":{"basic":{"summary":"Batch simple","value":{"callbackUrl":"https://example.com/webhooks/listar","options":{"searchPhone":true,"searchProEmail":true,"searchPersoEmail":true},"contacts":[{"ref":"crm-001","firstName":"Alexandre","lastName":"Brel","company":"Alacaza"},{"ref":"crm-002","linkedinUrl":"https://www.linkedin.com/in/alexandre-brel-5b090227"},{"ref":"crm-003","email":"alex@alacaza.fr"}]}}}}}},"responses":{"202":{"description":"Batch accepté. Le résultat complet arrivera par webhook `batch_enrichment.completed`.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"id":{"type":"string"},"totalContacts":{"type":"integer","example":250},"status":{"type":"string","example":"processing"}}}}}},"402":{"description":"Crédit insuffisant pour le coût max estimé.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreflightPaymentRequired"}}}},"422":{"description":"Plus de 10 000 contacts dans le batch."},"429":{"description":"Trop de requêtes (60 / min par clé API). `Retry-After` donne le délai d’attente."}}}},"/search/results/{id}":{"get":{"tags":["Person Enrichment"],"operationId":"getSearchResults","summary":"Récupérer un résultat (polling)","description":"Poll du résultat d’un enrichissement async. Boucler tant que le statut est `202`/`425`. Un résultat en attente de paiement est débité automatiquement sur votre crédit dès que le solde le couvre (la réponse porte alors `paidWithCredit` et `creditDeducted`). Une recharge règle aussi, de la plus ancienne à la plus récente, les recherches restées en attente de paiement.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"ID renvoyé par POST /search/unified."}],"responses":{"200":{"description":"Résultat complet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnifiedSearchResponse"}}}},"202":{"description":"Encore en cours - re-poller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PendingResponse"}}}},"402":{"description":"Prêt, paiement requis : le crédit ne couvre pas le prix. Rechargez (le résultat est alors réglé automatiquement) ou payez via POST /search/pay-with-credit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRequiredResponse"}}}},"404":{"description":"Recherche introuvable ou expirée."},"409":{"description":"Cet `id` est celui d’une recherche entreprise : poller `GET /company-enrichment/search/{id}`."},"425":{"description":"Trop tôt - enrichissement en cours, réessayer."}}}},"/search/pay-with-credit":{"post":{"tags":["Person Enrichment"],"operationId":"payWithCredit","summary":"Payer un résultat avec le crédit","description":"Débite le crédit pour débloquer un résultat en `pending_payment` (réponse 402).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"ID de la recherche."}}}}}},"responses":{"200":{"description":"Payé - résultat complet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnifiedSearchResponse"}}}},"402":{"description":"Crédit insuffisant.","content":{"application/json":{"example":{"success":false,"error":"Insufficient credit","requiresPayment":true,"id":"6a1f3c9e2b7d4a0012ab34cd","contactsCount":1,"priceInCents":60,"priceFormatted":"0.60 €","currentBalanceCents":20,"topUpNeededCents":1000,"minTopUpCents":1000},"schema":{"$ref":"#/components/schemas/PaymentRequiredResponse"}}}},"404":{"description":"Recherche introuvable."},"425":{"description":"Enrichissement encore en cours."}}}},"/search/verify-emails":{"post":{"tags":["Person Enrichment"],"operationId":"verifyEmails","summary":"Vérifier des emails","description":"Vérifie la délivrabilité de 1 à 10 emails. **Facturé 1 ct par email** pour les appels par clé API. Renvoie `402` si le crédit est insuffisant.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["emails"],"properties":{"emails":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"string","format":"email"}}}}}}},"responses":{"200":{"description":"Statuts de vérification.","content":{"application/json":{"example":{"results":[{"email":"jane.doe@acme.com","status":"verified","verifiedAt":"2026-10-01T09:12:00.000Z"},{"email":"j.doe@acme.com","status":"invalid","verifiedAt":"2026-10-01T09:12:00.000Z"},{"email":"jane@mailinator.com","status":"disposable","verifiedAt":"2026-10-01T09:12:00.000Z"}],"creditDeducted":3},"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"email":{"type":"string"},"status":{"type":"string","enum":["verified","invalid","disposable","unknown"]},"verifiedAt":{"type":"string","format":"date-time","nullable":true}}}},"creditDeducted":{"type":"integer","example":3,"description":"Montant débité (centimes, 1 par email) - uniquement pour les appels par clé API."}}}}}},"402":{"description":"Crédit insuffisant.","content":{"application/json":{"example":{"success":false,"error":"Insufficient credit","requiresPayment":true,"priceInCents":3,"priceFormatted":"0.03 €","currentBalanceCents":1,"topUpNeededCents":1000,"minTopUpCents":1000},"schema":{"$ref":"#/components/schemas/InsufficientCreditResponse"}}}},"429":{"description":"Trop de requêtes (300 / min par clé API)."}}}},"/search/verify-whatsapp":{"post":{"tags":["Person Enrichment"],"operationId":"verifyWhatsapp","summary":"Vérifier des numéros WhatsApp","description":"Vérifie si 1 à 10 numéros (format `+E164`) sont sur WhatsApp. **Facturé 2 cts par numéro** pour les appels par clé API (remboursé pour un numéro sans réponse). Renvoie `402` si le crédit est insuffisant.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phones"],"properties":{"phones":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"string","example":"+33612345678"}}}}}}},"responses":{"200":{"description":"Statuts WhatsApp.","content":{"application/json":{"example":{"results":[{"phone":"+33612345678","isWhatsApp":true,"isWhatsAppBusiness":false,"pushname":"Jane","profilePicUrl":"https://…/avatar.jpg","verifiedAt":"2026-10-01T09:12:00.000Z"},{"phone":"+33698765432","isWhatsApp":false,"isWhatsAppBusiness":false,"pushname":null,"profilePicUrl":null,"verifiedAt":"2026-10-01T09:12:00.000Z"},{"phone":"+33611223344","isWhatsApp":null,"isWhatsAppBusiness":null,"pushname":null,"profilePicUrl":null,"verifiedAt":null}],"creditDeducted":4,"creditRefunded":2},"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"phone":{"type":"string"},"isWhatsApp":{"type":"boolean","nullable":true},"isWhatsAppBusiness":{"type":"boolean","nullable":true},"pushname":{"type":"string","nullable":true},"profilePicUrl":{"type":"string","nullable":true},"verifiedAt":{"type":"string","format":"date-time","nullable":true}}}},"creditDeducted":{"type":"integer","example":4,"description":"Montant débité (centimes, 2 par numéro), après remboursement des numéros sans réponse - uniquement pour les appels par clé API."},"creditRefunded":{"type":"integer","example":2,"description":"Montant remboursé (centimes) pour les numéros sans réponse. Ici : 3 numéros, 6 cts réservés, 1 sans réponse remboursé."}}}}}},"402":{"description":"Crédit insuffisant.","content":{"application/json":{"example":{"success":false,"error":"Insufficient credit","requiresPayment":true,"priceInCents":6,"priceFormatted":"0.06 €","currentBalanceCents":2,"topUpNeededCents":1000,"minTopUpCents":1000},"schema":{"$ref":"#/components/schemas/InsufficientCreditResponse"}}}},"429":{"description":"Trop de requêtes (60 / min par clé API)."}}}},"/search/verify-phone-ownership":{"post":{"tags":["Person Enrichment"],"operationId":"verifyPhoneOwnership","summary":"Vérifier l'appartenance de numéros","description":"Vérifie si 1 à 10 numéros (format `+E164`) appartiennent à la personne donnée, à partir des comptes de messagerie liés au numéro (photo et nom), des vérifications passées et de notre index. L'URL LinkedIn, optionnelle, permet la comparaison de visage. **Facturé 5 cts par numéro tranché** (`owned` ou `not_owned`) ; les numéros `unknown` et `invalid` sont remboursés. Renvoie `402` si le crédit est insuffisant pour l'ensemble de la requête. Chaque numéro est interrogé en direct : comptez jusqu'à une minute. Un même numéro ne peut figurer qu'une fois par requête.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["contacts"],"properties":{"contacts":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"object","required":["phone"],"description":"`firstName` et `lastName` sont requis sans `linkedinUrl`. Avec, ils peuvent être omis : le nom est lu sur le profil. Si le profil ne donne pas de nom, le verdict est `unknown` (`profile_unknown`) et remboursé.","properties":{"phone":{"type":"string","example":"+33612345678"},"firstName":{"type":"string","example":"Jane","description":"Avec `lastName`, ou omis avec lui : jamais un seul des deux."},"lastName":{"type":"string","example":"Doe","description":"Avec `firstName`, ou omis avec lui."},"linkedinUrl":{"type":"string","nullable":true,"example":"https://www.linkedin.com/in/jane-doe","description":"URL du profil LinkedIn de la personne : permet la comparaison de visage, bien plus précise. Sans les noms, elle doit être une URL de profil (`linkedin.com/in/…`). Avec les noms, une autre URL LinkedIn (Sales Navigator, page entreprise) est ignorée."}}}}}}}}},"responses":{"200":{"description":"Verdicts par numéro.","content":{"application/json":{"example":{"results":[{"phone":"+33612345678","status":"owned","reason":"face_match","checkedName":"Jane Doe","signals":{"lineType":"mobile","country":"FR","carrier":"SFR","isVirtual":null,"isWhatsApp":true,"isWhatsAppBusiness":false,"isTelegram":false,"faceMatch":{"isMatch":true,"confidence":"high"},"nameMatch":null,"recordedUnderOtherName":false,"formerOwner":false}},{"phone":"+33698765432","status":"unknown","reason":"no_evidence","checkedName":"Jane Doe","signals":{"lineType":"mobile","country":"FR","carrier":"Bouygues Telecom","isVirtual":null,"isWhatsApp":null,"isWhatsAppBusiness":null,"isTelegram":null,"faceMatch":null,"nameMatch":null,"recordedUnderOtherName":false,"formerOwner":false}}],"creditDeducted":5,"creditRefunded":5},"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"phone":{"type":"string"},"status":{"type":"string","enum":["owned","not_owned","unknown","invalid"]},"checkedName":{"type":"string","nullable":true,"description":"Nom contre lequel le numéro a été vérifié : celui fourni, ou celui lu sur le profil LinkedIn (renvoyé seulement avec un verdict facturé)."},"reason":{"type":"string","description":"`face_match` / `face_mismatch` : la photo du compte lié au numéro correspond / montre une autre personne. `name_match` / `name_mismatch` : le compte lié au numéro porte exactement ce nom / celui d'une autre personne. `known_owner` / `known_owner_other` : une vérification passée a rattaché le numéro à cette personne / à une autre (un numéro n'appartient qu'à une personne). `profile_unknown` : aucun nom lisible sur le profil LinkedIn. `invalid_number` : numéro non attribuable. `no_evidence` : pas assez d'éléments.","enum":["face_match","face_mismatch","name_match","name_mismatch","known_owner","known_owner_other","profile_unknown","invalid_number","no_evidence"]},"signals":{"type":"object","description":"Ce que les vérifications ont trouvé, renseigné seulement avec un verdict facturé (`owned`, `not_owned`). Sur un verdict remboursé, seuls `lineType`, `country`, `carrier` et `isVirtual` sont remplis.","properties":{"lineType":{"type":"string","enum":["mobile","fixed","special","unknown"],"nullable":true},"country":{"type":"string","nullable":true},"carrier":{"type":"string","nullable":true,"description":"Opérateur auquel l'ARCEP a attribué le bloc du numéro (mobiles français uniquement, `null` ailleurs). Ce n'est pas l'opérateur actuel d'un numéro porté."},"isVirtual":{"type":"boolean","nullable":true,"description":"Numéro virtuel (application de second numéro, téléphonie cloud, tranche VoIP) : `true`, ou `null` quand rien ne l'indique. Jamais `false` : un numéro porté vers un opérateur virtuel garde le préfixe de son opérateur d'origine. Mobiles français : bloc ARCEP attribué à un opérateur de numéros virtuels. Ailleurs : tranche que le plan national réserve à la VoIP."},"isWhatsApp":{"type":"boolean","nullable":true},"isWhatsAppBusiness":{"type":"boolean","nullable":true},"isTelegram":{"type":"boolean","nullable":true},"faceMatch":{"type":"object","nullable":true,"properties":{"isMatch":{"type":"boolean"},"confidence":{"type":"string","enum":["high","medium","low"]}}},"nameMatch":{"type":"object","nullable":true,"properties":{"isMatch":{"type":"boolean"},"confidence":{"type":"string","enum":["high","medium","low"]}}},"recordedUnderOtherName":{"type":"boolean","description":"Notre base connaît ce numéro, mais jamais sous ce nom. Indicatif seulement : ne décide pas du verdict."},"formerOwner":{"type":"boolean","description":"Le numéro a appartenu à la personne mais a changé de titulaire depuis."}}}}}},"creditDeducted":{"type":"integer","example":5,"description":"Montant débité (centimes, 5 par numéro tranché), après remboursement des numéros non tranchés."},"creditRefunded":{"type":"integer","example":5,"description":"Montant remboursé (centimes) pour les numéros non tranchés. Ici : 2 numéros, 10 cts réservés, 1 tranché et 1 remboursé."}}}}}},"400":{"description":"Requête invalide, rien n'est débité : un seul des deux noms, ni noms ni URL de profil LinkedIn, URL qui n'est pas un profil alors que les noms sont omis, même numéro deux fois, numéro hors format `+E164`."},"402":{"description":"Crédit insuffisant.","content":{"application/json":{"example":{"success":false,"error":"Insufficient credit","requiresPayment":true,"priceInCents":10,"priceFormatted":"0.10 €","currentBalanceCents":3,"topUpNeededCents":1000,"minTopUpCents":1000},"schema":{"$ref":"#/components/schemas/InsufficientCreditResponse"}}}},"429":{"description":"Trop de requêtes (60 / min par clé API)."}}}},"/history/searches.csv":{"get":{"tags":["Credit"],"operationId":"exportSearchesCsv","summary":"Export de vos recherches (CSV)","description":"Toutes les recherches de personne de votre compte (ou de votre organisation pour un propriétaire ou un administrateur) sur la période, une ligne par recherche : ce que vous avez envoyé, ce qui vous a été livré (téléphone, email du profil retenu) et le montant débité, net des remboursements. Sous contrat, le montant dû et le tarif appliqué s'ajoutent. Par défaut les 3 derniers mois, 12 mois au plus par export. Les colonnes du contrat sont réservées au propriétaire et aux administrateurs de l'organisation. Limité à 5 exports par heure et par compte.","parameters":[{"name":"from","in":"query","description":"Début de la période (ISO 8601). Par défaut : 3 mois avant `to`.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"Fin de la période (ISO 8601, instant). Par défaut : maintenant.","schema":{"type":"string","format":"date-time"}},{"name":"sep","in":"query","description":"Séparateur : `;` (virgules décimales, Excel en français) ou `,`. Par défaut selon `Accept-Language`.","schema":{"type":"string","enum":[",",";"]}}],"responses":{"200":{"description":"Le fichier CSV (UTF-8 avec BOM).","content":{"text/csv":{"example":"search_id,created_at,status,input_linkedin_slug,input_first_name,input_last_name,input_company,input_email,input_phone,requested_channels,delivered_phone,delivered_email,charged_eur\n6ab125b632aa7227db804ca9,2026-09-21T12:40:22.958Z,paid,jane-doe,,,,,,phone,+33600000001,,0.3000\n"}}},"429":{"description":"Trop d’exports : réessayez après `Retry-After`."}}}},"/credit/balance":{"get":{"tags":["Credit"],"operationId":"getCreditBalance","summary":"Solde de crédit","responses":{"200":{"description":"Solde courant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditBalance"}}}}}}},"/credit/transactions":{"get":{"tags":["Credit"],"operationId":"getCreditTransactions","summary":"Historique des transactions","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}}],"responses":{"200":{"description":"Transactions récentes.","content":{"application/json":{"example":{"success":true,"transactions":[{"id":"6a1f3c9e2b7d4a0012ab3401","type":"topup","amountCents":5000,"amountFormatted":"+50.00 €","description":"Credit top-up","relatedResourceType":null,"createdAt":"2026-09-28T10:00:00.000Z"},{"id":"6a1f3c9e2b7d4a0012ab3402","type":"usage","amountCents":-60,"amountFormatted":"-0.60 €","description":"Enrichment: phone + email","relatedResourceType":"SearchResult","createdAt":"2026-09-30T14:21:00.000Z"},{"id":"6a1f3c9e2b7d4a0012ab3403","type":"refund","amountCents":2,"amountFormatted":"+0.02 €","description":"WhatsApp verification refund","relatedResourceType":"PhoneVerification","createdAt":"2026-10-01T09:12:00.000Z"}]},"schema":{"type":"object","properties":{"success":{"type":"boolean"},"transactions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["topup","usage","refund","reversal"]},"amountCents":{"type":"integer"},"amountFormatted":{"type":"string"},"description":{"type":"string"},"relatedResourceType":{"type":"string","nullable":true,"enum":["SearchResult","Message","EnrichmentSession","CompanyEnrichmentSession","EmailVerification","PhoneVerification","AgentToolCall"],"description":"Type de ressource facturée. Clé stable à préférer au texte de `description` pour catégoriser une ligne."},"createdAt":{"type":"string","format":"date-time"}}}}}}}}}}}},"/api-keys":{"get":{"tags":["API Keys"],"operationId":"listApiKeys","summary":"Lister les clés API","responses":{"200":{"description":"Liste des clés (masquées).","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"apiKeys":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"maskedKey":{"type":"string","example":"sk_live_••••••••••••••••••••abcd"},"createdAt":{"type":"string","format":"date-time"},"lastUsedAt":{"type":"string","format":"date-time","nullable":true}}}}}}}}}}},"post":{"tags":["API Keys"],"operationId":"createApiKey","summary":"Créer une clé API","description":"Renvoie la clé en clair (`rawKey`) **une seule fois** - stockez-la immédiatement.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":100,"example":"Mon intégration"}}}}}},"responses":{"200":{"description":"Clé créée.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"rawKey":{"type":"string","example":"sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx","description":"À copier maintenant - non ré-affichée."},"apiKey":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"maskedKey":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}}}}}}}}}},"/api-keys/{id}":{"delete":{"tags":["API Keys"],"operationId":"revokeApiKey","summary":"Révoquer une clé API","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Clé révoquée."},"404":{"description":"Clé introuvable."}}}},"/company-enrichment/search":{"post":{"tags":["Company Enrichment"],"operationId":"companySearch","summary":"Lancer une recherche entreprise","description":"Recherche entreprise **asynchrone** : firmographics + contacts (si `withContacts`). Réponse `202` avec un `id` ; récupérez le résultat via `GET /company-enrichment/search/{id}` ou par webhook (`callbackUrl`). Facturé à la complétion : **2 ct** si l’entreprise est trouvée + **5 ct par contact** retourné. `402` immédiat si le crédit ne couvre pas le coût max.\n\n**Disponibilité : GA en France** (résolution d’identité sur registre officiel, dirigeants légaux inclus). **Alpha hors de France** : la résolution repose alors sur `domain`/`website` + `country`, la couverture des identifiants et des représentants légaux est partielle et le taux de match plus faible. Nous vous recommandons de tester un échantillon avant de lancer un volume hors France.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Au moins un critère requis : siren, siret, tva, domain, website ou companyName. ⚠️ `companyName` SEUL est ambigu (homonymes) : ajoutez `domain`/`website` - le signal d’identité le plus fort - et `country` si la société n’est pas française.","properties":{"siren":{"type":"string"},"siret":{"type":"string"},"tva":{"type":"string"},"domain":{"type":"string","description":"Signal d’identité le PLUS FIABLE : lève l’ambiguïté des homonymes et permet de résoudre les sociétés étrangères. À privilégier dès qu’il est connu."},"website":{"type":"string"},"companyName":{"type":"string"},"city":{"type":"string"},"country":{"type":"string","description":"Pays de la société (ex. `France`, `Belgium`). **Requis dès que la société n’est pas française.** La résolution par nom seul s’appuie sur le registre français : sans `country`, un nom étranger est résolu vers l’homonyme français (une « Altius » belge → la SARL française du même nom), et la recherche est facturée. Hors France, passez `country` ET `domain`/`website` comme ancre d’identité. Couverture : **GA en France**, **alpha ailleurs** (cf. description de l’endpoint)."},"withFirmographics":{"type":"boolean","default":true,"description":"Inclure (et facturer) les firmographics - infos société. Activé par défaut. Passez false pour ne payer/recevoir que les contacts (`withContacts:true` alors requis). Au moins un des deux (firmographics ou contacts) doit rester activé."},"withContacts":{"type":"boolean","default":false,"description":"Inclure les contacts en plus des firmographics."},"contactsTargetCount":{"type":"integer","minimum":1,"maximum":100,"default":5},"hintTitles":{"type":"string","maxLength":1000,"description":"Intitulés de poste à privilégier parmi les contacts (texte libre, séparés par virgule / point-virgule / retour-ligne - ex. `DAF, DRH, responsable achats`). Oriente la sélection/catégorisation des contacts ; sans effet sur les firmographics."},"selectionInstructions":{"type":"string","maxLength":500,"description":"Consigne libre pour affiner la sélection des contacts de CETTE recherche (ex. « exclure les stagiaires et assistants », « prioriser les rôles techniques »). Affine par FONCTION/rôle uniquement (pas de filtre géographique). Les dirigeants légaux restent toujours inclus."},"contactHint":{"type":"string","maxLength":200,"description":"Contact ciblé pour CETTE entreprise (texte libre : nom et/ou rôle de la personne à prioriser, ex. `Nicolas Hernandez` ou `Directeur Général`). Si la personne est trouvée et confirmée dans l’entreprise, elle est renvoyée en premier, puis complétée par d’autres décideurs jusqu’au nombre cible ; sinon repli automatique sur les décideurs. Sans effet sur les firmographics."},"industry":{"type":"string","maxLength":200,"description":"Industrie / secteur de CETTE entreprise (texte libre, ex. `édition de logiciels`, `fonds d’investissement`). Sert à lever l’ambiguïté quand plusieurs sociétés portent le même nom : on privilégie celle dont l’activité correspond. Recommandé quand vous cherchez par NOM sans identifiant (SIREN/domaine)."},"validatedOnly":{"type":"boolean","default":true,"description":"✅ Activé par défaut. N’affecte QUE les contacts (jamais les firmographics/infos société, toujours renvoyées). Si true (défaut), ne renvoie/facture que les contacts explicitement validés (les plus fiables). Cela EXCLUT les contacts non confirmés - vous pouvez donc obtenir moins de contacts. Passez false pour inclure aussi les contacts non confirmés."},"callbackUrl":{"type":"string","format":"uri","maxLength":2000,"description":"Webhook https notifié quand le résultat est prêt."}}},"examples":{"parNom":{"summary":"Par nom + contacts","value":{"companyName":"Acme","city":"Paris","withContacts":true,"contactsTargetCount":5,"hintTitles":"DAF, DRH, responsable achats","contactHint":"Nicolas Hernandez","validatedOnly":true,"callbackUrl":"https://example.com/webhooks/listar"}},"parSiren":{"summary":"Par SIREN (firmographics seuls)","value":{"siren":"823634910","callbackUrl":"https://example.com/webhooks/listar"}},"parSirenContacts":{"summary":"Par SIREN + contacts","value":{"siren":"823634910","withContacts":true,"contactsTargetCount":5,"hintTitles":"DAF, DRH, responsable achats","validatedOnly":true,"callbackUrl":"https://example.com/webhooks/listar"}}}}}},"responses":{"202":{"description":"Recherche lancée - poller GET /company-enrichment/search/{id}.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"},"message":{"type":"string"}}}}}},"402":{"description":"Crédit insuffisant pour le coût max.","content":{"application/json":{"example":{"success":false,"error":"Insufficient credit to start this enrichment","requiresPayment":true,"requiresTopUp":true,"estimatedMaxCostCents":27,"estimatedMaxCostFormatted":"0.27 €","currentBalanceCents":10,"reservedCents":0,"inFlightCount":0,"availableBalanceCents":10,"topUpNeededCents":1000,"minTopUpCents":1000},"schema":{"$ref":"#/components/schemas/PreflightPaymentRequired"}}}},"429":{"description":"Trop de requêtes (120 / min par clé API), ou 20 recherches déjà en cours pour ce compte : réessayer quand l’une d’elles se termine."}}}},"/company-enrichment/search/{id}":{"get":{"tags":["Company Enrichment"],"operationId":"getCompanySearch","summary":"Récupérer une recherche entreprise (polling)","description":"Poll du résultat. Boucler tant que le statut est `202`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"ID renvoyé par POST /company-enrichment/search."}],"responses":{"200":{"description":"Résultat complet (firmographics + contacts).","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"},"result":{"$ref":"#/components/schemas/CompanyEnrichmentResult"}}}}}},"202":{"description":"Encore en cours - re-poller."},"402":{"description":"Prêt, paiement requis.","content":{"application/json":{"example":{"success":false,"error":"Insufficient credit","requiresPayment":true,"requiresTopUp":true,"currentBalanceCents":10,"topUpNeededCents":1000,"minTopUpCents":1000,"priceInCents":27},"schema":{"$ref":"#/components/schemas/InsufficientCreditResponse"}}}},"404":{"description":"Recherche introuvable ou expirée."},"422":{"description":"La recherche a échoué."}}}}},"webhooks":{"enrichment.completed":{"post":{"tags":["Person Enrichment"],"summary":"Webhook : enrichment.completed","description":"Envoyé sur votre `callbackUrl` quand un enrichissement async se termine. En-tête `X-Listar-Signature: t=<ts>,v1=<hmac_sha256>`. Répondez 2xx pour acquitter.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPayload"}}}},"responses":{"200":{"description":"Acquittement."}}}},"company_enrichment.completed":{"post":{"tags":["Company Enrichment"],"summary":"Webhook : company_enrichment.completed","description":"Envoyé sur votre `callbackUrl` quand une recherche entreprise async se termine. En-tête `X-Listar-Signature`. Répondez 2xx.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyWebhookPayload"}}}},"responses":{"200":{"description":"Acquittement."}}}},"batch_enrichment.completed":{"post":{"tags":["Person Enrichment"],"summary":"Webhook : batch_enrichment.completed","description":"Envoyé sur votre `callbackUrl` quand un batch (POST /search/bulk) se termine. Le résultat complet est embarqué dans `results[]` (un objet par contact, forme aplatie - pas de `bestPhone`/`bestEmail`). En-tête `X-Listar-Signature: t=<ts>,v1=<hmac_sha256>`. Répondez 2xx pour acquitter.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchWebhookPayload"}}}},"responses":{"200":{"description":"Acquittement."}}}},"/webhooks/signing-secret":{"get":{"tags":["Webhooks"],"operationId":"getWebhookSigningSecret","summary":"Récupérer le secret de signature webhook","description":"Renvoie le secret de signature des webhooks sortants de votre organisation (généré au premier appel).\n\nChaque webhook envoyé sur votre `callbackUrl` porte un en-tête de signature `X-Listar-Signature: t=<timestamp>,v1=<hmac_sha256>`. La signature = HMAC-SHA256 de `timestamp + \".\" + corps brut`, avec votre secret comme clé :\n\n```js\nconst crypto = require('crypto');\nfunction verify(rawBody, header, secret) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));\n  if (!parts.v1) return false;\n  const expected = crypto\n    .createHmac('sha256', secret)\n    .update(parts.t + '.' + rawBody)\n    .digest('hex');\n  const a = Buffer.from(expected);\n  const b = Buffer.from(parts.v1);\n  return a.length === b.length && crypto.timingSafeEqual(a, b);\n}\n```","responses":{"200":{"description":"Secret de signature.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"secret":{"type":"string","example":"whsec_…"}}}}}}}}},"/webhooks/signing-secret/rotate":{"post":{"tags":["Webhooks"],"operationId":"rotateWebhookSigningSecret","summary":"Régénérer le secret de signature webhook","description":"Génère un nouveau secret et invalide l’ancien (réservé aux rôles owner/admin).","responses":{"200":{"description":"Nouveau secret.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"secret":{"type":"string","example":"whsec_…"}}}}}}}}}}}