# outsend — Documentation complète (FR) Toutes les pages de la documentation publique outsend, concaténées pour ingestion par un LLM. Chaque page est délimitée par ``. --- title: Documentation outsend slug: section: summary: Référence technique pour outsend — modules, pipelines, veille, API. Conçue pour les développeurs et les assistants AI. --- Cette documentation décrit les **contrats publics** de chaque module outsend — ce que chacun accepte en entrée, ce qu'il renvoie, son comportement dans le temps, et comment les modules s'enchaînent en pipelines. Double objectif : 1. **Aider les intégrateurs et les power users** à comprendre ce que fait chaque module et comment le piloter depuis l'UI ou l'API. 2. **Être lisible par un assistant AI** — chaque page est du markdown brut, téléchargeable en masse, exposée via le standard `llms.txt`. ## Comment lire - **Concepts** — par où commencer. Couvre ce qu'est un *job*, un *pipeline*, une *veille*, plus le cycle de vie et les événements émis. - **Modules** — une page par module (19 actifs + 4 sur demande). Structure : Objet → Entrées → Sorties → Cycle de vie → Limites → Erreurs. - **Référence API** — chaque endpoint REST, groupé par domaine. - **Intégration** — bring-your-own-key (BYOK), MCP server (prévu), `llms.txt`. ## Tout copier en un clic Le bouton **Copy** en haut à droite de chaque page permet de récupérer : - La page courante (markdown brut) - La section courante (toutes les pages de modules par ex.) - **Toute la documentation** — un seul bundle markdown concaténé, prêt à coller dans Claude, ChatGPT, Cursor, ou tout autre assistant AI. Index LLM stable à [`/docs/fr/llms.txt`](/docs/fr/llms.txt) et bundle complet à [`/docs/fr/llms-full.txt`](/docs/fr/llms-full.txt) — tous deux suivent le standard [llms.txt](https://llmstxt.org), détecté automatiquement par la plupart des outils AI. ## Périmètre Cette documentation décrit **ce qu'outsend expose**, pas comment c'est construit en interne. Détails d'implémentation — stack de scraping, infrastructure proxy, sélecteurs DOM, heuristiques de timing, taux de réussite exacts — volontairement omis. Ce ne sont pas des contrats stables et ils n'aident pas à intégrer. S'il manque quelque chose, écrire à [support@outsend.xyz](mailto:support@outsend.xyz). ## Liens rapides - [Qu'est-ce qu'outsend](/docs/fr/what-is-outsend) - [Démarrage rapide](/docs/fr/quickstart) - [Jobs & cycle de vie](/docs/fr/concepts/jobs-lifecycle) - [Registre des modules](/docs/fr/concepts/module-registry) - [Vue d'ensemble API](/docs/fr/api/overview) --- title: Authentification slug: api/auth section: API summary: Émission et révocation de cookies de session, gestion des identifiants, vérification d'email et endpoints RGPD self-service sous /api/auth. --- # Authentification L'API d'authentification émet et révoque les cookies de session, gère les identifiants, vérifie la propriété d'email et expose les endpoints RGPD self-service. Toutes les routes sont montées sous `/api/auth` et répondent en JSON sauf mention contraire. ## Cookie de session Les appels réussis à `signup`, `login` et `password/change` posent un cookie `outsend_session` : | Attribut | Valeur | |----------|--------| | Nom | `outsend_session` | | TTL | 7 jours (`SESSION_DURATION_DAYS = 7`) | | `HttpOnly` | true | | `Secure` | true (production) | | `SameSite` | `Lax` | | `Path` | `/` | Le cookie est un jeton signé lié à une ligne de `sessions`. Révoquer une session (logout, changement de mot de passe, suppression de compte) supprime la ligne côté serveur même si le cookie est rejoué. ## Limites de débit et erreurs Chaque endpoint applique des fenêtres par IP et par identité (voir [Limites](/docs/fr/concepts/limits)). Le dépassement renvoie `429 Too Many Requests` avec un message en français contenant le délai de retry en secondes. Toutes les erreurs suivent la forme FastAPI `{ "detail": "" }`. Codes génériques : `400` (payload invalide, jeton expiré, mauvais mot de passe actuel, échec captcha), `401` (identifiants erronés ou session manquante sur routes protégées), `429` (limite de débit). Les `detail` spécifiques sont listés en ligne ci-dessous. --- ## POST /api/auth/signup Crée un utilisateur, envoie l'email de bienvenue + vérification, et ouvre une session. Sans auth. Limite : 3 / heure / IP. ### Corps de requête | Champ | Type | Notes | |-------|------|-------| | `email` | string (email) | Requis. | | `password` | string | 8 à 128 caractères, doit contenir une lettre ET un chiffre/symbole. | | `invitation_code` | string | 1 à 64 caractères. L'alpha est sur invitation. | | `accept_responsibility` | boolean | Doit valoir `true`. | | `hcaptcha_token` | string ou null | Requis quand `HCAPTCHA_SECRET` est configuré. | ```json { "email": "ada@example.com", "password": "lovelace-1843", "invitation_code": "ALPHA-7K2", "accept_responsibility": true, "hcaptcha_token": "10000000-aaaa-bbbb-cccc-000000000001" } ``` ### Réponse — `200 OK` ```json { "ok": true, "user": { "id": 42, "email": "ada@example.com", "is_admin": false, "is_active": true, "email_verified": false, "created_at": "2026-05-27T09:14:00Z" } } ``` Pose le cookie `outsend_session`. ### Erreurs spécifiques | Code | Detail | |------|--------| | `400` | `Captcha invalide. Réessaie.` | | `400` | `Code invitation invalide` | | `400` | `Email existe déjà` | --- ## POST /api/auth/login Valide les identifiants et ouvre une session. Sans auth. Limite : 5 / 15 min / IP et 5 / 15 min / email. ### Corps de requête | Champ | Type | Notes | |-------|------|-------| | `email` | string (email) | Requis. | | `password` | string | 1 à 128 caractères. | ```json { "email": "ada@example.com", "password": "lovelace-1843" } ``` ### Réponse — `200 OK` ```json { "ok": true, "user": { "id": 42, "email": "ada@example.com", "is_admin": false, "is_active": true, "email_verified": true, "created_at": "2026-05-27T09:14:00Z" } } ``` Pose le cookie `outsend_session`. ### Erreurs spécifiques | Code | Detail | |------|--------| | `401` | `Email ou mot de passe incorrect` | | `401` | `Compte désactivé` | --- ## POST /api/auth/logout Révoque la session courante et efface le cookie. Auth optionnelle. Corps vide. Réponse : `200 OK` `{ "ok": true }`. --- ## GET /api/auth/me Renvoie l'utilisateur authentifié. ```json { "id": 42, "email": "ada@example.com", "is_admin": false, "is_active": true, "email_verified": true, "created_at": "2026-05-27T09:14:00Z" } ``` --- ## POST /api/auth/password/reset-request Envoie un lien de réinitialisation à l'email si (et seulement si) il correspond à un utilisateur actif. La réponse est identique dans tous les cas pour prévenir l'énumération de comptes. Sans auth. Limite : 3 / heure / IP et 3 / heure / email (silencieux quand épuisé). ### Corps de requête ```json { "email": "ada@example.com" } ``` ### Réponse — `200 OK` ```json { "ok": true } ``` --- ## POST /api/auth/password/reset-confirm Consomme un jeton de réinitialisation à usage unique et fixe le nouveau mot de passe. Révoque toutes les sessions existantes de l'utilisateur. Sans auth (le jeton fait office d'identifiant). ### Corps de requête | Champ | Type | Notes | |-------|------|-------| | `token` | string | 10 à 256 caractères, livré par email. | | `new_password` | string | 8 à 128 caractères, lettre + chiffre/symbole. | ```json { "token": "eyJ...", "new_password": "babbage-1822" } ``` ### Réponse — `200 OK` ```json { "ok": true } ``` ### Erreurs spécifiques | Code | Detail | |------|--------| | `400` | `Lien invalide ou expiré` | | `422` | Complexité de mot de passe refusée par le validateur. | --- ## POST /api/auth/password/change Renouvelle le mot de passe d'un utilisateur connecté. Requiert le mot de passe actuel, révoque les autres sessions, émet un cookie frais. Limite : 5 / heure / utilisateur. ### Corps de requête | Champ | Type | Notes | |-------|------|-------| | `current_password` | string | 1 à 200 caractères. | | `new_password` | string | 8 à 200 caractères, doit différer de l'actuel. | ```json { "current_password": "lovelace-1843", "new_password": "babbage-1822" } ``` ### Réponse — `200 OK` ```json { "ok": true } ``` Pose un cookie `outsend_session` rafraîchi. ### Erreurs spécifiques | Code | Detail | |------|--------| | `400` | `Mot de passe actuel incorrect` | | `400` | `Le nouveau mot de passe doit être différent de l'actuel` | --- ## POST /api/auth/email/verify Consomme un jeton de vérification à usage unique et bascule `email_verified` à `true`. Sans auth. ### Corps de requête ```json { "token": "eyJ..." } ``` ### Réponse — `200 OK` ```json { "ok": true } ``` Erreur spécifique : `400 Lien de vérification invalide ou expiré`. --- ## POST /api/auth/email/resend-verify Renvoie l'email de vérification à l'utilisateur authentifié. Idempotent si l'adresse est déjà vérifiée. Corps vide. Limite : 3 / heure / utilisateur. ### Réponse — `200 OK` ```json { "ok": true } ``` ou, si déjà vérifié : ```json { "ok": true, "already_verified": true } ``` --- ## DELETE /api/auth/me Supprime définitivement le compte et tous les enregistrements possédés (jobs, pipelines, surveillances, sessions, jetons). Les fichiers de jobs sur disque sont purgés après le delete DB en cascade. Les fils de feedback sont anonymisés plutôt que supprimés. ### Corps de requête | Champ | Type | Notes | |-------|------|-------| | `confirm_email` | string | Doit égaler l'email de l'utilisateur (insensible à la casse). | ```json { "confirm_email": "ada@example.com" } ``` ### Réponse — `204 No Content` Corps vide. Efface le cookie `outsend_session`. Erreur spécifique : `400 Confirmation email incorrecte`. --- ## GET /api/auth/me/export Endpoint de portabilité RGPD. Diffuse une archive ZIP contenant tous les enregistrements possédés par l'utilisateur. ### Réponse — `200 OK` `Content-Type: application/zip` `Content-Disposition: attachment; filename="outsend-export--.zip"` Contenu de l'archive : | Entrée | Contenu | |--------|---------| | `account.json` | Métadonnées du compte, sans secrets. | | `jobs.json` | Tous les jobs avec métadonnées. | | `jobs//*` | Sorties CSV/JSON pour chaque job `done`. | | `pipelines.json` | Définitions de pipelines. | | `veille.json` | `recurring_scraps` + historique des runs. | | `manifest.txt` | Résumé lisible. | --- title: API Feedback slug: api/feedback section: API summary: Chat in-app avec l'admin de la plateforme et point d'entrée pour les demandes d'activation de modules on-demand. --- # API Feedback L'API Feedback alimente le chat in-app entre un utilisateur authentifié et l'admin de la plateforme. Elle sert aussi de point d'entrée pour les demandes d'activation de modules on-demand : cliquer "Demander" sur un module stub (email, SMS, WhatsApp, opérateur téléphonique) ouvre un fil de feedback avec un `topic` dédié, qui apparaît dans l'inbox "On demand" du dashboard admin. Un fil est une conversation stable épinglée à un `topic`. Chaque réponse est une ligne `feedback_message` rattachée à ce fil. L'état de lecture est tracké par rôle (user, admin) afin que chaque côté ne voie que son propre badge non-lu. Tous les endpoints requièrent un appelant authentifié. Erreurs génériques : `401` (non authentifié), `404` (fil inexistant). Les causes spécifiques sont en ligne. ## Conventions de topic Le champ `topic` d'un fil est une chaîne libre plafonnée à 64 caractères, mais le produit suit un petit jeu de conventions : | Valeur de topic | Signification | | ------------------------ | ------------------------------------------------ | | `general` | Défaut. Chat fourre-tout. | | `feedback` | Feedback produit générique. | | `bug` | Rapport de bug. | | `feature` | Demande de fonctionnalité. | | `on_demand_email` | Demande d'activation du stub campagne email. | | `on_demand_sms` | Demande d'activation du stub campagne SMS. | | `on_demand_whatsapp` | Demande d'activation du stub WhatsApp. | | `on_demand_phone_carrier`| Demande d'activation du stub opérateur tél. | Tout `topic` correspondant à `on_demand_*` est capté par l'endpoint admin `GET /api/admin/feedback/on-demand`, qui groupe les fils par topic et expose les compteurs ouverts. Les stubs on-demand sont listés dans le registre des modules sous `on_demand` ; un client peut lire le registre et construire `topic = "on_demand_" + slug`. Le champ plus court `type` (`bug`, `feature`, `other`) est indépendant du topic et ne porte que l'intention grossière pour le tri. --- ## POST /api/feedback/threads Crée un nouveau fil avec son premier message. Limite : 20 fils par utilisateur par heure. | Champ | Type | Notes | | -------------- | -------- | ------------------------------------------------- | | `type` | string | `bug`, `feature` ou `other`. Défaut `other`. | | `message` | string | 3 à 5000 caractères. Corps du premier message. | | `topic` | string | Optionnel. Défaut `general`. Max 64 caractères. | ### Requête ```json POST /api/feedback/threads { "type": "feature", "topic": "on_demand_whatsapp", "message": "L'envoi de relances WhatsApp sur les leads scrapés serait utile." } ``` ### Réponse — 201 Created ```json { "id": 142, "user_id": 7, "user_email": "user@example.com", "type": "feature", "status": "open", "created_at": "2026-05-27 10:11:12", "last_read_user": "2026-05-27 10:11:12", "last_read_admin": null, "messages": [ { "id": 991, "author_role": "user", "author_user_id": 7, "message": "L'envoi de relances WhatsApp sur les leads scrapés serait utile.", "created_at": "2026-05-27 10:11:12" } ], "preview": "L'envoi de relances WhatsApp sur les leads scrapés serait utile.", "last_message_at": "2026-05-27 10:11:12", "unread_for_me": 0 } ``` Causes spécifiques : `400` `type` hors de `{bug, feature, other}` ; `422` `message` plus court que 3 ou plus long que 5000 ; `429` plus de 20 fils dans la dernière heure. --- ## POST /api/feedback/threads/{thread_id}/messages Ajoute une réponse à un fil existant. L'appelant doit posséder le fil, et le fil ne doit pas être `closed`. Poster un message marque aussi le fil comme lu côté user. ### Requête ```json POST /api/feedback/threads/142/messages { "message": "Pour contexte : le tracking d'opt-out serait aussi nécessaire." } ``` ### Réponse — 201 Created Renvoie le fil sérialisé complet, identique en forme à la réponse de `POST /threads`, avec le message ajouté inclus. Causes spécifiques : `400` fil `closed` ; `403` appelant ne possède pas le fil ; `422` message vide ou plus long que 5000. --- ## GET /api/feedback/threads Liste les fils de l'appelant, les plus récents d'abord. Plafonné à 100 lignes. Chaque entrée embarque la liste complète des messages pour rendre previews et compteurs non-lus sans second aller-retour. ### Réponse — 200 OK ```json [ { "id": 142, "user_id": 7, "user_email": "user@example.com", "type": "feature", "status": "open", "created_at": "2026-05-27 10:11:12", "last_read_user": "2026-05-27 10:11:12", "last_read_admin": null, "messages": [ /* ... */ ], "preview": "L'envoi de relances WhatsApp...", "last_message_at": "2026-05-27 10:11:12", "unread_for_me": 0 } ] ``` Le compteur `unread_for_me` reflète les réponses admin pas encore vues, calculé à partir de `last_read_user`. L'endpoint compagnon `GET /api/feedback/unread` retourne le même nombre agrégé sur tous les fils, prêt à être lié à un badge d'en-tête. --- title: API Jobs slug: api/jobs section: API summary: Surface unifiée pour toutes les charges exécutées par Outsend — acquisition de sources, enrichissement, vérification, reporting. --- # API Jobs L'API Jobs est la surface unifiée pour toutes les charges qu'Outsend exécute pour un tenant : acquisition de sources (`scrap`) et les modules d'enrichissement, vérification et reporting qui opèrent sur les items résultants. Un job est la seule unité facturable. Voir aussi : - [Cycle de vie des jobs](/docs/fr/concepts/jobs-lifecycle) — pending → running → done | failed | cancelled | expired - [États et événements](/docs/fr/concepts/states-and-events) — référence des payloads SSE - [Limites](/docs/fr/concepts/limits) — quota EF, plafonds par job, rétention Tous les endpoints requièrent un cookie de session authentifié. Les endpoints qui créent ou mutent des jobs requièrent en plus un utilisateur actif ; `POST /api/jobs` et `POST /api/jobs/resume` exigent aussi un email vérifié. Les routes admin (`/api/admin/*`, `/api/jobs/queue`) ne sont pas documentées ici. ## Conventions | Élément | Valeur | |---|---| | URL de base | `https://outsend.xyz` | | Auth | Cookie de session (`outsend_session`) | | Content-Type | `application/json` pour les corps POST | | Identifiant de job | Chaîne opaque (`job.id`), stable durant la vie du job | | Horodatages | ISO 8601 UTC | ### L'objet `JobPublic` Tout endpoint qui retourne un job retourne la même forme : ```json { "id": "j_01HXYZ...", "job_type": "scrap", "queries": ["dentiste"], "zones": ["Paris", "75015"], "include_reviews": false, "status": "running", "grid_points_count": 412, "processed_points": 87, "results_count": 64, "error_count": 0, "ef_cost": 0.041, "created_at": "2026-05-27T09:12:03Z", "started_at": "2026-05-27T09:12:05Z", "completed_at": null, "expires_at": "2026-06-26T09:12:03Z", "error_message": null, "output_filename": null, "download_available": false, "source_job_id": null, "email_mode": null, "breakdown": { "by_query": {"dentiste": 64}, "by_zone": {"Paris": 64} }, "dead_queries": [], "flagged_tiles_count": 0, "total_attempts_count": 87, "query_stats": { "dentiste": { "tiles": 87, "with_results": 71 } } } ``` `status` prend l'une des valeurs `pending | running | done | failed | cancelled | expired`. ### Erreurs Tous les endpoints renvoient `{"detail": "..."}` (ou `{"detail": {"message": ..., "errors": [...]}}` pour les erreurs de validation). Codes génériques : `401` non authentifié, `403` non autorisé (autre tenant ou email non vérifié), `404` non trouvé, `422` validation Pydantic. Les causes spécifiques sont listées en ligne. --- ## Créer un job (générique) ``` POST /api/jobs ``` Crée un job `scrap` — la charge canonique d'acquisition qui exécute des requêtes sur une grille géographique. Pour toute autre charge, utiliser le raccourci typé décrit plus bas ; passer un champ `type` à `POST /api/jobs` n'est **pas** supporté. **Corps de requête** ```json { "queries": ["dentiste", "orthodontiste"], "zones": ["Paris", "75015", "Lyon 2e"], "include_reviews": false } ``` | Champ | Type | Notes | |---|---|---| | `queries` | `string[]` (1..20) | Chaque item ≤ 200 caractères, trimé, dédupliqué | | `zones` | `string[]` (1..50) | Noms de villes, codes postaux ou arrondissements ; résolus côté serveur | | `include_reviews` | `boolean` | Si `true`, récupère les derniers avis par POI (augmente le coût EF) | **Réponse** — `200 OK`, un `JobPublic` en statut `pending`. Causes spécifiques : `400` parsing de zone échoué / quota EF dépassé / grille vide ; `403` email non vérifié. --- ## Créer un job (raccourci typé) Chaque module d'enrichissement, vérification et reporting a un endpoint dédié qui accepte les items sur lesquels il opère. Chaque raccourci renvoie un `JobPublic` dont le `job_type` est fixé au slug du module. ``` POST /api/jobs/{type} ``` | `type` | Rôle | Doc module | |---|---|---| | `reviews` | Récupérer les derniers avis par POI | [reviews](/docs/fr/modules/reviews) | | `emails` | Découvrir les emails de contact depuis chaque site | [emails](/docs/fr/modules/emails) | | `verify-emails` | Vérification anti-bounce (sans VPN) | [verify-emails](/docs/fr/modules/verify-emails) | | `socials` | Détecter les profils sociaux liés | [socials](/docs/fr/modules/socials) | | `phones-extra` | Trouver des numéros au-delà du listing Maps | [phones-extra](/docs/fr/modules/phones-extra) | | `legal-ids` | Extraire SIRET / SIREN depuis le site | [legal-ids](/docs/fr/modules/legal-ids) | | `legal-mentions` | Parser la page mentions légales (capital, RCS, …) | [legal-mentions](/docs/fr/modules/legal-mentions) | | `legal-data` | Enrichir via SIRENE / INPI (`api.gouv.fr`) | [legal-data](/docs/fr/modules/legal-data) | | `pricing` | Extraire les tarifs SaaS / B2B | [pricing](/docs/fr/modules/pricing) | | `techstack` | Détecter CMS, frameworks, analytics, paiement, CRM | [techstack](/docs/fr/modules/techstack) | | `pagespeed` | Score via Google PSI API v5 | [pagespeed](/docs/fr/modules/pagespeed) | | `ads-intelligence` | Profilage marketing/ads (pixels, CMP, retargeting) | [ads-intelligence](/docs/fr/modules/ads-intelligence) | | `brand-assets` | Logo, favicon, palette, screenshot optionnel | [brand-assets](/docs/fr/modules/brand-assets) | | `dead-check` | Marquer les sites morts (DNS, parking, default-server, SSL) | [dead-check](/docs/fr/modules/dead-check) | | `delivery-check` | Test de placement Gmail Inbox / Promotions / Spam | [delivery-check](/docs/fr/modules/delivery-check) | **Corps de requête (forme partagée par tous les modules par item)** ```json { "items": [ { "nom": "Cabinet Dupont", "site_web": "https://dupont-dentiste.fr", "ville": "Paris" } ], "source_job_id": "j_01HXYZ..." } ``` | Champ | Type | Notes | |---|---|---| | `items` | `dict[]` (1..10 000) | Clés spécifiques au module ; généralement un sous-ensemble du CSV d'un job précédent | | `source_job_id` | `string?` | Chaîne le nouveau job à un précédent, utilisé pour traçabilité et affichage facturation | **Surcharges spécifiques aux modules** - `POST /api/jobs/emails` — accepte `mode: "normal" | "deep"` (défaut `normal`). - `POST /api/jobs/brand-assets` — accepte `capture_screenshot: boolean` (défaut `false`, ~5× plus lent par item quand activé). - `POST /api/jobs/delivery-check` — ne prend **pas** `items`. Corps : ```json { "domain": "example.com", "subject_filter": "outsend" } ``` **Réponse** — `200 OK`, un `JobPublic` en statut `pending`. Cause additionnelle : `422` si `items` est vide, trop grand ou si des clés requises par le module manquent. --- ## Lister les jobs ``` GET /api/jobs?limit={n}&offset={n} ``` Renvoie les jobs de l'utilisateur authentifié, les plus récents d'abord. | Param | Type | Défaut | Plage | |---|---|---|---| | `limit` | `int` | `100` | borné à `[1, 500]` | | `offset` | `int` | `0` | `≥ 0` | **Réponse** — `200 OK`, `JobPublic[]`. --- ## Récupérer un job ``` GET /api/jobs/{id} ``` **Réponse** — `200 OK`, un seul `JobPublic`. Inclut des compteurs live (`processed_points`, `results_count`, `query_stats`, `breakdown`) que le tableau de bord interroge entre les événements SSE. --- ## Suivre la progression en direct (SSE) ``` GET /api/jobs/{id}/stream?since={log_id} ``` Flux Server-Sent Events qui émet les transitions de statut, lignes de log et mises à jour de compteurs au fur et à mesure de l'avancement. Les reconnexions honorent automatiquement `Last-Event-ID` ; le paramètre `since` est un fallback pour clients qui ne parlent pas SSE nativement. La taxonomie d'événements (`status`, `log`, `progress`, `done`, `error`) et les payloads sont documentés dans [États et événements](/docs/fr/concepts/states-and-events). **En-têtes renvoyés** ``` Content-Type: text/event-stream Cache-Control: no-cache X-Accel-Buffering: no ``` --- ## Lister les items d'un job ``` GET /api/jobs/{id}/items?offset={n}&limit={n} ``` Renvoie les lignes du CSV de sortie en JSON, pour chaînage vers un job d'enrichissement. Disponible uniquement pour les jobs dont `status == "done"` et dont le `job_type` produit un CSV réutilisable (c.-à-d. ni `delivery_check` ni `viewport_test`). **Réponse** — `200 OK` ```json { "count": 412, "items": [ { "nom": "Cabinet Dupont", "site_web": "https://...", "telephone": "+33 1 ...", "...": "..." } ] } ``` Causes spécifiques : `400` job non terminé ou job_type sans sortie réutilisable ; `410` CSV expiré ou supprimé. --- ## Télécharger le résultat d'un job ``` GET /api/jobs/{id}/download?format=csv|json|xlsx ``` Télécharge la sortie du job. Le CSV est l'artefact canonique écrit par le worker (UTF-8 BOM, séparateur `;`) ; JSON et XLSX sont dérivés à la volée. Tous les exports passent par un sanitiseur d'injection de formules tableur. | `format` | Media type | Nom de fichier | |---|---|---| | `csv` (défaut) | `text/csv; charset=utf-8` | `{job.output_filename}` | | `json` | `application/json; charset=utf-8` | `{base}.json` | | `xlsx` | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` | `{base}.xlsx` | Causes spécifiques : `400` job encore pending/running ou `format` non supporté ; `410` sortie expirée, manquante, ou job échoué avant la première ligne. --- ## Arrêter un job ``` POST /api/jobs/{id}/cancel ``` Arrête un job `pending` ou `running`. Les résultats déjà extraits sont **conservés** (téléchargeables en CSV partiel et réutilisables). Renvoie `400` si le job est déjà terminal. Si le job appartient à un pipeline, l'arrêt **met la pipeline en pause** sur cette étape — les étapes suivantes ne sont **pas** lancées automatiquement (idem en cas de crash). Pour poursuivre la chaîne avec les résultats partiels, l'utilisateur déclenche explicitement [`POST /api/pipelines/{id}/nodes/{node_id}/continue`](./pipelines.md) (bouton « Continuer avec les résultats »). Pour arrêter au contraire la pipeline entière, utiliser [`POST /api/pipelines/{id}/cancel`](./pipelines.md). **Réponse** — `200 OK`, `{"ok": true}`. --- ## Reprendre un job ``` POST /api/jobs/{id}/resume ``` Crée un **nouveau** job qui reprend un `scrap` `cancelled` ou `failed` là où il s'est arrêté. Le nouveau job hérite des queries, zones et CSV partiel de la source ; le worker saute les coordonnées déjà traitées. EF est débité uniquement pour les points restants. **Réponse** — `200 OK`, un nouveau `JobPublic` (le job de reprise) en statut `pending`. Son `source_job_id` référence l'original. Causes spécifiques : `400` job source non reprisable (mauvais type, non interrompu, ou déjà entièrement traité) ; `403` email non vérifié. --- ## Supprimer un job ``` DELETE /api/jobs/{id} ``` Supprime définitivement le job et son CSV. Refuse de supprimer un job encore en cours — il faut l'arrêter d'abord. **Réponse** — `204 No Content`. Cause spécifique : `400` job encore en cours. --- ## Estimer le coût EF ``` POST /api/estimate ``` Calcule le coût EF d'un job `scrap` hypothétique sans le créer. Alimente le compteur de coût live du formulaire de lancement. L'estimation est gratuite et non comptée. **Corps de requête** — même forme que `POST /api/jobs`, mais `queries` et `zones` peuvent être vides (renvoie `valid: false`). **Réponse** — `200 OK`, un `JobEstimateResponse` : ```json { "valid": true, "grid_points": 412, "total_requests": 824, "queries_count": 2, "ef_cost": 0.041, "estimated_duration_seconds": 1380, "errors": [], "warnings": [] } ``` | Champ | Signification | |---|---| | `valid` | `true` ssi `errors` est vide | | `grid_points` | Tuiles GPS distinctes sur l'union des zones | | `total_requests` | `grid_points × len(queries)` — ce que le worker appellera réellement | | `queries_count` | Reflète `len(queries)` pour l'affichage UI | | `ef_cost` | Unités équivalent France ; voir [Limites](/docs/fr/concepts/limits) | | `estimated_duration_seconds` | Estimation au mieux du temps horloge | | `errors` | Bloqueurs durs (hors-quota, zones impossibles à parser, grille vide) | | `warnings` | Signaux doux (non utilisés actuellement) | --- ## Notes sur les endpoints omis Les routes suivantes existent mais ne font pas partie de la surface publique : - `GET /api/jobs/queue` — file globale anonymisée pour le widget public. Sans tenant, périmètre séparé. - `/api/admin/*` — réservé opérateur. - `GET /api/jobs/{id}/breakdown`, `GET /api/jobs/{id}/map`, `GET /api/jobs/{id}/output-columns`, `GET /api/jobs/{id}/delivery-result`, `POST /api/jobs/parse-list`, `GET /api/brand-lookup`, `GET /api/brand-assets/{owner}/{filename}`, `GET /api/delivery-check/seeds` — helpers internes UI susceptibles de changer sans préavis. --- title: Vue d'ensemble API slug: api/overview section: API summary: Conventions partagées par tous les endpoints de l'API Outsend — URL de base, authentification, types de contenu, versioning, erreurs. --- # Vue d'ensemble API L'API Outsend expose la même surface que l'application web. Le tableau de bord et l'API partagent un backend, un schéma d'authentification et un ensemble d'objets uniques. ## URL de base ``` https://outsend.xyz ``` Les endpoints sous `/api/` renvoient du JSON ou diffusent des événements. L'URL de base est stable durant l'alpha. ## Authentification Les sessions utilisent un cookie nommé `outsend_session`. Pour l'obtenir, envoyer les identifiants : ``` POST /api/auth/login Content-Type: application/json { "email": "nom@example.com", "password": "..." } ``` La réponse pose `outsend_session` en `HttpOnly`, `Secure`, `SameSite=Lax`. Les requêtes suivantes doivent l'inclure. Les sessions restent valides jusqu'à la déconnexion (`POST /api/auth/logout`) ou expiration. Les requêtes sans cookie valide reçoivent `401` sur les routes protégées. Des jetons API scopés par workspace sont prévus à la feuille de route ; les sessions par cookie sont actuellement le seul mécanisme supporté. ## Types de contenu | Surface | Type de contenu | Notes | |---|---|---| | Endpoints lecture/écriture | `application/json` | UTF-8, champs en snake_case | | Flux d'événements | `text/event-stream` | Server-Sent Events | | Téléchargements | `application/octet-stream` et apparentés | Endpoints terminant par `/download` | | Exports tabulaires | `text/csv`, `application/json`, `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` | Sélectionné via `?format=csv|json|xlsx` | Les endpoints acceptant `format` retournent du JSON par défaut. ## Versioning L'API est en alpha. Aucun préfixe `/v1/`, aucun header de version — la surface évolue sur place. Les ruptures sont annoncées à l'avance via le changelog et, le cas échéant, par bannières in-app. Les ajouts (nouveaux champs, endpoints, paramètres optionnels) sont livrés sans préavis. Un préfixe versionné sera introduit avant la disponibilité générale. ## Limites de débit Les endpoints sensibles (authentification, contact, création de jobs) sont protégés par des quotas par route. Le dépassement renvoie `429` avec un en-tête `Retry-After`. Voir [/docs/fr/concepts/limits](/docs/fr/concepts/limits). ## Erreurs Les échecs renvoient un corps JSON et un code HTTP conventionnel : ```json { "detail": "Message lisible", "errors": [ { "field": "email", "message": "Format invalide" } ] } ``` Le tableau `errors` n'est présent que lorsque l'échec concerne des champs spécifiques. | Code | Signification | |---|---| | 400 | Requête malformée, violation de règle métier | | 401 | Pas de session, ou session expirée | | 403 | Authentifié mais non autorisé ; également pour comptes désactivés | | 404 | Ressource inexistante, ou non visible par l'appelant | | 422 | Requête bien formée mais échec de validation | | 429 | Limite de débit atteinte ; réessayer après la valeur du header | | 5xx | Défaillance côté serveur ; les retries avec backoff sont sûrs | Considérer tout 5xx comme transitoire et appliquer un backoff exponentiel. ## Groupes d'endpoints | Groupe | Chemin | Rôle | |---|---|---| | Authentification | [/docs/fr/api/auth](/docs/fr/api/auth) | Login, logout, inscription, reset mot de passe, vérification email | | Jobs | [/docs/fr/api/jobs](/docs/fr/api/jobs) | Créer, lister, inspecter, contrôler et exporter des jobs | | Pipelines | [/docs/fr/api/pipelines](/docs/fr/api/pipelines) | Composer des workflows multi-étapes et les exécuter | | Veille | [/docs/fr/api/veille](/docs/fr/api/veille) | Monitoring continu de requêtes et de sources | | Feedback | [/docs/fr/api/feedback](/docs/fr/api/feedback) | Soumettre du feedback produit et des rapports de bug | | Registre | [/docs/fr/api/registry](/docs/fr/api/registry) | Découvrir les types de jobs disponibles et leurs paramètres | ## Protocole SSE Les opérations longues exposent leur progression via Server-Sent Events. Noms d'événements, forme de payload et machine à états sont documentés sur [/docs/fr/concepts/states-and-events](/docs/fr/concepts/states-and-events). --- title: API Pipelines slug: api/pipelines section: API summary: Composer et exécuter des DAG d'étapes de scraping, enrichissement et transformation sous /api/pipelines. --- # API Pipelines Un pipeline est un graphe orienté acyclique (DAG) de nodes qui enchaîne des étapes de scraping, enrichissement et transformation. Soumettre un pipeline lance les jobs racines de façon synchrone ; les jobs en aval sont engendrés à mesure que chaque prédécesseur atteint `done`. Tous les endpoints sont sous `/api/pipelines` et requièrent une session authentifiée. Les routes mutantes requièrent en plus un compte actif (non suspendu). Erreurs génériques : `401` pas de session, `403` non propriétaire / compte suspendu, `404` pipeline ou node inconnu — les causes spécifiques sont listées en ligne. Voir aussi : [Orchestration de pipelines](/docs/fr/concepts/pipeline-orchestration) et [Module filter](/docs/fr/modules/filter). ## Forme du graphe Le document `definition` décrit le DAG. Les edges sont explicites et référencent des ids de node ; ils ne sont pas inférés d'un champ `inputs` par node. ```json { "nodes": [ {"id": "n1", "type": "scrap", "config": {"queries": ["dentist"], "zones": ["Paris"]}, "x": 100, "y": 100}, {"id": "n2", "type": "emails", "config": {"mode": "normal"}, "x": 320, "y": 100}, {"id": "n3", "type": "verify", "config": {}, "x": 540, "y": 100} ], "edges": [ {"id": "e1", "from": "n1", "to": "n2"}, {"id": "e2", "from": "n2", "to": "n3"} ] } ``` ### Types de node | Type | Rôle | Accepte (entrée) | Produit (sortie) | |-------------------|------------------------------------------|-------------------|-------------------| | `scrap` | Scrape Google Maps (racine) | aucune | `pois` | | `import` | Import CSV/Sheets (racine) | aucune | `pois` | | `reviews` | Récupérer les avis des POIs | `pois_any` | `reviews` | | `emails` | Découvrir les emails depuis les sites | `pois_any` | `pois_email` | | `verify` | Vérification SMTP des emails | `pois_email` | `verified` | | `socials` | Découvrir les profils sociaux | `pois_any` | `pois` | | `dead_check` | Détecter les POIs inactifs | `pois_any` | `pois` | | `techstack` | Détecter la pile tech du site | `pois_any` | `pois` | | `ads_intelligence`| Détecter les campagnes publicitaires | `pois_any` | `pois` | | `brand_assets` | Extraire logo et assets de marque | `pois_any` | `pois` | | `filter` | Appliquer un filtre à base de règles | `any_pois` | passthrough | | `sort` | Réordonner les lignes par colonne | `any_pois` | passthrough | `filter` et `sort` préservent le type amont ; la compatibilité de types est résolue en remontant jusqu'au premier ancêtre non-passthrough. ### Règles de validation Le serveur rejette une définition avec HTTP 400 si l'une des conditions suivantes est vraie : | Règle | Message d'erreur | |-------------------------------------------------|-------------------------------------------------| | Liste `nodes` vide | `Pipeline vide` | | Plus de 20 nodes | `Trop de nodes (max 20)` | | Id de node en doublon | `IDs de nodes en doublon` | | `type` inconnu | `Type de node inconnu : ...` | | Extrémité d'edge référence un node manquant | `Edge référence un node inexistant` | | Boucle (`from == to`) | `Edge vers soi-même interdit` | | Type racine branché comme successeur | `Le node '...' ne peut pas avoir de prédécesseur` | | Sortie incompatible avec entrée | `Connexion X → Y incompatible` | | Node avec plusieurs prédécesseurs (limite MVP) | `Le node ... a plusieurs prédécesseurs` | Les racines doivent être `scrap` ou `import`. Le fan-out (un node alimentant plusieurs successeurs) est autorisé ; le fan-in ne l'est pas. --- ## POST /api/pipelines Crée un pipeline et lance ses jobs racines. **Corps de requête** ```json { "name": "Dentistes Paris", "definition": { "nodes": [...], "edges": [...] } } ``` `name` est optionnel (≤ 120 caractères, défaut `"Pipeline"`). **Réponse 201** ```json { "id": "f1a2…-uuid", "status": "running", "initial_jobs": ["job_abc", "job_def"] } ``` Cause spécifique : `400` la définition échoue à une règle de validation, ou la création du job racine échoue. En cas d'échec racine, le pipeline est persisté avec `status = failed`. --- ## GET /api/pipelines Liste les pipelines de l'appelant (plus récents d'abord, plafonné à 50). **Réponse 200** ```json [ { "id": "f1a2…", "name": "Dentistes Paris", "status": "running", "created_at": "2026-05-27 10:14:02", "completed_at": null, "nodes_count": 3 } ] ``` `status` prend l'une des valeurs `pending | running | done | failed | cancelled`. `nodes_count` est dérivé de la définition stockée. --- ## GET /api/pipelines/{id} Retourne un pipeline avec sa définition et les jobs engendrés jusqu'ici. **Réponse 200** ```json { "id": "f1a2…", "user_id": 42, "name": "Dentistes Paris", "definition": { "nodes": [...], "edges": [...] }, "status": "running", "created_at": "2026-05-27 10:14:02", "completed_at": null, "progress_pct": 42, "jobs": [ { "id": "job_abc", "job_type": "scrap", "status": "done", "pipeline_node_id": "n1", "results_count": 187, "error_message": null, "created_at": "2026-05-27 10:14:02", "completed_at": "2026-05-27 10:18:55" } ], "output_job": { "id": "job_xyz", "job_type": "verify_emails", "results_count": 142, "status": "done", "download_available": true } } ``` `output_job` est le **jeu de données final** du pipeline — la sortie de l'étape la plus en aval ayant réellement produit des lignes (un pipeline filtre/réduit, il n'additionne pas ; `output_job.results_count` correspond donc au compteur affiché, pas à la somme des étapes). On le télécharge via [`GET /api/jobs/{id}/download`](jobs.md#get-apijobsiddownload) avec `output_job.id`, en `csv` / `xlsx` / `json`. Il vaut `null` tant que le pipeline n'a rien produit de téléchargeable, et `download_available` indique si un CSV (final **ou** partiel — donc valable aussi pour un pipeline en cours/arrêté) est encore présent sur disque et non expiré. --- ## PATCH /api/pipelines/{id} Non implémenté. L'API actuelle n'expose pas de mutation du graphe après création ; cloner le pipeline en relançant `POST /api/pipelines` avec une définition mise à jour. Retourne `405 Method Not Allowed`. --- ## DELETE /api/pipelines/{id} Non implémenté. Les pipelines sont immuables une fois créés ; la suppression sera ajoutée lorsque la politique de rétention sera définie. Retourne `405 Method Not Allowed`. --- ## POST /api/pipelines/{id}/cancel Arrête le pipeline **entier** : l'étape en cours est stoppée et les étapes restantes ne sont **pas** lancées. Les résultats partiels déjà extraits restent téléchargeables sur chaque job concerné. À distinguer de l'arrêt d'une seule étape (`POST /api/jobs/{id}/cancel`), qui met simplement la pipeline en pause sur cette étape. Le pipeline passe en statut `cancelled`. Renvoie `400` si le pipeline est déjà terminal (`done`, `failed`, `cancelled`). **Réponse** — `200 OK`, `{"ok": true}`. --- ## POST /api/pipelines/{id}/nodes/{node_id}/continue « Continuer avec les résultats ». Quand une étape de scraping a été **arrêtée** (assez de résultats) ou a **crashé**, la pipeline se met en pause sur cette étape (les suivantes ne démarrent pas automatiquement). Cet endpoint relance la chaîne à partir de cette étape en consommant ses **résultats partiels** : il crée le(s) job(s) de l'étape suivante. Conditions : le dernier job du node est `cancelled` ou `failed`, et la pipeline n'est pas terminale. Si l'étape n'a produit aucune ligne, la branche est court-circuitée (pas de successeur créé). Renvoie `400` si l'étape n'est pas dans un état poursuivable. **Réponse** — `200 OK`, `{"ok": true}`. --- ## POST /api/pipelines/{id}/run Non implémenté. Les pipelines démarrent automatiquement à la création via `POST /api/pipelines` ; aucun endpoint de run séparé. Pour ré-exécuter un graphe, le re-poster comme nouveau pipeline. --- ## GET /api/pipelines/{id}/nodes/{node_id}/input-columns Inspecte le schéma du CSV qui alimentera un node donné. Utile pour construire des UIs de filtre. **Comportement.** L'endpoint localise le job prédécesseur le plus récent du node. Si le prédécesseur n'est pas encore `done`, la réponse porte une liste `columns` vide et un code `reason`. Sinon le CSV de sortie est lu (jusqu'à 5000 lignes) et chaque colonne est profilée pour type, taux de remplissage et exemples. **Réponse 200 — prédécesseur terminé** ```json { "columns": [ { "name": "telephone", "type": "phone", "fill_rate": 0.92, "sample_values": ["+33 1 23 45 67 89", "0612345678"], "distinct_count": null }, { "name": "categorie", "type": "category", "fill_rate": 1.0, "sample_values": ["dentiste", "orthodontiste"], "distinct_count": 4, "distinct_values": ["dentiste", "endodontiste", "orthodontiste", "stomatologue"] } ], "row_count": 187, "predecessor_job_id": "job_abc" } ``` `type` prend l'une des valeurs `phone | email | url | number | category | text`. Une colonne est étiquetée `category` uniquement si elle a entre 1 et 200 valeurs non vides distinctes ; sinon elle retombe à `text`. Un verdict typé requiert ≥ 80% des valeurs non vides correspondant au pattern. **Réponse 200 — pas d'entrée utilisable** ```json { "columns": [], "reason": "no_predecessor" } ``` | `reason` | Signification | |-------------------|----------------------------------------------------------| | `no_predecessor` | Le node est une racine, ou n'a pas encore d'edge entrant.| | `no_data_yet` | Le job prédécesseur existe mais n'est pas `done`. | | `no_csv_found` | Le prédécesseur a fini mais aucun CSV n'est sur disque. | | `csv_read_error` | Le fichier CSV n'a pas pu être parsé. | --- ## POST /api/pipelines/{id}/nodes/{node_id}/filter-preview Applique un jeu de règles de filtre en mémoire contre le CSV amont et renvoie le nombre de correspondances plus un petit échantillon. Aucun job n'est créé ; aucun état n'est muté. Le node cible doit être de type `filter`. Le corps utilise la même forme `rules` que les nodes `filter` persistent dans leur `config.rules` ; les previews sont calculés par la même fonction que le worker utilise à l'exécution, donc le compte fait autorité pour les données inspectées. **Corps de requête** ```json { "rules": { "logic": "AND", "conditions": [ {"column": "fill_rate", "op": ">=", "value": 0.5}, {"column": "categorie", "op": "in", "value": ["dentiste", "orthodontiste"]} ] } } ``` La grammaire exacte des règles est définie par le module filter (voir [Module filter](/docs/fr/modules/filter)). **Réponse 200** ```json { "total": 187, "matched": 73, "samples": [ {"nom": "Cabinet Dupont", "telephone": "0123456789", "categorie": "dentiste"} ], "predecessor_job_id": "job_abc", "fieldnames": ["nom", "telephone", "categorie", "site_web"], "capped": false } ``` `samples` contient jusqu'à 5 lignes correspondantes avec les champs vides retirés. `capped` vaut `true` quand le CSV amont a dépassé la limite preview de 5000 lignes — dans ce cas `total` ne reflète que la fenêtre inspectée, mais le ratio `matched/total` reste représentatif. Quand le prédécesseur n'est pas prêt, la réponse est la même squelette `{total, matched, samples, reason}` avec tous les compteurs à `0`. Les codes `reason` possibles miroitent l'endpoint input-columns : `no_predecessor`, `no_data_yet`, `no_csv_found`. Causes spécifiques : `400` node cible n'est pas de type `filter`, ou application de règle a levé une erreur ; `500` CSV non lisible. --- ## Résumé du cycle de vie 1. `POST /api/pipelines` valide le graphe, persiste le pipeline en `running`, et engendre un job par node racine. 2. Quand un job se termine **normalement** (`done`), le worker lit son CSV, transforme les lignes pour le type d'entrée du successeur et crée le job suivant. Une sortie vide court-circuite la branche. 3. Quand un job est **arrêté** (`cancelled`) ou **crashe** (`failed`), la pipeline se met **en pause** sur cette étape : aucune suite automatique. L'utilisateur reprend explicitement via [`POST /api/pipelines/{id}/nodes/{node_id}/continue`](#post-apipelinesidnodesnode_idcontinue) (« Continuer avec les résultats »), qui crée le job suivant à partir du CSV partiel. 4. La pipeline est finalisée en `done` quand toutes les étapes atteignables sont terminales et résolues. Un arrêt global (`POST /api/pipelines/{id}/cancel`) la finalise en `cancelled`. Un job `expired` la finalise en `failed`. Une étape `cancelled`/`failed` non poursuivie ne finalise rien : la pipeline reste `running` (en pause) jusqu'à un « Continuer » ou un arrêt global. --- title: API Registre des modules slug: api/registry section: API summary: Source de vérité unique listant tous les modules exposés par la plateforme — scrapers actifs, stubs on-demand, méta-fonctionnalités, items à venir. --- # API Registre des modules Le Registre de modules est la source de vérité unique qui liste tous les modules exposés par la plateforme : scrapers actifs, stubs on-demand, méta-fonctionnalités et items à venir qui collectent encore des votes d'intérêt. Le frontend lit le registre au lieu de coder en dur les slugs de modules, donc ajouter un module ne demande que deux fichiers (`frontend/static/job_types.js` et `app/job_registry.py`). Voir aussi : [/docs/fr/concepts/module-registry](/docs/fr/concepts/module-registry). ## Forme d'une entrée Chaque module est décrit par un petit objet que le frontend rend dans les tuiles du tableau de bord, la palette de recherche et les pages tarif. | Champ | Type | Rôle | | -------------- | -------------- | ----------------------------------------------------------------------- | | `slug` | string | Identifiant stable. Utilisé comme job_type, paramètre de route et clé. | | `category` | string | Bucket de groupe (`sources`, `enrich`, `signals`, `outreach`, `tools`). | | `label` | objet | `{ "fr": "...", "en": "..." }`. Nom d'affichage bilingue. | | `needs` | string[] | Artefacts amont consommés (ex. `["leads"]`). | | `produces` | string[] | Artefacts aval émis (ex. `["emails"]`). | | `pipelinable` | boolean | Le module peut-il être chaîné dans un Pipeline. | | `is_on_demand` | boolean | Module stub — cliquer activer ouvre un fil de feedback, pas un job. | | `coming_soon` | boolean | Listé pour le vote d'intérêt seulement. Pas d'exécution backend. | | `alpha_unavailable` | boolean | Construit et listé comme actif, mais gelé pendant l'alpha. Son endpoint de création renvoie `503`. | | `api_endpoint` | string \| null | Chemin appelé par le tableau de bord pour lancer un run, ou `null`. | Un module est au plus l'un de `is_on_demand`, `coming_soon`, `alpha_unavailable`, ou actif. Les modules actifs ont un `api_endpoint` non-null ; les stubs et coming-soon ont `api_endpoint = null`. Un module `alpha_unavailable` se présente comme actif et garde un `api_endpoint` non-null, mais cet endpoint renvoie `503` tant que le gel alpha est en vigueur. --- ## GET /api/modules-registry Endpoint public. Renvoie le miroir côté serveur du registre JS. La réponse est un objet plat avec un tableau par bucket plus un mapping `feature_pages` qui pointe chaque module actif vers sa page de vente publiée `/features/` (ou `null` si pas encore écrite). ### Réponse — 200 OK ```json { "active": [ "ads_intelligence", "brand_assets", "dead_check", "delivery_check", "emails", "filter", "import", "legal_data", "legal_ids", "legal_mentions", "pagespeed", "phones_extra", "pricing", "reviews", "scrap", "socials", "sort", "techstack", "verify_emails", "viewport_test" ], "multi_proxy": [ "dead_check", "emails", "legal_ids", "legal_mentions", "phones_extra", "pricing", "reviews", "scrap", "socials", "techstack" ], "parallel": [ "ads_intelligence", "brand_assets", "delivery_check", "filter", "import", "legal_data", "pagespeed", "sort", "verify_emails", "viewport_test" ], "on_demand": [ "email_campaign", "phone_carrier", "sms_campaign", "whatsapp_campaign" ], "meta": ["pipeline", "veille"], "coming_soon": [ "ai_personalization", "ai_team_members", "bing_places", "campaign", "chrome_extension", "crm", "directories", "email_warmup", "funding", "hiring", "integrations", "job_changes", "linkedin", "mobile_phones", "multichannel", "natural_filter", "pagesjaunes", "press_monitoring", "public_api", "review_patterns", "seo_data", "tech_adoption", "tracking", "whatsapp", "yelp_tripadvisor" ], "alpha_unavailable": ["finance"], "feature_pages": { "scrap": "scraper-google-maps-gratuit-export-csv", "emails": "email-finder-pro-rgpd-france", "ads_intelligence": null } } ``` L'ensemble `multi_proxy` liste les scrapers qui partagent le pool VPN global — un seul peut tourner à la fois à l'échelle plateforme. Les modules `parallel` utilisent HTTP direct et peuvent tourner en concurrence. Les clients qui planifient des jobs devraient vérifier les deux ensembles pour faire ressortir des avertissements "Sera en file". --- ## GET /api/features Renvoie l'état d'intérêt de l'appelant plus un compteur global par fonctionnalité coming-soon. Les comptes incluent chaque id de fonctionnalité autorisée, même ceux à zéro vote, afin que le frontend puisse rendre les libellés `Souhaité (N)` sans branche de fallback. La liste des ids acceptables égale `coming_soon` du registre, plus un petit ensemble legacy (`company`, `monitoring`, `pagespeed`) conservé pour préserver les votes historiques. ### Réponse — 200 OK ```json { "voted": ["linkedin", "funding"], "counts": { "linkedin": 27, "funding": 14, "hiring": 6, "ai_personalization": 3, "directories": 0, "press_monitoring": 0 } } ``` Cause spécifique : `401` appelant non authentifié. --- ## POST /api/features/{feature_id}/interest Enregistre un vote d'intérêt pour `feature_id`. L'opération est idempotente — un second appel par le même utilisateur est un no-op. Utiliser `DELETE` sur le même chemin pour retirer le vote. `feature_id` est validé contre l'allow-list du registre (ids coming-soon plus ids legacy). Les ids inconnus renvoient 404 afin que l'endpoint ne puisse pas servir de KV store écrit-partout. ### Requête ```json POST /api/features/linkedin/interest ``` Pas de corps. L'utilisateur est identifié par session. ### Réponse — 204 No Content Corps vide. Re-récupérer `GET /api/features` pour le compteur mis à jour. Causes spécifiques : `401` non authentifié ; `403` authentifié mais pas actif (invitation en attente) ; `404` `feature_id` hors allow-list du registre. --- ## Liens - [Concept de registre de modules](/docs/fr/concepts/module-registry) - [API Feedback](/docs/fr/api/feedback) — utilisée par les stubs on-demand pour faire remonter les demandes d'activation dans le dashboard admin. --- title: API Veille slug: api/veille section: API summary: Monitoring récurrent de scrapes et pipelines, avec buckets de diff et signaux de réputation. --- # API Veille L'API Veille gère les jobs de monitoring récurrents. Une *veille* rejoue un scrape source — ou un pipeline entier — à une cadence fixe, puis calcule un diff contre le run précédent pour faire ressortir ce qui a changé. Voir [Concepts de monitoring Veille](/docs/fr/concepts/veille-monitoring) pour le cycle de vie, l'ordonnancement et le modèle de diff. Tous les endpoints sont sous `/api/veille` et requièrent une session authentifiée et active. Les réponses sont en JSON. La propriété de ressource est appliquée à chaque requête : un accès cross-user renvoie `404`. Erreurs génériques : `401` (pas de session) et `404` (non trouvé / non propriétaire) ; les causes spécifiques sont en ligne. ## Modèle de ressource Un objet `Veille` expose les champs suivants : | Champ | Type | Description | | -------------------- | --------------- | ------------------------------------------------------------ | | `id` | entier | Identifiant stable. | | `name` | string | Libellé lisible (2–200 caractères). | | `source_job_id` | string \| null | Scrape source rejoué à chaque tick (exclusif avec `source_pipeline_id`). | | `source_pipeline_id` | string \| null | Pipeline source rejoué à chaque tick. | | `frequency_days` | entier | Cadence en jours, entre `1` et `365`. | | `status` | string | `active`, `paused` ou `deleted`. | | `next_run_at` | string (ISO8601)| Prochaine exécution planifiée. | | `last_run_at` | string \| null | Horodatage du dernier run terminé. | | `last_run_job_id` | string \| null | Id de job du dernier run. | | `run_count` | entier | Total de runs réussis. | | `created_at` | string (ISO8601)| Horodatage de création. | ## Endpoints ### Lister les veilles `GET /api/veille` Renvoie les veilles actives et en pause de l'appelant. Les entrées soft-deleted sont exclues. **Réponse** `200 OK` — `{ "items": [Veille, ...] }`. ### Créer une veille `POST /api/veille` Crée un monitor récurrent à partir d'un scrape ou pipeline terminé appartenant à l'appelant. Exactement un de `source_job_id` ou `source_pipeline_id` est requis. **Corps de requête** | Champ | Type | Requis | Notes | | -------------------- | ------- | ------ | -------------------------------------- | | `name` | string | oui | 2–200 caractères. | | `source_job_id` | string | un des | 8–64 caractères. | | `source_pipeline_id` | string | un des | 8–64 caractères. | | `frequency_days` | entier | oui | `1` ≤ valeur ≤ `365`. | ```json { "name": "Plombiers Lyon 3", "source_job_id": "job_8f2c91a4", "frequency_days": 7 } ``` **Réponse** `200 OK` — la veille nouvellement créée. Cause spécifique : `400` échec de validation (champs source manquants/doubles, source non possédée, source non terminée, fréquence invalide). ### Récupérer une veille `GET /api/veille/{id}` Renvoie une veille appartenant à l'appelant. Les entrées soft-deleted renvoient `404`. ### Mettre à jour une veille `PATCH /api/veille/{id}` Patche les champs mutables. Les champs omis sont laissés inchangés. **Corps de requête** | Champ | Type | Notes | | ---------------- | ------- | ------------------------------------------------------ | | `name` | string | 2–200 caractères. | | `frequency_days` | entier | `1` ≤ valeur ≤ `365`. Reprogramme `next_run_at`. | | `status` | string | `active`, `paused` ou `deleted`. | ```json { "status": "paused", "frequency_days": 14 } ``` **Réponse** `200 OK` — la veille mise à jour. Cause spécifique : `400` valeur de champ invalide. ### Supprimer une veille `DELETE /api/veille/{id}` Soft-delete de la veille. L'enregistrement est conservé pour audit mais exclu de tous les endpoints de liste et n'est plus planifié. **Réponse** `200 OK` — `{ "ok": true }`. ## Runs Un *run* est une exécution unique de la veille plus les statistiques de diff calculées contre le run précédent. Le premier run est un *baseline* (`is_baseline: true`) et n'a pas de compteurs de diff. ### Lister les runs `GET /api/veille/{id}/runs` Renvoie l'historique des runs ordonné par `computed_at` descendant. **Réponse** `200 OK` ```json { "items": [{ "id": 17, "job_id": "job_b71e0d22", "prev_job_id": "job_aa44e0f1", "is_baseline": false, "total_count": 312, "prev_total_count": 305, "new_count": 9, "removed_count": 2, "modified_count": 24, "unchanged_count": 279, "computed_at": "2026-05-27T08:11:04Z", "job_status": "done", "job_completed_at": "2026-05-27T08:10:48Z" }] } ``` ### Récupérer un run `GET /api/veille/{id}/runs/{run_id}` Renvoie le run, incluant `samples` — previews plafonnés des lignes dans chaque bucket de diff. **Réponse** `200 OK` ```json { "id": 17, "job_id": "job_b71e0d22", "is_baseline": false, "new_count": 9, "removed_count": 2, "modified_count": 24, "unchanged_count": 279, "total_count": 312, "computed_at": "2026-05-27T08:11:04Z", "samples": { "new": [{ "key": "...", "nom": "..." }], "removed": [{ "key": "...", "nom": "..." }], "modified": [{ "key": "...", "nom": "...", "before": { "note": "4.3", "nb_avis": 42 }, "after": { "note": "3.8", "nb_avis": 51 }, "changed_fields": ["note", "nb_avis"] }] } } ``` ## Catégories de signaux Chaque run non-baseline classe chaque ligne du dataset dans exactement un bucket : | Catégorie | Signification | | ---------- | ----------------------------------------------------------------------------- | | `new` | Ligne présente dans le run courant, absente du précédent. | | `removed` | Ligne présente dans le précédent, absente du courant (fermée/retirée). | | `modified` | Ligne présente dans les deux runs avec au moins un champ tracké modifié. | | `unchanged`| Ligne présente dans les deux runs, identique sur les champs trackés. | Les compteurs sont exposés en `new_count`, `removed_count`, `modified_count` et `unchanged_count`. Les tableaux correspondants `samples.{new,removed,modified}` portent des previews plafonnés pour l'UI. > Le champ `removed` est le bucket fermé/retiré : un enregistrement qui n'est plus listé à la source. ## Signaux de réputation Les signaux de réputation sont une vue dérivée du bucket `modified` d'un run. Ils isolent les lignes dont la réputation publique a bougé d'une manière critique pour l'outreach — typiquement des listings Google Maps dont la note a baissé ou dont le volume d'avis a explosé entre deux runs. ### Logique de classement (vue haute) Une ligne modifiée devient un signal quand au moins une condition est vraie : - **Chute de note** — la note moyenne a baissé d'au moins `0.2` point. - **Surge d'avis** — le nombre d'avis a augmenté d'au moins `3` depuis le run précédent. Chaque signal porte un `score` qui classe l'urgence. Les chutes de note plus larges dominent ; les surges d'avis contribuent un boost additif plus petit au-dessus d'un seuil de bruit bas volume. Les signaux sont retournés triés par `score` descendant. La pondération exacte est un détail d'implémentation susceptible d'évoluer ; ne pas dépendre des valeurs absolues, uniquement de l'ordre relatif. ### Lister les signaux `GET /api/veille/{id}/runs/{run_id}/signals` **Réponse** `200 OK` ```json { "items": [{ "nom": "Garage du Centre", "adresse": "12 rue Voltaire, 69003 Lyon", "telephone": "+33 4 78 00 00 00", "site_web": "https://...", "email": "contact@...", "lien_google_maps": "https://maps.google.com/...", "note_avant": 4.3, "note_apres": 3.8, "delta_note": -0.5, "avis_avant": 42, "avis_apres": 51, "delta_avis": 9, "score": 12.0 }], "total": 1 } ``` ### Exporter les signaux `GET /api/veille/{id}/runs/{run_id}/signals.{fmt}` Diffuse la même liste classée comme fichier téléchargeable. | Format | Media type | Extension | | ------ | ------------------------------------------------------------------------- | --------- | | `csv` | `text/csv; charset=utf-8` | `.csv` | | `json` | `application/json` | `.json` | | `xlsx` | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` | `.xlsx` | La réponse pose `Content-Disposition: attachment` avec un nom de fichier de la forme `signaux-reputation-veille-{id}-run-{run_id}.{fmt}`. Cause spécifique : `400` `fmt` non supporté (doit être `csv`, `json`, `xlsx`). --- title: Plafonds de dépense IA slug: concepts/ai-spending-caps section: Concepts summary: Plafonds de dépense IA stricts par utilisateur ($/requête, $/jour, $/mois), avec estimation du coût avant l'action et alertes email. --- Les fonctionnalités IA d'outsend tournent sur **votre propre clé fournisseur** (BYOK) : la facturation se fait au réel, directement chez le fournisseur. Pour qu'un prompt mal calibré ne brûle jamais votre facture, outsend applique des **plafonds de dépense stricts** à chaque requête IA — côté serveur, donc impossibles à contourner. ## Les trois plafonds | Plafond | Défaut | Configurable jusqu'à | |----------------|--------|----------------------| | Par requête | 10 $ | 100 $ | | Par jour | 10 $ | 100 $ | | Par mois | 100 $ | 1 000 $ | À régler dans **Paramètres → Plafonds de dépense IA**. Les jours et mois sont comptés en UTC. ## Comment ça marche 1. **Estimation avant** — avant une action IA, outsend affiche le coût worst-case (vos tokens d'entrée + le maximum de tokens en sortie) et le budget restant aujourd'hui et ce mois. 2. **Blocage avant dépassement** — si une requête risque de dépasser un plafond, elle est refusée *avant* tout appel au fournisseur. Rien n'est dépensé. 3. **Suivi du coût réel** — après chaque appel, le coût réel (les tokens consommés rapportés par le fournisseur × le prix du modèle) est ajouté à vos totaux du jour et du mois. 4. **Alertes email** — vous recevez un email à 80 % d'un plafond jour/mois, puis lorsqu'un plafond est atteint (IA en pause jusqu'au reset). ## Modèles sans prix connu Les prix viennent d'un catalogue public de prix de modèles (~2 700 modèles). Si un modèle n'y figure pas (certains endpoints custom ou exotiques), outsend ne peut pas calculer son coût : la requête est **autorisée et tracée, mais non plafonnée**, et l'interface signale le prix comme inconnu. Les modèles courants de chaque fournisseur supporté sont, eux, tarifés. ## Bon à savoir - Les plafonds sont une **sécurité côté outsend** — la vraie facture reste celle de votre fournisseur, et l'estimation est indicative. - Les resets sont calendaires : le total du jour se remet à zéro à 00:00 UTC, le total du mois le 1er. - Augmenter un plafond prend effet immédiatement ; l'IA reprend dès que vous repassez en dessous. --- title: Jobs & cycle de vie slug: concepts/jobs-lifecycle section: Concepts summary: Un job est une unité de travail. Cette page décrit ses états, transitions, événements et la sémantique de reprise. --- Un **job** est une unité de travail. Chaque module s'exécute sous forme de job. Les jobs sont isolés, observables, reprenables. ## Machine à états ``` ┌─────────┐ pris par file ┌─────────┐ succès ┌──────┐ │ pending │ ────────────────► │ running │ ────────────► │ done │ └─────────┘ └─────────┘ └──────┘ │ │ │ annulation │ erreur fatale ▼ ▼ ┌───────────┐ ┌────────┐ │ cancelled │ │ failed │ └───────────┘ └────────┘ done / failed / cancelled ──── (après 7 jours) ────► expired ``` | État | Signification | |-------------|-------------------------------------------------------------------------------------| | `pending` | Créé, en attente dans la file FIFO | | `running` | Pris par un worker, en cours d'exécution | | `done` | Terminé avec succès, résultats téléchargeables | | `failed` | Échec (voir `error_message`) | | `cancelled` | Annulé via l'UI ou l'API | | `expired` | Plus de 7 jours depuis l'état terminal — fichiers de résultats purgés | Transitions et assignation à la file sont atomiques ; un job n'est jamais pris deux fois. ## Création ``` POST /api/jobs { "queries": [...], "zones": [...] } # crée un job scrap POST /api/jobs/{type} { ...paramètres module } # raccourci typé ``` Voir [API Jobs](/docs/fr/api/jobs). ## Observabilité ``` GET /api/jobs/{id} # statut, compteurs, métadonnées GET /api/jobs/{id}/stream # SSE : status / log / done ``` Le flux se ferme à la fin du job. Timeout de sécurité : 6 heures. Payloads des événements : voir [États & événements SSE](/docs/fr/concepts/states-and-events). ## Résultats ``` GET /api/jobs/{id}/download?format=csv|json|xlsx GET /api/jobs/{id}/items?offset=0&limit=200 ``` Les résultats restent **7 jours** après l'état terminal, puis sont purgés. L'enregistrement du job demeure. ## Erreurs & reprises Un job `failed` expose `error_message` et `error_count` (items en erreur dans le job — un job peut être `done` avec `error_count > 0`). ``` POST /api/jobs/{id}/resume ``` Crée une nouvelle tentative qui repart du dernier item réussi. ## Annulation ``` POST /api/jobs/{id}/cancel # conserve les résultats partiels DELETE /api/jobs/{id} # annule et supprime l'enregistrement ``` ## Concurrence - Jusqu'à **5 jobs simultanés par utilisateur** (au-delà : file d'attente) - Deux voies : **série** (extraction) et **parallèle** (6 slots : vérification, utilitaires pipeline, `delivery_check`) - Les jobs sont indépendants — les ré-exécutions n'attendent pas l'original ## La suite - [États & événements SSE](/docs/fr/concepts/states-and-events) - [Orchestration pipeline](/docs/fr/concepts/pipeline-orchestration) - [Limites & quotas](/docs/fr/concepts/limits) --- title: Limites & quotas slug: concepts/limits section: Concepts summary: Toutes les limites numériques appliquées par la plateforme, dans un seul tableau. --- Référence pour la planification de capacité. Global plateforme sauf mention par-utilisateur. ## Jobs | Limite | Valeur | Portée | |------------------------------------|-----------------|-----------------| | Jobs simultanés par utilisateur | 5 | par utilisateur | | Slots worker voie parallèle | 6 | global plateforme (verify_emails, delivery_check, import, filter, sort) | | Rétention fichiers résultats | 7 jours | par job | | Durée max d'un flux SSE | 6 heures | par flux | | EF max par job | 1.0 | par job | La voie parallèle est un pool séparé de la voie série utilisée par les modules d'extraction. ## Veille | Limite | Valeur | |--------------------|------------| | Fréquence min | 1 jour | | Fréquence max | 365 jours | ## Pipelines | Limite | Valeur | |--------------------|------------| | Nœuds max | 20 | | Inputs max/nœud | 1 (MVP) | ## Dépense IA (BYOK) Plafonds stricts par utilisateur sur les fonctionnalités IA — facturées sur votre propre clé fournisseur — avec alertes email. Configurables dans les Paramètres jusqu'à ×10 le défaut. | Plafond | Défaut | Max | Portée | |--------------|--------|---------|---------------------------| | Par requête | 10 $ | 100 $ | par utilisateur | | Par jour | 10 $ | 100 $ | par utilisateur, jour UTC | | Par mois | 100 $ | 1 000 $ | par utilisateur, mois UTC | Une requête qui dépasserait un plafond est bloquée avant tout appel au fournisseur ; un email part à 80 % et quand un plafond est atteint. Voir [Plafonds de dépense IA](/docs/fr/concepts/ai-spending-caps). ## Rate limits auth Fenêtres par endpoint. Dépassement = `429 Too Many Requests`. | Endpoint | Limite | Fenêtre | |--------------------------------|--------------|----------------------------| | Inscription | 3 tentatives | par heure, par IP | | Connexion | 5 tentatives | par 15 min, par IP+email | | Demande de réinitialisation | 3 tentatives | par heure, par IP+email | | Changement de mot de passe | 5 tentatives | par heure, par utilisateur | | Renvoi vérification email | 3 tentatives | par heure, par utilisateur | | Création thread feedback | 20 tentatives| par heure, par utilisateur | | Durée de session | 7 jours | fenêtre glissante | Aucun throttle API global au-delà. ## Spécifiques aux modules - **[`scrap`](/docs/fr/modules/scrap)** — max 1.0 EF par job - **[`emails`](/docs/fr/modules/emails)** — modes `normal` et `deep` avec profils EF distincts - Tous les modules multi-proxy — tableau `items` borné à 1–10000 par requête ## La suite - [Jobs & cycle de vie](/docs/fr/concepts/jobs-lifecycle) - [Vue d'ensemble API](/docs/fr/api/overview) --- title: Registre des modules slug: concepts/module-registry section: Concepts summary: Une source de vérité unique décrit chaque module — ses entrées, sorties, catégorie et son emplacement dans l'UI. --- Chaque module exposé par outsend est déclaré dans un **registre unique**. Il alimente la grille de modules du dashboard, le sélecteur de nouveau job, l'éditeur de pipeline et le listing landing. Garanties : un module visible au dashboard a un endpoint (et réciproquement) ; un snapshot lisible par machine est publié ; les catégories sont des indices, tandis que `slug`, `needs` et `produces` sont stables. ## L'endpoint ``` GET /api/modules-registry ``` Renvoie le registre complet en JSON. Chaque entrée : ```json { "slug": "scrap", "category": "extraction", "label": { "fr": "Scrap Google Maps", "en": "Scrape Google Maps" }, "needs": null, "produces": "poi_list", "pipelinable": true, "is_on_demand": false, "coming_soon": false, "api_endpoint": "/api/jobs/scrap" } ``` | Champ | Signification | |-----------------|--------------------------------------------------------------------------| | `slug` | Identifiant stable, utilisé dans URLs et chemins d'API | | `category` | `extraction` \| `enrichment` \| `intelligence` \| `verification` \| `pipeline` \| `meta` | | `label` | Libellés d'affichage par langue | | `needs` | Forme d'entrée (`poi_list`, `csv_rows`, …) — `null` si produit à partir de rien | | `produces` | Forme de sortie | | `pipelinable` | Utilisable comme nœud d'un pipeline | | `is_on_demand` | Si true, pas de backend — déclenche une conversation avec l'équipe | | `coming_soon` | Si true, listé pour visibilité seulement ; intérêt votable | | `api_endpoint` | Raccourci pour démarrer un job de ce type | ## Correspondance souple des entrées `needs` et `produces` décrivent des noms de colonnes *canoniques* (`nom`, `telephone`, `site_web`, `email`, `lien_google_maps`, …). Vous n'avez jamais à formater vos données pour les coller exactement : les entrées sont résolues via une table partagée d'alias acceptés, de sorte que des colonnes nommées `Website`, `url`, `e-mail`, `name` ou `raison sociale` sont mappées vers le bon champ canonique. Les fichiers sans en-tête sont détectés automatiquement et les colonnes déduites de leur contenu. Chaque job le signale en clair. Chaque exécution renvoie une **`notice`** non bloquante (bannière d'info sur la page du job, ⓘ discret au dashboard) décrivant ce qui a été auto-mappé, deviné ou ignoré — par exemple les lignes écartées faute de site web. Un job n'échoue que lorsqu'une colonne requise est réellement absente (ex : un enrichissement qui a besoin de `site_web` ne la trouve sur aucune ligne), et cette erreur **nomme explicitement les alias acceptés** pour que vous sachiez quel en-tête fournir. ## Catégories | Catégorie | Rôle | Exemples | |----------------|------------------------------------------------------------|-------------------------------------------------------------| | `extraction` | Produit des données depuis des sources publiques | [`scrap`](/docs/fr/modules/scrap) | | `enrichment` | Enrichit les lignes existantes avec de nouveaux champs | [`emails`](/docs/fr/modules/emails), [`socials`](/docs/fr/modules/socials), [`legal_ids`](/docs/fr/modules/legal_ids) | | `intelligence` | Calcule des signaux sur les lignes existantes | [`pricing`](/docs/fr/modules/pricing), [`techstack`](/docs/fr/modules/techstack), [`ads_intelligence`](/docs/fr/modules/ads_intelligence) | | `verification` | Valide ou score les lignes existantes | [`verify_emails`](/docs/fr/modules/verify_emails), [`delivery_check`](/docs/fr/modules/delivery_check) | | `pipeline` | Utilitaires d'orchestration | [`import`](/docs/fr/modules/import), [`filter`](/docs/fr/modules/filter), [`sort`](/docs/fr/modules/sort) | | `meta` | N'est pas un job — décrit pipelines ou veilles | (aucun endpoint API) | ## Cycle de vie d'un module 1. **Coming soon** — landing uniquement, pas de backend, intérêt votable 2. **On-demand** — listé au dashboard, le CTA ouvre une conversation, exécuté manuellement 3. **Active** — entièrement supporté par un endpoint 4. **Disponible (gelé en alpha)** — construit et présenté comme un module actif sur toutes les surfaces, mais non lançable pendant l'alpha : l'UI affiche un bandeau maintenance avec bouton « Lancer » désactivé, et l'endpoint de création renvoie `503`. Contrairement à *coming soon*, ce n'est pas un placeholder et il ne porte pas de vote d'intérêt — c'est un module fini, retenu uniquement par la capacité serveur de l'alpha. 5. **Deprecated** — toujours appelable mais signalé Les changements de phase apparaissent dans le registre via `coming_soon`, `is_on_demand`, `alpha_unavailable` et `deprecated_at`. ## Ajouter un module (contributeurs) Ajouter un module = 2 fichiers dans le codebase : une entrée registre JS (surfaces UI) et une entrée registre Python (API + dispatcher worker). Le runtime branche ensuite le module partout automatiquement. ## La suite - [Jobs & cycle de vie](/docs/fr/concepts/jobs-lifecycle) - [Orchestration pipeline](/docs/fr/concepts/pipeline-orchestration) --- title: Orchestration pipeline slug: concepts/pipeline-orchestration section: Concepts summary: Chaîner des modules dans un DAG réutilisable. Chaque bloc consomme la sortie du précédent, sans code de liaison. --- Un **pipeline** est un graphe orienté acyclique de modules. Chaque nœud est un appel de module ; chaque arête déclare quelle sortie alimente quelle entrée. Le pipeline sauvegarde une recette multi-étapes une fois et la relance à volonté. Les pipelines supportent aussi la [veille](/docs/fr/concepts/veille-monitoring) : un scrap récurrent est en interne un pipeline planifié. ## Anatomie ``` ┌──────────┐ │ scrap │ queries=["boulangerie"], zones=["Paris"] └────┬─────┘ │ produces: poi_list ▼ ┌──────────┐ ┌──────────┐ │ emails │ │ ads_intel│ └────┬─────┘ └────┬─────┘ │ │ ▼ ▼ ┌────────────────────────────┐ │ filter │ rules: emails_present=true, ads_score≥30 └────────────┬───────────────┘ ▼ ┌────────┐ │ sort │ sort_by=ads_score, desc, top_n=200 └────────┘ ``` Chaque nœud possède : - **type** — slug du module (voir [registre des modules](/docs/fr/concepts/module-registry)) - **params** — configuration du module, identique à un job autonome - **inputs** — références au(x) nœud(s) amont - **id** — identifiant local dans le pipeline ## Règles de chaînage Une arête est valide uniquement si `produces` du producteur correspond à `needs` du consommateur (formes comme `poi_list`, `enriched_list`, `csv_rows`). L'éditeur vérifie à la conception. ## Limites | Limite | Valeur | |--------------------|---------------------------------------| | Nœuds max | 20 | | Inputs max/nœud | 1 (fusion multi-entrées non ouverte) | | Profondeur max | 20 | | Ré-exécutions | Illimitées | ## Exécution Les pipelines **démarrent auto à la création** — `POST /api/pipelines` met en file le nœud racine, la suite suit à mesure que les prédécesseurs atteignent `done`. Chaque nœud tourne comme un job normal (même cycle de vie, observabilité, reprises). Le coordinateur avance sur `done`, s'arrête au premier `failed`. Un pipeline échoué peut être repris au nœud fautif. Pour relancer, créer un nouveau pipeline (le graphe est en JSON — copier et re-poster). ## Endpoints ``` POST /api/pipelines # création (démarre auto) GET /api/pipelines # liste des pipelines utilisateur GET /api/pipelines/{id} # détail + graphe ``` Un pipeline appartient à un seul utilisateur. ### Aperçu filtre ``` POST /api/pipelines/{id}/nodes/{node_id}/filter-preview ``` Exécute un nœud `filter` sur un échantillon de la sortie du prédécesseur, sans lancer tout le pipeline. ## La suite - [Veille (surveillance)](/docs/fr/concepts/veille-monitoring) - [`filter`](/docs/fr/modules/filter), [`sort`](/docs/fr/modules/sort), [`import`](/docs/fr/modules/import) --- title: Modes de scrap (Fast / Avancé / Ultra) slug: concepts/scrape-modes section: Concepts summary: Les trois modes du scrap Google Maps règlent la profondeur de subdivision adaptative — donc l'arbitrage entre vitesse, coût (EF) et exhaustivité des contacts. --- Le scrap Google Maps d'outsend propose **trois modes** qui règlent un seul paramètre : la **profondeur de subdivision adaptative**. Ils arbitrent entre vitesse, coût et exhaustivité. | Mode | Pour qui | En une phrase | |------|----------|----------------| | **Fast** *(défaut)* | La majorité des cas | Rapide, moins de coût, récupère déjà l'essentiel des contacts. | | **Avancé** | Besoin d'enrichir | Équilibré : plus de contacts dans les zones denses, coût modéré. | | **Ultra** | Couverture maximale | Subdivise au maximum : recall quasi-exhaustif, plus lent et plus coûteux. | ## Pourquoi trois modes : le cap des 120 résultats Google Maps **plafonne toute recherche à ~120 résultats** (« vous êtes arrivé au bout de la liste »). Pour aller au-delà, outsend découpe une tuile saturée en 4 sous-tuiles plus zoomées et rescanne chacune (dédup par lien Google Maps). C'est la **subdivision adaptative**. Mais subdiviser n'a de sens que si la sous-tuile apporte de **nouveaux** contacts : dans une zone peu dense, Google élargit son rayon au-delà de la tuile et renvoie souvent les mêmes 120 fiches → subdiviser = 4× plus de travail pour 0 nouveau lead. Chaque mode fixe donc un **seuil** : on ne subdivise une tuile saturée que si elle a ramené au moins *N* nouveaux contacts uniques. | Mode | Seuil (nouveaux uniques requis pour subdiviser) | Effet | |------|------------------------------------------------|-------| | **Fast** | 15 | Ne subdivise que les zones franchement riches → peu de tuiles. | | **Avancé** | 7 | Subdivise plus volontiers → plus de couverture. | | **Ultra** | 1 | Subdivise dès qu'il reste du nouveau → couverture maximale. | La profondeur de subdivision est bornée (zoom 13 → 17, soit 4 niveaux : une tuile fait alors ~300 m de côté, ≈ 1 bloc urbain), donc même Ultra reste fini. ## Les modes ne divergent que dans les zones denses Point essentiel : **un mode ne change quelque chose que là où des tuiles saturent** (≥ 120 résultats). - **Zone dense** (centre-ville, requête courante comme « plombier » ou « restaurant ») : les tuiles saturent, la subdivision se déclenche → Fast / Avancé / Ultra donnent des volumes de contacts **nettement différents**. - **Zone peu peuplée** (rural, requête de niche) : rien ne sature, aucune subdivision → **les trois modes donnent exactement le même résultat**. Choisir Ultra ne sert alors à rien (même résultat, même coût). C'est pour cela que le mode est un choix **par scrap**, pas un réglage global : il dépend de la densité de ce que vous cherchez. ## Coût (EF) et durée L'**EF** (équivalent-France) est l'unité de coût d'un scrap. La base est simple : > **1 EF = scraper la France entière, une fois, en mode Fast.** Une ville ou un département coûtent donc une petite fraction d'EF. Comme les modes plus profonds lancent **beaucoup plus** de requêtes Google Maps (ils re-subdivisent les tuiles saturées), ils coûtent proportionnellement plus : | Mode | Coût relatif | Durée relative | |------|:---:|:---:| | Fast | **×1** (base) | ×1 | | Avancé | **≈ ×2** | ≈ ×2 | | Ultra | **≈ ×6** | ≈ ×6 | Ces facteurs sont des **moyennes mesurées** (ratio de tuiles traitées vs Fast, campagne du 2026-06-05). Le coût réel dépend de la **densité réelle** de la zone : - **Zone peu peuplée** : rien ne sature → aucune subdivision → les 3 modes coûtent **pareil** (le facteur ne s'applique pas vraiment). - **Zone dense** : l'écart se creuse (Ultra peut atteindre ×14 en centre-ville très dense). L'estimation pré-scrap applique ces facteurs (l'EF affiché monte quand vous passez en Avancé/Ultra). En cours de scrap, l'**ETA tient compte** des subdivisions à venir, et le **temps écoulé** est affiché en direct. ## Mesures > **Méthodologie.** 3 requêtes de densités différentes — « plombier » (clusterise), « pharmacie » (nombreuse et répartie), « cordonnier » (niche) — toutes des catégories qui **affichent un téléphone** (les catégories grand-public type restaurant/coiffeur affichent ~0 tél → faussement filtrées par l'anti-bot, non testables). 3 zones (dense / moyenne / rurale), les 3 modes chacune, **chaque scrap mené à terminaison complète** (aucun timeout). On mesure : contacts uniques, tuiles traitées (≈ coût/requêtes), durée réelle. Le % est exprimé vs Fast. **Matrice complète (campagne 2026-06-05, « plombier », tout à terminaison)** | Zone | Densité | Mode | Contacts | Tuiles | Temps | vs Fast | Contacts/tuile | |------|---------|------|---------:|-------:|------:|--------:|---------------:| | Lyon 6 km | dense | Fast | 606 | 53 | 50 min | — | 11,4 | | Lyon 6 km | dense | Avancé | 627 | 89 | 84 min | +3,5 % | 7,0 | | Lyon 6 km | dense | Ultra | 647 | 193 | 180 min | +6,8 % | 3,4 | | Tours 10 km | moyenne | Fast | 311 | 14 | 8 min | — | 22,2 | | Tours 10 km | moyenne | Avancé | 351 | 42 | 20 min | +13 % | 8,4 | | Tours 10 km | moyenne | Ultra | 377 | 150 | 72 min | +21 % | 2,5 | | Aurillac 12 km | rurale | Fast | 213 | 19 | 7 min | — | 11,2 | | Aurillac 12 km | rurale | Avancé | 211 | 23 | 9 min | −1 % | 9,2 | | Aurillac 12 km | rurale | Ultra | 215 | 83 | 40 min | +1 % | 2,6 | - **Rural → les 3 modes sont identiques** (213 / 211 / 215). Ultra met 40 min (vs 7 min pour Fast) pour **+2 contacts**. Pousser ne sert à rien quand rien ne sature. - **Moyenne → Ultra +21 %** vs Fast, mais en **9× le temps** (72 min vs 8 min) ; Avancé +13 % en 2,5×. - **Dense → Ultra +6,8 %** vs Fast, en **3,6× le temps** (3 h vs 50 min). - **Efficacité** : Fast est **3 à 9× plus rentable par tuile** (donc par EF/temps) que Ultra dans toutes les zones. **Deux autres requêtes (« pharmacie » = catégorie dense et nombreuse ; « cordonnier » = niche), gain d'Ultra vs Fast** | Requête | Lyon (dense) | Tours (moyenne) | Aurillac (rurale) | |---------|:---:|:---:|:---:| | plombier (clusterise) | +6,8 % | +21 % | +1 % | | **pharmacie (nombreuse, répartie)** | **+50 %** | **+44 %** | bruit* | | cordonnier (niche) | +16 % | +3 % | +12 % | Détail pharmacie : Lyon Fast 411 / Ultra 617 (36→157 min) ; Tours Fast 253 / Ultra 364 (8→110 min). Cordonnier Lyon Fast 173 / Ultra 200. *Rural pharmacie = bruit : les tuiles ne saturent pas de façon stable (frontière des 120), l'ordre des modes y est aléatoire. > **Conclusion.** Le gain d'Ultra **n'a pas de valeur unique : de +1 % à +50 % selon la catégorie**. Les catégories **nombreuses et réparties** (pharmacies, commerces réguliers) bénéficient énormément d'Ultra (+44 à +50 % — Fast en rate la moitié à cause du cap des 120). Les catégories qui **se regroupent** (plombier) ou **rares** (cordonnier) n'y gagnent que +1 à +16 %. Dans tous les cas, Ultra coûte **3 à 14× le temps** de Fast, et en rural/faible densité les 3 modes convergent. ## Recommandation - **Par défaut : Fast.** Meilleur rapport vitesse/coût pour un premier jet et pour les catégories qui se regroupent (artisans, services spécialisés). - **Ultra quand la cible est dense ET nombreuse** (pharmacies, commerces, agences…) et que vous voulez l'exhaustivité : le gain est réel, jusqu'à **+50 %** de contacts. Acceptez 3 à 14× le temps. - **Avancé** = compromis intermédiaire. - **Niche ou zone peu peuplée → Fast**, point : les modes convergent, Ultra ne fait que perdre du temps. Voir aussi : [Jobs & cycle de vie](concepts/jobs-lifecycle), [Limites & quotas](concepts/limits). --- title: États & événements SSE slug: concepts/states-and-events section: Concepts summary: Payloads exacts pour chaque état de job et chaque événement émis sur le flux SSE. --- Le contrat pour toute intégration contre le flux job — bots, dashboards, alerting, assistants IA. ## États — énumération complète | Valeur | Terminal | Fichiers résultats dispo | Reprenable | |-------------|----------|---------------------------|------------| | `pending` | non | non | s/o | | `running` | non | non | s/o | | `done` | oui | oui (7 jours) | oui | | `failed` | oui | partiels | oui | | `cancelled` | oui | partiels | oui | | `expired` | oui | non (purgés) | non | Un job `pending` ou `running` ne peut être supprimé, seulement **annulé**. ## Flux SSE ``` GET /api/jobs/{id}/stream Accept: text/event-stream ``` SSE standard ; chaque événement : ``` event: data: ``` ### Événement `status` Toutes les **2 secondes** tant que non-terminal, plus une fois à l'état terminal. ```json { "id": "j_abc123", "status": "running", "processed_points": 412, "grid_points_count": 1280, "results_count": 387, "error_count": 2, "download_available": false, "query_stats": { "bakery": { "found_pct": 92 }, "dentist": { "found_pct": 78 } } } ``` | Champ | Type | Description | |----------------------|---------|--------------------------------------------------------------| | `id` | string | Id du job | | `status` | enum | Voir tableau ci-dessus | | `processed_points` | int | Items traités | | `grid_points_count` | int | Items planifiés | | `results_count` | int | Lignes de résultats à ce stade | | `error_count` | int | Items en échec (le job peut quand même atteindre `done`) | | `download_available` | bool | `true` une fois le fichier prêt | | `query_stats` | object | Stats par requête ; dépend du module | ### Événement `log` Émis au fil des nouvelles lignes de log (par lots, sondage interne 0,5 s). ```json { "message": "Picked up 12 POIs in Lyon centre", "level": "info", "timestamp": "2026-05-27T14:21:08Z" } ``` `level` ∈ `debug` · `info` · `warn` · `error`. ### Événement `done` Émis une fois, puis le flux se ferme. Même événement pour `failed` et `cancelled` — vérifier `status`. ```json { "id": "j_abc123", "status": "done", "results_count": 1820, "duration_seconds": 1342 } ``` ### Événement `error` Erreurs au niveau du flux (auth, not-found). Différent d'un job terminé en `failed` (qui arrive via `done` avec `status: "failed"`). ```json { "code": "forbidden", "message": "Not your job" } ``` ## Intervalles de sondage (sans SSE) | Endpoint | Intervalle minimum | |------------------|--------------------| | `/api/jobs/{id}` | 2 secondes | | `/api/jobs` | 5 secondes | L'état interne se rafraîchit toutes les 2 s ; sonder plus vite n'apporte rien. ## Timeouts | Élément | Valeur | |-------------------------------------------|------------| | Durée max d'un flux SSE | 6 heures | | Timeout global d'un job | 6 heures | | Fenêtre de reconnexion worker idle | 30 secondes | | Rétention fichiers résultats après `done` | 7 jours | ## La suite - [Jobs & cycle de vie](/docs/fr/concepts/jobs-lifecycle) - [Limites & quotas](/docs/fr/concepts/limits) --- title: Veille (surveillance) slug: concepts/veille-monitoring section: Concepts summary: Un scrap récurrent qui compare chaque exécution à la précédente et fait remonter des signaux de réputation. --- Une **veille** est la réexécution planifiée d'un job ou pipeline existant. Chaque exécution est comparée à la précédente, et les différences sont exposées comme **signaux**. Une veille est créée à partir d'un **job scrap** existant (la source). Sa requête + zones + paramètres deviennent le modèle, cloné à chaque exécution planifiée. ``` job source (scrap ponctuel) │ enregistré comme veille, fréquence = 7 jours ▼ run 1 ──► poi_list_v1 │ 7 jours plus tard ▼ run 2 ──► poi_list_v2 │ diff(v1, v2) ▼ rapport de changements : - nouveaux POIs (ouvertures) - POIs fermés (introuvables) - POIs modifiés (notes en baisse, contact changé, ...) ``` ## Fréquence Jours, **1**–**365**. L'horaire est volontairement interdit : la donnée prospect n'évolue pas aussi vite, et les rate limits sources ne tiendraient pas. Typique : 7 (hebdo), 30 (mensuel), 90 (trimestriel). ## Signaux Trois catégories extraites de chaque diff : - **`new`** — dans la nouvelle exécution, absent avant (concurrents nouvellement ouverts, partenaires, cibles d'acquisition) - **`closed`** — absent de la nouvelle exécution, présent avant (nettoyage prospection ; signal précoce de fermeture) - **`modified`** — présent dans les deux, changé : - **Delta de note** — chute Google = signal fort « client en difficulté » - **Delta de nombre d'avis** — activité en envol ou en stagnation - **Delta de contact** — téléphone ou site web changé (souvent un relancement) Les lignes modifiées sont scorées ; l'endpoint signals les renvoie classées. ## Endpoints ``` GET /api/veille # liste les veilles utilisateur POST /api/veille # création PATCH /api/veille/{id} # mise à jour nom, fréquence, statut DELETE /api/veille/{id} # suppression douce GET /api/veille/{id}/runs # historique des exécutions GET /api/veille/{id}/runs/{run_id} # une exécution + diff GET /api/veille/{id}/runs/{run_id}/signals # signaux filtrés, scorés ``` L'endpoint signals supporte CSV / JSON / XLSX via `?format=…`. ## États | État | Signification | |----------|--------------------------------------------------------| | `active` | S'exécute selon le planning | | `paused` | Planning suspendu ; exécutions existantes conservées | | `deleted`| Supprimé en douce ; données conservées | Une exécution de veille est un job normal — mêmes workers, mêmes quotas. Ne compte dans le plafond de jobs en cours qu'au moment de l'exécution. ## La suite - [Jobs & cycle de vie](/docs/fr/concepts/jobs-lifecycle) - [Orchestration pipeline](/docs/fr/concepts/pipeline-orchestration) - [`scrap`](/docs/fr/modules/scrap) --- title: BYOK — Bring your own AI key slug: integration/byok section: Intégration summary: Connecter une clé API personnelle de n'importe quel grand fournisseur AI (Anthropic, OpenAI, Gemini, Mistral, Groq, DeepSeek, xAI, ou tout endpoint OpenAI-compatible) et choisir un modèle. Clé du user, quota du user. --- > **Statut : partiellement live.** Connecter une clé, choisir un fournisseur et sélectionner un modèle sont disponibles dès maintenant dans **Settings → Connecter une IA**, et alimentent les features AI déjà livrées (ex. résumé d'avis Google, génération de pipeline depuis une description). L'assistant intégré plus large décrit ci-dessous reste sur la roadmap. L'intégration BYOK ("bring your own key") permet au user de coller une clé API AI dans les settings d'outsend et d'utiliser un assistant directement dans l'app — pour configurer des recherches, écrire des règles de filtre, résumer des résultats, construire des pipelines en langage naturel. ## Pourquoi BYOK plutôt qu'un modèle hébergé - Le spend reste sur le compte du user, facturé directement par le fournisseur AI. - Pas de médiation côté outsend : l'assistant ne voit que ce que le user accorde. - Le choix du fournisseur reste au user : Anthropic, OpenAI, ou tout endpoint compatible. ## Fournisseurs supportés Le fournisseur et le modèle se choisissent dans **Settings → Connecter une IA**. Les modèles sont **détectés en live** depuis l'API du fournisseur — aucune liste figée à maintenir, les nouveaux modèles apparaissent automatiquement à mesure que le fournisseur les publie. | Fournisseur | Format de clé | Notes | |-------------|---------------|-------| | Anthropic (Claude) | `sk-ant-…` | API Messages native | | OpenAI | `sk-…` | Inclut les modèles de raisonnement (série o, GPT-5) | | Google (Gemini) | `AIza…` | Endpoint OpenAI-compatible | | Mistral | — | | | Groq | `gsk_…` | | | DeepSeek | `sk-…` | | | xAI (Grok) | `xai-…` | | | Tout endpoint OpenAI-compatible | — | Coller une base URL custom (Together, Perplexity, OpenRouter, Ollama / vLLM local, …) | La clé est stockée chiffrée au repos (Fernet, secret serveur), liée au compte du user, et jamais envoyée hors backend outsend sauf vers le fournisseur choisi. Une estimation de coût indicative s'affiche avant les actions AI — purement indicative (comptage de tokens best-effort face aux tarifs publics connus), elle peut différer de la facturation réelle du fournisseur. La dépense IA est aussi protégée par des **plafonds stricts** — par requête, par jour et par mois — que vous réglez dans les **Paramètres** : une requête qui dépasserait un plafond est bloquée *avant* tout appel au fournisseur, avec des alertes email à 80 % et quand un plafond est atteint. Voir [Plafonds de dépense IA](/docs/fr/concepts/ai-spending-caps). ## Ce que l'assistant peut faire Mêmes endpoints qu'un user humain — voir [Vue d'ensemble API](/docs/fr/api/overview). Il peut : - Lire les jobs, pipelines et veilles du user - Démarrer de nouveaux jobs (avec confirmation explicite sur le coût) - Composer des pipelines en enchaînant des modules du [registre](/docs/fr/concepts/module-registry) - Calculer des règles de filtre depuis une description en langage naturel et prévisualiser le résultat Il ne peut pas : - Accéder aux données d'autres users - Modifier billing, paramètres compte, ou codes d'invitation - Faire quoi que ce soit hors du scope normal de permission du user ## Pourquoi pas juste Claude.ai avec outsend en MCP ? Les deux options coexisteront : - **BYOK** — pour les users qui veulent l'assistant **dans outsend.xyz**, avec l'UI qui rend nativement les formulaires de recherche et les tables pendant que le modèle orchestre. - **[MCP](/docs/fr/integration/mcp)** — pour les users qui veulent piloter outsend depuis leur propre Claude.ai ou Claude Desktop, avec leur abonnement existant. Les deux patterns sont complémentaires, pas concurrents. ## Pour aller plus loin - [Intégration MCP](/docs/fr/integration/mcp) — piloter outsend depuis son propre client AI - [llms.txt](/docs/fr/integration/llms-txt) — pointer un assistant AI vers la doc --- title: llms.txt — documentation AI-friendly slug: integration/llms-txt section: Intégration summary: Une seule URL expose toute la doc outsend à n'importe quel assistant AI — pas d'auth, pas de scraping, pas de parsing. --- La doc outsend est publiée au format [llms.txt](https://llmstxt.org). N'importe quel assistant AI — Claude, ChatGPT, Cursor, Perplexity, ou un modèle local — peut ingérer la référence complète en un seul fetch. ## Les deux endpoints | URL | Rôle | |---------------------------------------------------------------------------|-------------------------------------------------------------------------------------| | [`/docs/fr/llms.txt`](/docs/fr/llms.txt) | Index plat — une ligne par page, avec titre + URL + résumé une ligne | | [`/docs/fr/llms-full.txt`](/docs/fr/llms-full.txt) | Bundle complet — chaque page concaténée, délimitée par `` | Les deux endpoints renvoient du `text/plain`, sans auth, sans rate limit, sans rendering JS requis. ## Usage depuis un assistant AI La plupart des clients AI détectent désormais `llms.txt` automatiquement quand un domaine est mentionné. Pour ceux qui ne le font pas, coller l'URL directement : ``` https://outsend.xyz/docs/fr/llms-full.txt ``` Le bundle pèse ~150 KB et tient confortablement dans n'importe quelle fenêtre de contexte moderne. ## Bundles par section Pour des périmètres plus serrés : | URL | Contenu | |--------------------------------------------------|--------------------------------------| | `/docs/fr/_bundle/concepts.txt` | Seulement les pages Concepts | | `/docs/fr/_bundle/modules.txt` | Seulement les pages Modules | | `/docs/fr/_bundle/api.txt` | Seulement la référence API | | `/docs/fr/_bundle/integration.txt` | Seulement les pages Intégration | ## Le bouton Copy Chaque page de cette doc a un bouton **Copy** en haut à droite. Mêmes bundles, mais en un clic vers le presse-papier : - Copier cette page (markdown brut) - Copier cette section - Copier toute la doc L'action "Copier toute la doc" est le chemin recommandé quand on confie la doc à un assistant AI en interactif. ## Pourquoi ça compte Les assistants AI deviennent la couche d'intégration entre produits SaaS. Une doc qu'un assistant peut ingérer proprement — sans scraping, sans flow de login, sans parsing HTML — est intégrable ; une qui ne le peut pas, non. La doc outsend est conçue pour être lisible par un humain, mais sa **première audience** est le LLM qui va rédiger le code d'intégration, écrire le template de prompt, ou diagnostiquer le pipeline mal configuré. ## Pour aller plus loin - [Vue d'ensemble API](/docs/fr/api/overview) — la surface que l'assistant va appeler - [MCP](/docs/fr/integration/mcp) — le protocole que l'assistant devrait préférer --- title: MCP — Model Context Protocol slug: integration/mcp section: Intégration summary: Piloter outsend depuis son propre Claude.ai, Claude Desktop, ou tout client compatible MCP. Son abonnement, ses tokens. --- > **Statut : prévu.** Le serveur MCP est sur la roadmap. Cette page décrit le contrat ciblé. Sortie annoncée dans le changelog. L'intégration MCP expose outsend comme **serveur MCP distant**, branchable par tout client compatible : Claude.ai (custom connectors), Claude Desktop, Claude Code, Cursor, ou tout futur client qui parle le protocole. Le user se connecte une fois avec son compte outsend, et ensuite le client AI peut lancer des recherches, construire des pipelines, lire des résultats — en utilisant **son propre abonnement LLM** (aucun coût LLM côté outsend). ## Comment ça marchera 1. Le user ouvre les settings de son client MCP (ex. Claude.ai → Settings → Connectors → Add custom connector). 2. Il colle `https://outsend.xyz/mcp` et s'authentifie. 3. Le serveur MCP renvoie la liste des tools disponibles (voir ci-dessous). 4. Le modèle peut appeler ces tools au nom du user ; chaque appel tape l'API outsend en tant que ce user. ## Tools prévus | Tool | Action | |---------------------------|---------------------------------------------------------------------| | `list_jobs` | Lister les jobs récents du user | | `get_job` | Récupérer status, compteurs, et échantillon de résultats | | `create_scrap_job` | Démarrer une extraction Google Maps | | `create_enrich_job` | Démarrer un enrichissement sur un job existant (emails, socials…) | | `list_pipelines` | Lister les pipelines du user | | `create_pipeline` | Composer un pipeline depuis une description | | `run_pipeline` | Exécuter un pipeline sauvegardé | | `list_veilles` | Lister les veilles récurrentes | | `create_veille` | Enregistrer un job existant comme veille | | `get_signals` | Récupérer les derniers signaux de réputation d'une veille | Chaque tool a un schéma d'arguments calé sur l'[endpoint API](/docs/fr/api/overview) correspondant. ## Périmètre et limites Le serveur MCP hérite des permissions normales du user : - Pas d'accès aux données d'autres users. - Mêmes rate limits que l'API REST. - Pas de modification du billing, des paramètres compte, ou des codes d'invitation. ## BYOK vs MCP | Pattern | Où vit le chat | Qui paie les tokens LLM | |---------|-------------------------------------------------|-----------------------------------| | [BYOK](/docs/fr/integration/byok) | Dans outsend.xyz | Le user, via une clé API collée | | MCP | Dans le client AI existant du user | Le user, via son abonnement | Les deux patterns coexistent. BYOK si l'assistant doit vivre dans l'UI outsend ; MCP s'il doit vivre là où le user travaille déjà. ## Pour aller plus loin - [BYOK](/docs/fr/integration/byok) — assistant dans outsend.xyz - [llms.txt](/docs/fr/integration/llms-txt) — donner accès à la doc à n'importe quel assistant AI --- title: Profil marketing slug: modules/ads_intelligence section: Modules --- # Profil marketing Le module `ads_intelligence` profile la stack marketing du site de chaque POI et condense les résultats en un score unique de maturité marketing 0–100. Il découpe une liste de prospects en deux segments actionnables : les entreprises qui investissent déjà en acquisition payante et celles encore en premier contact froid. Les détections croisent la page d'accueil avec des listes de filtres maintenues par la communauté (uBlock Origin, EasyList, EasyPrivacy) plus une table de signatures outsend curatée, couvrant pixels publicitaires, réseaux de retargeting, CMP, CRM marketing et widgets de chat. ## Entrées Seuls les items avec un `site_web` non vide sont traités. | Champ | Type | Requis | Notes | |-----------------|--------|--------|------------------------------------------| | `site_web` | string | oui | URL absolue du site du POI | | `nom` | string | non | Repassé pour reporting | | `place_id` | string | non | Sert à recoller à la liste source | | `source_job_id` | string | non | ID d'un job `scrap` amont à chaîner | Taille de lot : 1 à 10 000 items par job. ## Sorties Une ligne par POI traité. Les pixels paid-media et le retargeting pèsent le plus dans le score ; les widgets de chat le moins. | Colonne | Type | Description | |-------------------|----------|-----------------------------------------------------------------------------| | `ads_score` | integer | Score de maturité marketing, 0–100 | | `pixels_detected` | string[] | Pixels publicitaires trouvés sur la page (ex. `meta`, `google_ads`, `tiktok`) | | `crm_detected` | string | CRM marketing identifié, le cas échéant (ex. `hubspot`, `klaviyo`, `brevo`) | | `chat_widget` | string | Solution de chat identifiée, le cas échéant (ex. `intercom`, `crisp`, `drift`) | | `marketing_tools` | string[] | Autres technologies marketing (CMP, CDP, affiliation, réseaux de retargeting) | Champs granulaires aussi stockés : `ads_active`, `ads_networks`, `pixel_meta`, `pixel_google_ads`, `cmp_vendor`, `retargeting`, `crm_marketing`, `chat_widgets`. ## Cycle de vie Cycle de vie standard des jobs outsend ; voir [/docs/fr/concepts/jobs-lifecycle](/docs/fr/concepts/jobs-lifecycle). La progression est reportée par item en unité `sites`. ## Pipeline | Direction | Clés | |------------|-------------------------------------------------------------------------------------------------------------------| | `needs` | `site_web` | | `produces` | `ads_active`, `ads_score`, `ads_networks`, `pixel_meta`, `pixel_google_ads`, `cmp_vendor`, `retargeting`, `crm_marketing`, `chat_widgets` | Tout job amont qui émet `site_web` (typiquement `scrap`) peut alimenter `ads_intelligence`. Le sélecteur de job se positionne par défaut sur le dernier job `scrap` du compte courant. ## Endpoints ### Créer un job `POST /api/jobs/ads-intelligence` ```json { "items": [ { "site_web": "https://example.com", "nom": "Example", "place_id": "..." } ], "source_job_id": "optional-upstream-job-uuid" } ``` Réponse : document `JobPublic` décrivant le job nouvellement créé (`id`, `status`, `job_type`, `output_filename`, `ef_cost`, timestamps). | Statut | Quand | |--------|------------------------------------------------------------------------| | `400` | Aucun item n'a de `site_web`, ou quota EF par job dépassé | | `401` | Session manquante ou invalide | | `403` | Compte inactif | | `422` | Payload non conforme au schéma (ex. `items` vide ou > 10 000) | L'état, la progression et les résultats du job se lisent via les endpoints partagés (`GET /api/jobs/{id}`, `GET /api/jobs/{id}/results`, flux SSE). ## Limites Voir [/docs/fr/concepts/limits](/docs/fr/concepts/limits). Coût EF par item : ~1 / 3 / 3700 EF. Temps mur par item : 0,6 – 6 s. ## Erreurs | Erreur | Cause | |-----------------------------------------|----------------------------------------------------------------| | `Aucun établissement avec site web` | Tous les items manquaient de `site_web` après normalisation | | `Quota dépassé` | Coût EF estimé au-dessus du plafond par job | | Échec de fetch au niveau item | Enregistré sur la ligne ; le job continue avec l'item suivant | | Page d'accueil vide / réponse non-HTML | Ligne émise avec `ads_score = 0` et détections vides | ## Pour aller plus loin Associer `ads_intelligence` aux modules suivants pour enrichir le profil prospect : - [`techstack`](/docs/fr/modules/techstack) — empreinte complète CMS, framework et hébergement. - [`pricing`](/docs/fr/modules/pricing) — fait remonter les tarifs visibles et les conditions commerciales. - [`pagespeed`](/docs/fr/modules/pagespeed) — Core Web Vitals et budget de performance. --- title: Assets visuels slug: modules/brand_assets section: Modules --- # Assets visuels Extrait l'identité visuelle de chaque prospect depuis son propre site : logo principal, variantes de logo, favicon, couleur de marque dominante, palette harmonique dérivée du logo. Capture d'écran de la page d'accueil en option. Toutes les images sont ré-hébergées dans le stockage privé de l'appelant, ce qui évite qu'un lien casse si le prospect change de CDN. Le module est en lecture seule contre le site du prospect — aucune soumission de formulaire, aucune connexion, aucun franchissement d'authentification. ## Inputs Une ligne par POI. Seul `site_web` est requis ; le reste est transmis en passe-plat. | Champ | Type | Requis | Notes | |------------|--------|--------|--------------------------------------------------------| | `site_web` | string | oui | URL HTTP(S) du site du prospect. | | `nom` | string | non | Nom affiché, exposé dans l'UI. | Un batch accepte 1 à 10 000 lignes. Les lignes sans `site_web` sont écartées avant la mise en file. Options au niveau du job : | Option | Type | Défaut | Notes | |----------------------|--------|---------|-------------------------------------------------------------| | `source_job_id` | string | null | Job parent dans la chaîne du pipeline. | | `capture_screenshot` | bool | false | Ajoute une capture de la page d'accueil. ~5× plus lent. | ## Outputs Sortie par ligne. Les URLs locales pointent vers des assets ré-hébergés sous `/api/brand-assets//.` et ne sont servies qu'au propriétaire (ou à un admin). | Colonne | Type | Notes | |-------------------------------|---------|-----------------------------------------------------------------------------| | `logo_url` | string | URL source du logo principal trouvée sur le site du prospect. | | `logo_local_url` | string | Copie ré-hébergée du logo principal, URL stable. | | `logo_source` | string | Origine du logo (par exemple `og:image`, JSON-LD, apple-touch). | | `logo_variants_local_urls` | list | Variantes ré-hébergées : apple-touch, mask-icon, monochrome, etc. | | `favicon_url` | string | URL source du favicon de meilleure qualité détecté. | | `favicon_local_url` | string | Copie ré-hébergée du favicon. | | `brand_color` | string | Couleur de marque dominante au format hex. | | `brand_color_source` | string | Origine de la couleur (meta theme-color, échantillonnage du logo, etc.). | | `brand_palette` | list | Cinq couleurs hex harmoniques dérivées du logo. | | `screenshot_local_url` | string | Capture de la page d'accueil. Renseigné si `capture_screenshot=true`. | Les binaires sont stockés sous `data/brand_assets//.`. Extensions autorisées : `svg`, `png`, `jpg`, `jpeg`, `webp`, `gif`, `ico`, `avif`. Chaque asset est haché pour la déduplication entre lignes d'un même propriétaire. ## Cycle de vie États de job standard — voir [Cycle de vie des jobs](/docs/fr/concepts/jobs-lifecycle). Les erreurs HTTP par ligne ne font jamais échouer le job : une ligne en échec porte `fetch_error` et un `logo_local_url` à null. ## Pipeline | Needs | Produces | |-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| | `site_web` | `logo_url`, `logo_local_url`, `logo_variants_local_urls`, `favicon_url`, `favicon_local_url`, `brand_color`, `brand_palette`, `screenshot_local_url` | `pipelinable: true`, s'insère après toute étape produisant `site_web` — typiquement un parent `scrap`. `supports_veille: false` : l'identité de marque est traitée comme une extraction one-shot, pas un signal récurrent. ## Endpoints ### Créer un job batch ``` POST /api/jobs/brand-assets ``` Body : ```json { "items": [ {"nom": "Stripe", "site_web": "https://stripe.com"} ], "source_job_id": null, "capture_screenshot": false } ``` Retourne une enveloppe `JobPublic`. Authentification : tout utilisateur actif. ### Lookup live mono-domaine ``` GET /api/brand-lookup?domain=&refresh= ``` One-shot, aucun job batch créé. Le premier appel sur un domaine donné fait un fetch live (~2–3s) et stocke le résultat dans un cache par utilisateur. Les appels suivants dans les sept jours retournent instantanément le profil mis en cache. `refresh=true` force un re-fetch. Forme de la réponse : ```json { "domain": "stripe.com", "cached": false, "cached_at": null, "profile": { "status": "ok", "logo_url": "...", "logo_local_url": "...", "logo_source": "og:image", "logo_variants_local_urls": ["..."], "favicon_url": "...", "favicon_local_url": "...", "brand_color": "#635BFF", "brand_color_source": "theme-color", "brand_palette": ["#635BFF", "..."], "http_status": 200, "final_url": "https://stripe.com/", "fetch_error": null } } ``` ### Servir un asset ré-hébergé ``` GET /api/brand-assets//. ``` Isolation par propriétaire : seul le propriétaire (ou un admin) peut lire les assets du namespace. Les noms de fichiers sont validés par une regex stricte ; les tentatives de path traversal sont rejetées avec `400`. Pour les quotas et caps globaux, voir [Limites](/docs/fr/concepts/limits). TTL du cache brand-lookup : 7 jours, par utilisateur, par domaine. Le mode screenshot multiplie le coût par ligne par ~5 ; opt-in uniquement. ## Erreurs | Code | Signification | |------|--------------------------------------------------------------------------------| | 400 | `Aucun établissement avec site web` — aucune ligne ne porte un `site_web` exploitable. | | 400 | Domaine invalide sur `/api/brand-lookup`. | | 400 | Nom de fichier d'asset invalide sur l'endpoint de service. | | 403 | Accès à un asset appartenant à un autre utilisateur. | | 502 | Lookup live en échec côté amont (`Lookup failed: : `). | ## Pour aller plus loin - [techstack](/docs/fr/modules/techstack) — détecter le CMS, l'analytics et les frameworks derrière le même `site_web`. - [ads_intelligence](/docs/fr/modules/ads_intelligence) — révéler l'empreinte d'acquisition payante du prospect en complément de l'identité visuelle. --- title: Vérification fermeture slug: modules/dead_check section: Modules summary: Vérifier si chaque point d'intérêt est toujours en activité, a fermé, ou est incertain — à partir des signaux réels d'abandon sur son site. --- ## Objet Le module `dead_check` inspecte le site web associé à chaque point d'intérêt (POI) et détermine si l'activité sous-jacente semble vivante, fermée ou incertaine. Il corrèle plusieurs signaux d'abandon sur le même domaine : nom de domaine en expiration, redirection vers une propriété sans rapport (revente ou rebranding), pages de parking déguisées en vrais sites, certificats TLS expirés ou invalides. Les annuaires (Doctolib, Pages Jaunes, Yelp, etc.) sont reconnus comme tels plutôt que catalogués à tort en site personnel. ## Inputs Une liste de POI, chacun portant au moins un site web. Les entrées sans `site_web` sont filtrées à la soumission. | Champ | Type | Requis | Description | |---|---|---|---| | `items` | array d'objets POI | oui | 1 à 10 000 entrées. Les entrées sans `site_web` sont écartées avant exécution. | | `source_job_id` | string | non | Identifiant du job amont produisant la liste (typiquement un `scrap`). Sert au lignage dans l'UI et le pipeline. | Aucun autre paramètre : le module tourne dans un mode unique. ## Outputs Chaque POI d'entrée est enrichi du verdict de fermeture pour son site. Les colonnes POI d'origine sont conservées et la suivante est ajoutée : | Colonne | Type | Description | |---|---|---| | `site_alive` | `"open"` \| `"closed"` \| `"uncertain"` | Verdict final. `open` = site se comporte comme une présence d'activité, `closed` = signaux convergents d'abandon, `uncertain` = signaux trop minces pour trancher. | L'unité de progression pendant l'exécution est `sites` ; l'unité de résultat aussi. ## Cycle de vie États de job standard — voir [Cycle de vie des jobs](/docs/fr/concepts/jobs-lifecycle). Des compteurs partiels sont diffusés en SSE, ce qui permet de consommer les premiers verdicts sans attendre l'export final. ## Pipeline Pipelinable ; typiquement inséré juste après l'étape produisant la liste, avant tout enrichissement coûteux. | Slot | Valeur | |---|---| | `needs` | `site_web` | | `produces` | `site_alive` | | Catégorie | `verify` | | Amont typique | `scrap` | | Aval typique | `emails`, `techstack`, `ads_intelligence`, `filter` | Pattern courant : `scrap` puis `dead_check` puis `filter` (garder `site_alive = "open"`) puis n'importe quel module d'enrichissement — pour ne pas payer de la donnée d'outreach sur des activités fermées. ## Endpoints Créer un job : ``` POST /api/jobs/dead-check Content-Type: application/json { "items": [ { "site_web": "https://example.com", "nom": "Example Co" } ], "source_job_id": "…" } ``` Réponse : un objet `JobPublic` avec `id`, `status` et les métadonnées de job standard. Poller `GET /api/jobs/{id}` ou s'abonner au flux SSE pour la progression ; télécharger le CSV final depuis la page de détail du job une fois `status = "done"`. Pour la surface complète de l'API des jobs (liste, détail, cancel, export, events), voir [API Jobs](/docs/fr/api/jobs). Pour les quotas par compte, voir [Limites](/docs/fr/concepts/limits). ## Erreurs | HTTP | `detail` | Cause | |---|---|---| | 400 | `Aucun établissement avec site web` | Aucun item d'entrée ne porte de champ `site_web`. | | 400 | `Quota dépassé : …` | Le coût estimé dépasse le quota équivalent-France par job. | | 401 / 403 | — | Session manquante ou inactive. | Les erreurs levées après la création du job remontent sur la page de détail et via l'événement SSE `error` ; le job termine en `status = "error"` et les résultats partiels, s'il y en a, restent téléchargeables. ## Pour aller plus loin - [filter](/docs/fr/modules/filter) — ne garder que les POI dont `site_alive` vaut `open` (ou exclure `closed`) avant d'engager du budget sur l'enrichissement. - [reviews](/docs/fr/modules/reviews) — pour les POI marqués `uncertain`, les avis récents départagent fortement entre activité en marche et activité dormante. --- title: Délivrabilité inbox slug: modules/delivery_check section: Modules --- # Délivrabilité inbox Teste où un message envoyé depuis un domaine donné atterrit réellement. Le module n'envoie rien à la place de l'appelant — le vrai message est envoyé à quinze boîtes seed, et le module remonte où chacune l'a classé : inbox principale, onglet secondaire (Promotions, Social) ou spam. Le résultat est un instantané de la façon dont une boîte destinataire a traité ce message précis, depuis ce domaine précis, à ce moment précis. Ce n'est ni une simulation, ni un lookup de réputation, ni une inspection d'en-têtes. Le module répond à une seule question : *si ce message est envoyé depuis ce domaine maintenant, où va-t-il ?* ## Inputs Un job de test prend deux valeurs. | Champ | Requis | Description | | --- | --- | --- | | `domain` | oui | Le domaine d'envoi à tester, en minuscules, sans `@` (par exemple `acme.fr`). Doit contenir un point et faire 3 à 120 caractères. | | `subject_filter` | non | Sous-chaîne optionnelle comparée aux sujets dans les boîtes seed. Utile pour désambiguïser quand plusieurs tests tournent en parallèle depuis le même domaine. Jusqu'à 120 caractères. | Le module ne prend pas de liste de destinataires. La délivrabilité inbox est un job standalone — il ne fait pas partie d'un pipeline et ne peut pas consommer la sortie d'un autre job. ## Outputs Le job écrit une ligne par boîte seed dans `results_delivery.csv`. Quinze boîtes seed sont interrogées ; chaque ligne décrit ce que cette boîte a observé. | Colonne | Description | | --- | --- | | `seed_email` | Adresse de la boîte de test concernée par la ligne. | | `seed_kind` | Famille de fournisseur de la seed (sert à grouper les résultats par type de boîte). | | `status` | `received` si le message a été trouvé, sinon un état vide ou en attente. | | `placement` | Où le message a atterri : `Inbox principal`, `Inbox · ` (par exemple Promotions, Social), `Spam`, ou vide si non reçu. | | `subject` | Sujet tel qu'observé dans la boîte seed. | | `received_relative` | Délai lisible entre envoi et observation (par exemple `2 min`). | L'endpoint de rapport structuré agrège ces lignes en un résumé. | Champ | Description | | --- | --- | | `received` | Nombre de seeds ayant observé le message. | | `total` | Total de boîtes seed interrogées (15). | | `missing` | `total - received`. | | `primary` | Seeds dont le placement est `Inbox principal`. | | `primary_pct` | Taux d'inbox principale en pourcentage de `received`. | | `inbox_secondary` | Seeds dont le placement est un onglet inbox non-principal. | | `promotions` | Seeds dont le placement correspond à Promotions ou Social. | | `spam` | Seeds dont le placement est `Spam`. | | `spam_pct` | Taux de spam en pourcentage de `received`. | | `verdict` | Verdict contextuel — voir ci-dessous. | | `seeds` | Tableau par seed décrit dans la table précédente. | L'objet `verdict` porte un jugement en une ligne et une note actionnable. | `verdict.label` | Quand | | --- | --- | | `EXCELLENT` | `primary_pct` ≥ 90. | | `TRÈS BON` | `primary_pct` ≥ 70. | | `MOYEN` | `primary_pct` ≥ 50. | | `MAUVAIS` | `spam_pct` ≥ 50. | | `INSUFFISANT` | La plupart des messages ont atterri dans des onglets secondaires. | | `EN ATTENTE` | Rien reçu pour l'instant. | ## Cycle de vie États de job standard — voir [Cycle de vie des jobs](/docs/fr/concepts/jobs-lifecycle). Le déroulé d'exécution : créer le job, récupérer les seeds via `GET /api/delivery-check/seeds`, envoyer le vrai message aux quinze depuis le domaine testé, attendre que le worker poll jusqu'à ce que toutes les seeds remontent `received` ou qu'un timeout déclenche, puis lire le rapport agrégé. Le module ne se chaîne pas. Sa sortie n'est pas réutilisable en entrée d'un autre job — la délivrabilité inbox est listée dans le set de jobs non-chaînables aux côtés de `viewport_test`. ## Pipeline La délivrabilité inbox est `standalone_only`. - **Needs :** rien. Le job prend une chaîne de domaine, pas une liste d'enregistrements. - **Produces :** aucune colonne réutilisable. Le CSV existe pour l'export mais n'est pas exposé au graphe pipeline. - **Pipelinable :** non. - **Veille :** non supportée. Si une campagne doit réagir à un résultat de placement, le rapport est consommé via l'API et branché dans une orchestration externe — le module ne nourrira pas directement un autre nœud. ## Endpoints | Méthode | Chemin | Rôle | | --- | --- | --- | | `POST` | `/api/jobs/delivery-check` | Créer un job delivery-check. Body : `{ "domain": "...", "subject_filter": "..." }`. Retourne l'objet job public. | | `GET` | `/api/delivery-check/seeds` | Lister les quinze adresses seed auxquelles envoyer le message de test. | | `GET` | `/api/jobs/{job_id}/delivery-result` | Rapport agrégé avec résumé, verdict et lignes par seed. | | `GET` | `/api/jobs/{job_id}` | Statut de job standard (queued, running, done, failed). | Tous les endpoints requièrent un utilisateur authentifié et actif. Lire le job d'un autre utilisateur retourne `403`. Le budget par job est fixé à quinze observations seed ; pas de slider, pas d'override. La délivrabilité inbox ne consomme pas de crédits scraping (`ef_per_item: 0`), même si le quota par utilisateur s'applique encore. Une exécution complète atterrit typiquement entre deux et huit minutes après l'envoi du message seed. Le domaine doit contenir un point et est mis en minuscules en interne. Pour les caps globaux, voir [Limites](/docs/fr/concepts/limits). ## Erreurs | Statut | Raison | | --- | --- | | `400` | `Domaine d'envoi invalide` — le domaine est vide ou ne contient pas de point. | | `400` | `Quota dépassé` — `MAX_EF_PER_JOB` atteint. La délivrabilité inbox est gratuite en soi, mais le check quota s'applique quand même. | | `400` | `Pas un job de test de délivrabilité` — `/delivery-result` appelé sur un job dont le type n'est pas `delivery_check`. | | `403` | Le job appartient à un autre utilisateur. | | `404` | L'ID de job n'existe pas. | | `410` | Le CSV du job a expiré et a été supprimé. | Si le rapport retourne `received: 0` après l'exécution du worker, le message seed n'est jamais arrivé — soit il n'a pas été envoyé, soit il a été bloqué entièrement, soit le domaine est sur une blocklist complète. Renvoyer aux seeds et re-poller avant de conclure. ## Pour aller plus loin - [Vérification emails](/docs/fr/modules/verify_emails) — nettoyer une liste d'adresses avant envoi, pour que le test seed reflète ce que verra le sous-ensemble délivrable. - [Ads intelligence](/docs/fr/modules/ads_intelligence) — une fois le placement solide, voir quels concurrents paient pour de la visibilité sur la même audience. --- title: Emails slug: modules/emails section: Modules summary: Trouve une adresse email exploitable pour chaque point d'intérêt d'une liste existante. --- ## Purpose Module d'enrichissement : déduit des adresses email depuis le site web de chaque POI. Les boîtes nominatives sont remontées avant les génériques (`info@`, `contact@`, `hello@`). Aucune adresse n'est inventée — vide quand aucun candidat ne qualifie. ## Inputs Une liste de POI portant chacun au moins un site web. Les deux modes d'exécution diffèrent en couverture vs coût. | Field | Type | Required | Description | |---|---|---|---| | `items` | array of POI objects | yes | 1 à 10 000 entrées. Les entrées sans `site_web` sont filtrées avant l'exécution. | | `mode` | `"normal"` \| `"deep"` | no, défaut `"normal"` | `normal` exécute l'extraction standard. `deep` exécute un second passage exhaustif et requiert un run `normal` déjà terminé sur la même source. | | `source_job_id` | string | conditionnel | Requis quand `mode = "deep"`. Doit référencer un job `emails` `done` en mode `normal` sur la même source amont. | ## Outputs Chaque POI d'entrée est augmenté de jusqu'à deux champs email. Les colonnes POI d'origine sont conservées ; le job ajoute : | Column | Type | Description | |---|---|---| | `email` | string \| null | Meilleure adresse classée pour ce POI. Vide quand aucun candidat ne qualifie. | | `email_personal` | string \| null | Renseigné quand le meilleur candidat ressemble à une boîte personnelle plutôt qu'à une adresse de rôle générique. | Le classement est déterministe. Unité de progression : `sites`. Unité de résultat : `emails`. ## Lifecycle Cycle de vie standard : voir [Jobs & lifecycle](/docs/fr/concepts/jobs-lifecycle). ## Pipeline | Slot | Value | |---|---| | `needs` | `poi_list` (POI avec un champ `site_web`) | | `produces` | `enriched_list` (POI augmentés de `email`, `email_personal`) | | Amont typique | `scrap` | | Aval typique | `verify_emails`, `delivery_check`, `filter` | Config pipeline par défaut : `{ "mode": "normal" }`. `deep` est conçu comme une relance manuelle sur les POI revenus vides du run normal. ## Endpoints Création d'un job : ``` POST /api/jobs/emails Content-Type: application/json { "items": [ { "site_web": "https://example.com", "nom": "Example Co" } ], "mode": "normal" } ``` Réponse : un objet `JobPublic` avec `id`, `status` et les métadonnées standard. Pour la surface API job complète, voir [Jobs API](/docs/fr/api/jobs). ## Limits Quotas globaux : voir [/docs/fr/concepts/limits](/docs/fr/concepts/limits). Plafonds spécifiques au module : | Limit | Value | |---|---| | Nombre minimum d'items par job | 1 | | Nombre maximum d'items par job | 10 000 | | Items retenus | uniquement ceux avec un `site_web` non vide | | Prérequis mode `deep` | Un job `emails` `normal` `done` sur le même `source_job_id` | Les items sans site web sont écartés à la normalisation. Si la liste filtrée est vide, le job est rejeté avec `"Aucun établissement avec site web"`. ## Errors | HTTP | `detail` | Cause | |---|---|---| | 400 | `Mode email invalide : ... (attendu: normal | deep)` | `mode` n'est ni `normal` ni `deep`. | | 400 | `Aucun établissement avec site web` | Aucun item d'entrée ne porte un champ `site_web`. | | 400 | `Le mode Deep Extract n'est dispo qu'après une extraction normale ...` | `mode = "deep"` soumis sans run normal valide préalable sur la même source. | | 400 | `Quota dépassé : ...` | Coût estimé supérieur au quota équivalent-France per-job. | | 401 / 403 | — | Session absente ou inactive. | Les erreurs levées après création remontent via l'événement SSE `error` ; le job termine en `status = "error"` et les résultats partiels restent téléchargeables. ## What's next - [verify_emails](/docs/fr/modules/verify-emails) — confirme que chaque adresse est délivrable avant envoi. - [delivery_check](/docs/fr/modules/delivery-check) — mesure le placement inbox sur un vrai message. - [filter](/docs/fr/modules/filter) — ne garder que les POI ayant une adresse personnelle, exclure les domaines jetables, ou échantillonner la liste. --- title: Filtrer slug: modules/filter section: Modules --- ## Objectif Le module `filter` restreint un jeu de données aux lignes qui satisfont un ensemble de règles. Il est interne au pipeline (voir [/docs/fr/concepts/pipeline-orchestration](/docs/fr/concepts/pipeline-orchestration)) : il consomme le CSV produit par un nœud amont et émet un sous-ensemble strict, avec les mêmes colonnes. Aucune nouvelle donnée n'est récupérée et aucune colonne n'est ajoutée. Filtrer tôt économise le budget sur les étapes d'enrichissement coûteuses qui suivent. ## Entrées Les règles sont lues depuis l'objet `config` du nœud et appliquées ligne par ligne dans un ordre fixe. Chaque clé est optionnelle ; une règle vide est sans effet. ### Règles standard | Clé | Type | Comportement | | --- | --- | --- | | `require_phone` | `bool` | Garde les lignes où `telephone` est non vide. | | `require_site` | `bool` | Garde les lignes où `site_web` est non vide. | | `require_email` | `bool` | Garde les lignes où `email` est non vide. | | `exclude_aggregators` | `bool` | Écarte les lignes dont `site_web` pointe vers un domaine d'agrégateur connu. | | `alive_only` | `bool` | Garde les lignes dont le `status` de dead-check est `alive` ou `stale`. | | `has_personal_email` | `bool` | Garde les lignes où au moins une adresse dans `email` est une boîte personnelle (non basée sur un rôle). | | `rating_min` | `float` | Garde les lignes où `note >= rating_min`. | | `reviews_min` | `int` | Garde les lignes où `nb_avis >= reviews_min`. | ### Règles avancées | Clé | Forme | Comportement | | --- | --- | --- | | `phone_prefix` | `{ column?, prefixes[], prefix_unparseable_keep? }` | Garde les lignes dont la colonne téléphone commence par l'un des `prefixes` (par ex. `06`, `+33`). Nécessite la bibliothèque `phonenumbers` sur le worker — sinon la règle est journalisée et ignorée. | | `email_domain` | `{ column?, include[], exclude[], reject_disposable? }` | Garde les lignes dont le domaine d'email est dans `include` (si défini) et pas dans `exclude`. `reject_disposable` écarte les fournisseurs jetables connus. | | `category` | `{ column, values[] }` | Garde les lignes dont la valeur de `column` est contenue dans `values`. | | `dedup_column` | `string` | Fusionne les lignes partageant la même valeur sur cette colonne (la première ligne l'emporte). | ### Échantillonnage | Clé | Type | Comportement | | --- | --- | --- | | `sample_type` | `"n" \| "pct" \| ""` | Sélectionne le mode d'échantillonnage appliqué après les règles ci-dessus. | | `sample_n` | `int` | Garde les `n` premières lignes retenues. | | `sample_pct` | `0..100` | Garde un pourcentage des lignes retenues. | | `sample_seed` | `int` | Graine pour un échantillonnage aléatoire reproductible. | Ordre d'application : drapeaux d'exigence → agrégateurs/alive/note/avis → email personnel → `phone_prefix` → `email_domain` → `category` → `dedup_column` → échantillonnage. ## Sorties Le module écrit un CSV avec les mêmes colonnes que le nœud amont, contenant uniquement les lignes retenues. Il ne produit pas de nouveaux champs (`needs: []`, `produces: []`, `pipeline_passthrough: true`). | Champ | Valeur | | --- | --- | | `output_filename` | `results_