API AIGEN

Génération d'offres · Synthèse candidat · Enrichissement entreprise · Claude Sonnet 4.6 · AWS Bedrock

Architecture

graph LR BUBBLE["🫧 Bubble
Application client"] API["⚙️ API AIGEN
FastAPI · Python"] BEDROCK["☁️ AWS Bedrock
Claude Sonnet 4.6"] DBMAIN[("🗄️ DB Main
PostgreSQL
Offres de référence
")] DBLOGS[("📊 DB Logs
PostgreSQL
KPIs · Coûts
")] OPENDATA["🏛️ open-data
SIRENE · INSEE · INPI
Données officielles
"] GROUNDING["🔎 Vertex AI
Grounding Google Search
Gemini 2.5 Flash
"] WEB["🌐 Site corporate
Fetch httpx
robots.txt respecté
"] BUBBLE -->|"REST / Bearer token"| API API -->|"AWS SigV4 · prompt TOON"| BEDROCK BEDROCK -->|"JSON response"| API API -->|"Lecture offres
de référence"| DBMAIN API -->|"Écriture logs
+ feedbacks"| DBLOGS API -->|"Réponse JSON"| BUBBLE API -->|"Données officielles
(X-Api-Key)"| OPENDATA API -->|"Site officiel · LinkedIn
· dernières offres"| GROUNDING API -->|"Contenu corporate
(site découvert/fourni)"| WEB style API fill:#7c3aed,stroke:#6d28d9,color:#fff style BUBBLE fill:#1e40af,stroke:#1d4ed8,color:#fff style BEDROCK fill:#065f46,stroke:#047857,color:#fff style DBMAIN fill:#1a1f2e,stroke:#4a5568,color:#e2e8f0 style DBLOGS fill:#1a1f2e,stroke:#4a5568,color:#e2e8f0 style OPENDATA fill:#92400e,stroke:#b45309,color:#fff style GROUNDING fill:#3730a3,stroke:#4338ca,color:#fff style WEB fill:#1e3a5f,stroke:#2563eb,color:#fff

Endpoints

Méthode Endpoint Description Source externe
Offres d'emploi
POST /api/v1/offers/generate Génère les 4 sections d'une offre complète DB Main (offres de référence)
POST /api/v1/offers/generate-section Génère un seul encart au choix DB Main
POST /api/v1/offers/modify Modifie les 4 sections existantes (style, langue, ton)
POST /api/v1/offers/modify-section Modifie une section (corriger / raccourcir / allonger)
POST /api/v1/offers/refine-section Affine une section via une instruction libre (Att_05)
POST /api/v1/offers/feedback Notation 1-5 étoiles — offres uniquement
POST /api/v1/offers/feedback/thumbs Feedback pouce haut/bas — offres uniquement
Candidats
POST /api/v1/candidates/generate-synthese Génère une synthèse candidat HTML (courte <1 000 car. ou longue <2 000 car.)
POST /api/v1/candidates/refine-synthese Affine une synthèse candidat existante via instruction en langage naturel (Att_05)
Entreprises
POST /api/v1/entreprises/enrich Enrichit automatiquement une fiche entreprise (open-data + IA) — politique FILL_EMPTY_ONLY open-data · Grounding Google Search · site corporate
POST /api/v1/entreprises/offers Recherche les dernières offres d'emploi de l'entreprise (synchrone, un appel) Grounding Google Search (Gemini)
Feedback global
POST /api/v1/feedback Notation 1-5 étoiles — toutes fonctionnalités
POST /api/v1/feedback/thumbs Feedback pouce haut/bas — toutes fonctionnalités
Session
GET /api/v1/session/{session_id} Vérifie l'existence d'une session et retourne ses logs
POST /api/v1/session/finalize Rattache une session temporaire à l'ID officiel Bubble
Emails
POST /api/v1/emails/generate Génère sujet + corps d'email via Assist IA — 14 types répartis sur 4 contextes (Candidat / Contact / Proposition active / Recrutement). Voir Types par contexte
POST /api/v1/emails/refine Affine une génération d'email existante à partir d'une instruction en langage naturel (Att_07) — stateless, même contrat que /generate
Système
GET /health Vérification de santé de l'API

Headers

Authorization
Bearer <token> — obligatoire sur tous les endpoints
X-Uid-User
UID de l'utilisateur connecté dans Bubble — tous les endpoints
X-Uid-Agence-Mere
UID de l'agence mère — tous les endpoints
X-Uid-Agence-Fille
UID de l'agence fille — tous les endpoints
X-Uid-Candidat
UID Bubble du candidat — candidates uniquement. Tracé dans session_entities.
X-Uid-Client
UID Bubble de l'entreprise — entreprises uniquement. Tracé dans session_entities.
X-Uid-Job
UID Bubble du poste / de la mission — emails, types orientés mission (PROPOSITION_MISSION, CONFIRMATION_MISSION, DEMANDE_DISPO) uniquement. Tracé dans bubble_job_id du log email pour relier la génération à une fiche poste. Aucun impact sur le contenu généré.

Offres d'emploi — flux /generate

🫧
Bubble envoie
la requête
🔐
Vérification
Bearer token
🗄️
Lecture ≤3 offres
de référence
📝
Prompt TOON
(−50% tokens)
☁️
Appel
Bedrock
📊
Log KPIs
+ coûts
HTML TinyMCE
à Bubble

Offres d'emploi — champs d'input

ChampTypeDescriptionObligatoire
job_titlestringMétier / intitulé du poste (max 500 car.)
sectionSectionNameSection à générer — generate-section uniquement✅ (generate-section)
type_contratstringType de contrat : CDI · CDD · Intérim · Stage · Alternance · … — adapte le ton et le profil cible (ex. Stage → étudiants). Si absent, le type de contrat n'est pas mentionné dans l'offre.
competenceslist[string]Compétences essentielles du candidat
qualificationslist[string]Diplômes, certifications, habilitations (ex. "CAP Plomberie", "CACES 3")
localisationstringVille / région du poste
experience_requisestringExpérience requise (ex. "2 ans minimum")
date_debutstringDate de début de mission
date_finstringDate de fin de mission
favoris_clientslist[string]Textes d'offres favorites — référence stylistique
session_idstringID de session Bubble pour traçabilité
styleModifyStyleStyle de rédaction
language_typeLanguageTypeType de langage
translationTranslationLanguageLangue de sortie
inclusiveInclusiveOptionLangage inclusif oui/non
user_promptstringInstruction libre transmise au modèle (max 1 000 car.). Injectée dans le prompt avec un encadrement de conformité — le modèle l'ignore si elle est discriminatoire, illégale ou hors sujet. Disponible sur generate, generate-section, modify, modify-section et refine-section.

Offres d'emploi — sections & actions

section
description profil responsabilites elements_complementaires
action (modify-section)
corriger raccourcir allonger

Limites HTML visible : description 1 000 car. · profil 2 000 car. · responsabilites 1 000 car. · elements_complementaires 500 car.

Offres d'emploi — affiner (Att_05)

POST /api/v1/offers/refine-section retravaille une rubrique déjà générée à partir d'une instruction en langage naturel. Il alimente le bloc « Affiner avec l'IA » sous chaque champ, y compris ses chips de raccourcis (« Rends ça plus court », « Ton plus dynamique », « Infos transports ») qui ne sont que des instructions pré-remplies.

Il complète /modify-section, dont l'action reste limitée aux trois valeurs fermées corriger / raccourcir / allonger. Stateless comme /api/v1/emails/refine et /api/v1/candidates/refine-synthese : Bubble renvoie à chaque itération la version courante (celle affichée, ou celle restaurée via « Revenir à cette version ») ; le fil de conversation et l'historique des versions sont gérés côté Bubble, pas par l'API.

ChampTypeDescriptionObligatoire
sectionSectionNameRubrique à affiner — description, profil, responsabilites, elements_complementaires
current_textstringVersion actuelle de la rubrique à retravailler (HTML TinyMCE)
instructionstringInstruction de retravail en langage naturel (ex. « utilise un ton plus dynamique, mets en avant les valeurs de l'entreprise »), max 500 car.
session_idstringID de session Bubble pour la traçabilité du recrutement

Les options de ton (style, language_type, translation, inclusive, user_prompt) sont acceptées comme sur les autres endpoints — voir Options de ton. Réponse identique à /modify-section : section, text, tokens, model.

Le LLM part de la version actuelle fournie et applique uniquement le changement demandé — il conserve le reste du contenu et les faits déjà présents sans en inventer de nouveaux sur le poste, l'entreprise ou la rémunération. L'instruction utilisateur est injectée avec un encadrement de conformité (ignorée si discriminatoire, illégale ou hors sujet). Loggé avec request_type=refine_section dans offer_generation_log.
Le champ « Présentation de l'entreprise » est hors périmètre IA (Att_02 / Att_03) : il n'est pas une valeur de SectionName, donc l'API rejette la requête en 422 si Bubble tente de l'affiner.

Offres d'emploi — options de ton

style
professionnelinformelinspirantconvivialengageant
language_type
vouvoiementtutoiementneutre
translation
francaisanglais
inclusive
ouinon
Note : sur /offers/generate, le paramètre style est transmis comme label uniquement. Sur tous les autres endpoints, il inclut les instructions détaillées par section.

Offres d'emploi — offres de référence

L'API recherche automatiquement jusqu'à 3 offres de référence dans la DB Main pour guider le style. Priorité :

PrioritéSourceCritère métier
1Agence fille (X-Uid-Agence-Fille)Même métier
2Agence filleN'importe quel métier
3Agence mère (X-Uid-Agence-Mere)Même métier
4Agence mèreN'importe quel métier

Sections récupérées : description · profil · responsabilites. Format TOON pour économie de tokens.

Candidats — flux synthèse

🫧
Bubble envoie
cv_data (JSON)
🔐
Auth Bearer
+ headers UID
📝
Prompt synthèse
(courte / longue)
☁️
Appel
Bedrock
📊
Log + session
CANDIDAT
HTML TinyMCE
à Bubble

Candidats — champs d'input

ChampTypeDescriptionObligatoire
cv_datadictDonnées du candidat en JSON (parsing CV ou fiche Bubble). Champs attendus : nom, titre, experience, formation, competences, langues
longueurSyntheseLongueurcourte — résumé <1 000 car. · longue — synthèse <2 000 car.
sourceSyntheseSourcecv_parsing (premier affichage, pas d'updated_at) · fiche_candidat (mise à jour, retourne updated_at)
session_idstringID de session Bubble
Header requis en plus : X-Uid-Candidat — UID Bubble du candidat, tracé dans session_entities avec le type candidat.

Candidats — affiner la synthèse (Att_05)

POST /api/v1/candidates/refine-synthese retravaille une synthèse déjà générée à partir d'une instruction en langage naturel (bloc « Affiner avec l'IA »). Stateless comme /api/v1/emails/refine : Bubble renvoie à chaque itération la version courante (celle affichée ou celle restaurée via « Revenir à cette version ») ; le fil de conversation est géré côté Bubble, pas par l'API.

ChampTypeDescriptionObligatoire
cv_datadictMêmes données que /generate-synthese — source de vérité pour éviter toute invention
current_synthesestringVersion actuelle de la synthèse à affiner (HTML TinyMCE)
instructionstringInstruction de retravail en langage naturel (ex. « Rends ça plus court », « Ton plus dynamique », « Infos transports »), max 500 car.
longueurSyntheseLongueurcourte (<1 000 car.) · longue (<2 000 car.)
sourceSyntheseSourcecv_parsing · fiche_candidat (défaut — un affinage est une mise à jour, updated_at renseigné)
session_idstringID de session Bubble
Le LLM part de la version actuelle fournie et applique uniquement le changement demandé — il conserve le reste du contenu et des faits déjà présents sans en inventer de nouveaux. Réponse identique à /generate-synthese (HTML TinyMCE + tokens + model). Loggé avec action=REFINE dans synthese_candidat_log. Header X-Uid-Candidat attendu comme pour la génération.

Entreprises — flux enrichissement

🫧
Bubble envoie
SIRET / SIREN / nom
🏛️
open-data
(OFFICIEL)
🔎
Grounding Google
site + LinkedIn
🌐
Fetch site officiel
(robots.txt)
☁️
Bedrock
(description, pages, tél.)
🔒
FILL_EMPTY
_ONLY
📊
Log + session
ENTREPRISE
JSON enrichi
+ summary

Entreprises — champs d'input

ChampTypeDescriptionObligatoire
sirenstringSIREN de l'entreprise (9 chiffres)Au moins un
des trois
siretstringSIRET de l'entreprise (14 chiffres) — les 9 premiers sont utilisés comme SIREN
raison_socialestringNom de l'entreprise (recherche par texte si SIREN absent)
website_urlstringURL du site corporate si déjà connue dans Bubble (optionnel). Si absente, elle est découverte automatiquement via Grounding Google Search. Le site retenu est ensuite fetché (robots.txt vérifié) pour en extraire la description et les pages web.
champs_existantsdictChamps déjà renseignés dans Bubble — structure { "web": {...}, "identity": {...}, "recruiting": {...} }. Les champs non-null ici ne seront jamais écrasés (FILL_EMPTY_ONLY).
session_idstringID de session Bubble pour traçabilité
Header requis en plus : X-Uid-Client — UID Bubble de l'entreprise, tracé dans session_entities avec le type entreprise.

Entreprises — champs_existants en détail

champs_existants sert à implémenter la politique FILL_EMPTY_ONLY : tout champ passé avec une valeur non-null ne sera jamais écrasé par l'enrichissement. Passer null ou omettre le champ pour laisser l'API l'enrichir.

Les valeurs passées sont des valeurs brutes (string, number…), pas des objets Field.

Bloc web

ChampType valeurDescription
website_urlstringURL du site corporate (ex : https://www.acme.fr)
linkedin_company_urlstringURL de la page LinkedIn entreprise
careers_page_urlstringURL de la page carrières / recrutement
contact_page_urlstringURL de la page contact
legal_notice_urlstringURL des mentions légales

Bloc identity

ChampType valeurDescription
descriptionstringDescription textuelle de l'entreprise (max 1 000 car.)
logo_urlstringURL du logo
phone_numberstringStandard téléphonique de l'entreprise
last_known_revenue_amountintegerDernier CA connu en EUR (ex. 46394000000)
last_known_revenue_yearintegerAnnée du CA (ex. 2023)

Bloc recruiting

ChampType valeurDescription
active_job_ads_countnumberNombre d'offres d'emploi actives (toujours null côté API — voir POST /offers)
active_job_ads_search_urlstringURL de recherche d'offres (Indeed généré automatiquement si absent)
sourcesstring[]Plateformes de recrutement utilisées (toujours null côté API)
top_qualificationsstringToujours null côté API — voir POST /offers (endpoint dédié) pour les dernières offres et le top 5 des qualifications demandées
Exemple minimal : Bubble a déjà le site et le logo, veut enrichir le reste.

"champs_existants": { "web": { "website_url": "https://www.acme.fr" }, "identity": { "logo_url": "https://www.acme.fr/logo.png" }, "recruiting": {} }
Résultat : website_url et logo_url ne seront pas écrasés. Tous les autres champs sont enrichis normalement.

Entreprises — structure de la réponse

Chaque champ enrichi est un objet Field avec ses métadonnées de traçabilité. Le type de value est fixe par champ — il ne change jamais.

// Champ texte (StringField) — value toujours string ou null
"nom_du_champ": {
  "value": string | null, // la donnée extraite, null si absente des sources
  "mode": "OFFICIEL" | "PUBLIC_WEB" | "ESTIMATED",
  "confidence": float, // 0.0 → 1.0
  "source_label": string | null,
  "source_url": string | null,
  "retrieved_at": string, // ISO 8601 UTC
  "needs_review": bool
}

// Champ numérique (IntField) — value toujours integer ou null
"nom_du_champ": {
  "value": integer | null,
  // … mêmes métadonnées que StringField
}
Champ entier = null : lorsque FILL_EMPTY_ONLY s'applique, le Field entier est null (pas {"value": null, ...}). Cela signifie "ne pas mettre à jour ce champ dans Bubble".

Blocs et types de chaque champ

BlocChampType valueSource
officiel
officielsiretstringOFFICIEL
officielsirenstringOFFICIEL
officielsiege_adressestringOFFICIEL
officieltranche_effectifstringOFFICIEL
officielforme_juridiquestringOFFICIEL
officielcode_nafstringOFFICIEL
officieldirigeantstringOFFICIEL
officielannee_creationstringOFFICIEL
web
webwebsite_urlstringPUBLIC_WEB
weblinkedin_company_urlstringPUBLIC_WEB
webcareers_page_urlstringPUBLIC_WEB
webcontact_page_urlstringPUBLIC_WEB
weblegal_notice_urlstringPUBLIC_WEB
identity
identitydescriptionstring (max 1 000 car.)PUBLIC_WEB
identitylogo_urlstringPUBLIC_WEB
identityphone_numberstringPUBLIC_WEB
identitylast_known_revenue_amountinteger (EUR)OFFICIEL
identitylast_known_revenue_yearinteger (ex. 2023)OFFICIEL
recruiting
recruitingactive_job_ads_countinteger — toujours null iciESTIMATED
recruitingactive_job_ads_search_urlstring (URL Indeed générée)ESTIMATED
recruitingsourcesstring — toujours null iciESTIMATED
recruitingtop_qualificationsstring — toujours null ici, voir POST /offersESTIMATED
enrichment_summary
enrichment_summary.fields_filledinteger
enrichment_summary.fields_needs_reviewinteger
enrichment_summary.statusstring : succès | partiel | erreur
enrichment_summary.sources_usedarray — voir ci-dessous
tokens
tokens.input_tokens · output_tokens · total_tokensinteger (0 si sans appel Bedrock)
tokens.cost_usdfloat
modelstring (ex. claude-sonnet-4-6)
needs_review = true lorsque mode ∈ {PUBLIC_WEB, ESTIMATED} ou confidence < 0.75. Ces champs doivent être présentés avec un badge "À valider" dans l'interface Bubble avant tout usage.

Entreprises — sources de données

SourceModeAuthDonnées
open-data (self-hosted : SIRENE / INSEE / INPI) OFFICIEL Header X-Api-Key Nom, SIREN, SIRET siège, code NAF, adresse, tranche d'effectif (INSEE), dernier CA connu (INPI-BCE). Forme juridique / dirigeant / année de création / téléphone non fournis par open-data → null.
Vertex AI — Grounding with Google Search (modèle gemini-2.5-flash) PUBLIC_WEB Compte de service GCP (rôle aiplatform.user) Découverte de l'URL du site officiel et de la page LinkedIn. Un modèle Gemini interroge l'index Google en direct et renvoie URLs + extraits — api-aigen ne crawle pas les sites cibles.
Site corporate PUBLIC_WEB Aucune — robots.txt vérifié avant fetch Description, URL carrières, contact, mentions légales, standard téléphonique — extraites du site officiel (fourni dans la requête ou découvert via le grounding) par Bedrock.
Dernières offres d'emploi PUBLIC_WEB Via POST /api/v1/entreprises/offers Liste des dernières offres publiées et top 5 des qualifications les plus demandées à travers ces offres, trouvées et structurées en un seul appel par le grounding Google (Gemini). Le bloc recruiting de /enrich reste léger (URL de recherche générée).

Entreprises — politique FILL_EMPTY_ONLY

L'enrichissement ne remplace jamais une valeur déjà saisie manuellement dans Bubble.

Comment ça marche : Bubble passe les valeurs existantes dans champs_existants. Pour chaque champ non-null dans ce dict, l'API annule la valeur LLM correspondante (mise à null) avant de retourner la réponse. Les autres champs sont enrichis normalement.
// Exemple : website_url déjà renseigné, ne pas toucher
{
  "champs_existants": {
    "web": { "website_url": "https://acme.fr" },
    "identity": {},
    "recruiting": {}
  }
}

→ Dans la réponse, web.website_url sera null. Tous les autres champs sont enrichis normalement.

Entreprises — dernières offres d'emploi

POST /api/v1/entreprises/offers — recherche synchrone, en un seul appel, les dernières offres d'emploi publiées par l'entreprise et le top 5 des qualifications les plus demandées à travers ces offres, via Vertex AI — Grounding with Google Search (modèle gemini-2.5-flash). Le modèle Gemini interroge l'index Google en direct et renvoie les deux déjà structurés dans la même réponse (pas d'appel Bedrock séparé, pas d'appel Vertex supplémentaire).

Périmètre légal : on ne consomme que les résultats renvoyés par Google (titres, extraits, domaine source). api-aigen ne crawle aucun site d'offres ici.
À propos des liens : url contient le meilleur lien disponible (lien de l'offre, sinon page carrières de l'entreprise ou page du job board — ex. jooble.org, optioncarriere.com) ; ce n'est pas toujours un lien profond par offre, et peut être null si aucun lien n'est trouvé. source = domaine réel. La résolution par raison_sociale seule est floue — préférer le siret pour un ciblage exact.

Requête

ChampTypeDescriptionRequis
sirenstringSIREN (9 chiffres)au moins un des trois
siretstringSIRET (14 chiffres)identifiants
raison_socialestringNom de l'entreprise. Si absent, résolu via open-data depuis siret/siren
villestringVille pour affiner la recherche (optionnel)
limitintegerNombre max d'offres, 1–20 (défaut 5)
session_idstringID de session Bubble (traçabilité)

Réponse

ChampTypeDescription
offersobject[]Liste d'offres — {titre, lieu, contrat, url, source, date_publication, extrait}. url = meilleur lien dispo (offre / page carrières / job board), parfois null ; source = domaine réel.
countintegerNombre d'offres retournées
top_qualificationsstring[]5 qualifications/compétences les plus demandées à travers les offres trouvées (ex. ["CACES 3", "Permis B"]) ; liste vide si aucune offre trouvée
search_availableboolfalse si le grounding n'était pas disponible/configuré (offres vides, aucune erreur)
raison_socialestringRaison sociale résolue utilisée pour la recherche
tokensobjectinput_tokens, output_tokens, total_tokens (Gemini) ; cost_usd = 0
modelstringModèle Gemini utilisé (ou none si recherche indisponible)
Endpoint distinct de l'enrichissement de base : Bubble l'appelle séparément (ex. onglet « Offres » de la fiche client). Le bloc recruiting de /enrich ne contient qu'une URL de recherche générée — ni la liste des offres, ni les qualifications.

Emails — flux génération

🫧
Bubble envoie
type d'email + paramètres
🔐
Auth Bearer
+ headers UID
🛡️
Pré-check
anti-discrimination
📝
Prompt email
verrouillé
☁️
Appel
Bedrock
🛡️
Post-check
sortie LLM
📊
Log complet
email_ai_log
Sujet + Corps
à Bubble
Aucun envoi automatique. L'utilisateur relit et valide toujours le contenu avant envoi. Le badge « ✳️ Contenu généré par IA – à valider » doit rester visible.
/emails/refine (Att_07) suit exactement le même pipeline (pré-check → prompt → Bedrock → post-check → log), avec action=REFINE dans le log au lieu de GENERATE. Voir section Affiner.

Emails — types par contexte (Att_03)

Le contexte d'envoi est détecté automatiquement côté Bubble et détermine les types proposés. Le champ context est optionnel mais affine la rédaction. AUTRE (message libre) est disponible dans tous les contextes.

Contexte (context)email_typeIntention
CANDIDATRELANCE_CANDIDATSuivi après envoi d'offre
DEMANDE_DISPODisponibilité pour mission
PROPOSITION_MISSIONOffre ciblée candidat
PRESENTATION_CANDIDATDétails du candidat
CONFIRMATION_MISSIONDétails de la mission
AUTREMessage libre
CONTACTPRISE_CONTACTPremier contact avec le prospect
RELANCE_COMMERCIALESuivi après échange resté sans réponse
COMPTE_RENDU_RDVSynthèse et prochaines étapes
PROPOSITION_COLLABORATIONPrésentation de l'offre de service
AUTREMessage libre
PROPOSITION_ACTIVEPROPOSITION_CANDIDATSValoriser un ou plusieurs profils
RELANCE_PROPOSITIONSuivi d'une proposition envoyée
DISPONIBILITE_PROFILSignaler un candidat disponible
AUTREMessage libre
RECRUTEMENTREPONSE_COMMANDEProposer des candidats pour le poste
PRESENTATION_CANDIDATDétails du candidat retenu
AUTREMessage libre

Emails — paramètres communs (tous types)

ChampTypeValeurs / DescriptionObligatoire
contextstring CANDIDAT · CONTACT · PROPOSITION_ACTIVE · RECRUTEMENT — contexte d'envoi détecté côté Bubble (Att_03) ; détermine les types proposés et affine la rédaction
email_typestring Type sélectionné (voir Types par contexte) — 14 valeurs : RELANCE_CANDIDAT, DEMANDE_DISPO, PROPOSITION_MISSION, PRESENTATION_CANDIDAT, CONFIRMATION_MISSION, PRISE_CONTACT, RELANCE_COMMERCIALE, COMPTE_RENDU_RDV, PROPOSITION_COLLABORATION, PROPOSITION_CANDIDATS, RELANCE_PROPOSITION, DISPONIBILITE_PROFIL, REPONSE_COMMANDE, AUTRE
tonestring Professionnel · Chaleureux · Dynamique · Formel · Bienveillantdéfaut Professionnel si omis (Att_04), modifiable par l'utilisateur
recipients[].typestring CLIENT — client existant · PROSPECT — pas encore client · CANDIDAT — candidat en base
recipients[].company_namestring Nom de l'entreprise ou agence destinataire
recipients[].contact_namestring Nom du contact (optionnel, utilisé pour la personnalisation)
recipients[].contact_rolestring Fonction du contact (ex : Responsable RH)
recipients[].vouvoiementbool true = vouvoiement (défaut) · false = tutoiement
variablesdict Variables Bubble disponibles. Clé = nom de la variable, valeur = description optionnelle pour l'IA (ou null). Ex. {"nom_contact": "Civilité et nom", "signature_agence": null}. L'IA place les #clé# dans l'email ; Bubble fait le remplacement. Voir section Variables Bubble.
session_idstring ID de session Bubble pour traçabilité
Headers requis en plus : X-Uid-User · X-Uid-Agence-Mere · X-Uid-Agence-Fille
Header optionnel (types orientés mission uniquement) : X-Uid-Job — UID Bubble du poste / de la mission. Tracé dans bubble_job_id du log pour relier l'email à une fiche poste Bubble. Ne pas envoyer pour les autres types.

Emails — candidats & poste / mission

candidates[] et job sont communs à tous les types d'email — à renseigner selon la pertinence du email_type choisi (ex : job pour PROPOSITION_MISSION, CONFIRMATION_MISSION, DEMANDE_DISPO).

ChampTypeDescriptionObligatoire
candidates[].uidstringUID Bubble du candidat — tracé dans candidate_ids du log
candidates[].first_namestringPrénom du candidat (utilisé via le placeholder CANDIDATE_FIRSTNAME)
candidates[].last_namestringNom du candidat
candidates[].profile_titlestringIntitulé du profil (ex : Soudeur TIG N2)
candidates[].job_mainstringMétier principal (champ jobMainLowercase Bubble)
candidates[].years_exp_or_levelstringExpérience ou niveau (ex : 4 ans, Confirmé)
candidates[].skills[]string[]Compétences clés
candidates[].certifications[]string[]Certifications et habilitations
candidates[].availabilitystringDisponibilité (ex : Immédiate, 01/06/2026)
candidates[].disponibilite_osstringDisponibilité opérationnelle (champ Bubble complémentaire)
candidates[].mobility_areastringZone de mobilité (ex : Grand Lyon)
candidates[].distance_accepteestringDistance de déplacement acceptée
candidates[].grand_deplacementbooltrue si le candidat accepte les grands déplacements
candidates[].private_notestringNote privée du recruteur sur le candidat — exploitée par le LLM pour personnaliser l'email, passée au pré-check anti-discrimination
Poste / mission — PROPOSITION_MISSION, CONFIRMATION_MISSION, DEMANDE_DISPO
job.job_titlestringIntitulé du poste
job.job_locationstringLieu de la mission
job.job_contractstringType de contrat (CDI, Intérim, CDD…)
job.job_textstringDescriptif complet du poste. Si absent, le LLM reste générique sans inventer.
context_textstringContexte libre complémentaire (étape 3, facultative — saisi ou issu des suggestions cliquables, max 800 car.)
Header X-Uid-Job : UID Bubble du poste / de la mission. Tracé dans bubble_job_id du log pour relier l'email à la fiche poste.

Emails — affiner (Att_07)

POST /api/v1/emails/refine retravaille une génération existante à partir d'une instruction en langage naturel. Stateless comme /api/v1/offers/refine-section : Bubble renvoie la version courante (celle affichée ou celle restaurée via « Revenir à cette version ») à chaque itération ; le fil de conversation est géré côté Bubble, pas par l'API.

Le corps reprend tous les champs de /generate (email_type, tone, recipients, candidates, job, context_text, variables, session_id) plus :

ChampTypeDescriptionObligatoire
current_subjectstringSujet de la version à affiner
current_body_htmlstringCorps HTML de la version à affiner
current_body_textstringCorps texte brut de la version à affiner
instructionstringInstruction de modification en langage naturel (ex. « raccourcis », « ajoute les détails de la mission »), max 500 car.
Le LLM part de la version actuelle fournie et applique uniquement le changement demandé — il conserve le reste du contenu et des faits déjà présents sans en inventer de nouveaux. Réponse et statuts de sécurité identiques à /generate (voir section Réponse).

Emails — variables Bubble

Bubble gère ses propres variables. L'API ne connaît pas les valeurs — elle place les #clé# dans le texte, Bubble effectue le remplacement de son côté.

Workflow :
1. Bubble passe dans variables les noms de ses variables (+ description optionnelle pour le contexte IA)
2. L'IA génère l'email en utilisant #clé# là où c'est pertinent
3. La réponse contient #clé# bruts — Bubble fait ses propres remplacements
4. placeholders_used liste les #clé# effectivement détectés dans le contenu généré (extraction serveur, fiable même si l'IA en oublie un dans son propre récapitulatif)
// Exemple de payload variables
"variables": {
  "nom_contact": "Civilité et nom du contact destinataire", // description pour l'IA
  "signature_agence": null, // sans description
  "nom_agence": "Raison sociale de l'agence"
}

Les noms de clés sont libres — Bubble définit ses propres variables. La description (valeur du dict) est optionnelle : elle aide l'IA à comprendre le contexte pour bien placer le #clé#. Passer null si inutile.

Cas particulier — synthèse candidat

Quand variables expose une clé de synthèse candidat et que des candidates sont rattachés à un email destiné à un client ou un prospect, l'API génère une consigne dédiée pour que l'IA insère la variable au lieu de rédiger elle-même les noms, prénoms et détails de profil (comportement constaté sinon : l'IA recopie les données de candidates).

  • La clé est détectée par motif dans variables (synthese_candidat, synthese_candidats, Synthese Candidats… — insensible à la casse, tolère _, - ou l'espace).
  • Le singulier ou le pluriel est choisi selon len(candidates) : avec 2 candidats et les deux clés proposées, l'IA reçoit #synthese_candidats#. Si Bubble n'envoie qu'une des deux formes, c'est celle-là qui est utilisée.
  • Le nommage est libre : syntheseCandidats en camelCase est reconnu comme synthese_candidats. Si variables n'expose aucune clé de synthèse, aucune consigne n'est émise et l'IA rédige les profils elle-même — l'API n'impose jamais un nom par défaut, car une clé que Bubble ne saurait pas substituer ressortirait en clair dans l'email envoyé au client.
  • Les first_name/last_name sont retirés du contexte envoyé au LLM quand la consigne s'applique : il ne peut pas écrire un nom qu'il n'a pas. La consigne seule ne suffisait pas — l'IA recopiait les noms qu'elle avait sous les yeux.
  • La consigne ne s'applique que si au moins un destinataire n'est pas de type CANDIDAT (client ou prospect). Un email adressé au candidat lui-même — RELANCE_CANDIDAT, DEMANDE_DISPO, CONFIRMATION_MISSION… — conserve donc les noms et ne reçoit pas de synthèse.
  • #CANDIDATE_FIRSTNAME# reste autorisé pour la salutation quand l'email est adressé au candidat lui-même.
Si la valeur injectée par Bubble contient du HTML (cas des synthèses issues de /generate-synthese, produites pour TinyMCE), elle arrivera telle quelle dans body_text après substitution : la normalisation côté API s'applique au texte généré, pas à ce que Bubble injecte ensuite.

Emails — réponse & statuts de sécurité

Champ réponseTypeDescription
subjectstring | nullSujet de l'email généré. null si safety.status = BLOCKED
body_htmlstring | nullCorps HTML simple (<p>, <br>, <ul>, <li>). null si BLOCKED
body_textstring | nullCorps texte brut — garanti sans balise ni entité HTML : le balisage éventuellement laissé par le LLM (<br>, <li>, &lt;p&gt;, &nbsp;…) est converti en sauts de ligne / tirets côté serveur. null si BLOCKED
placeholders_usedstring[]Placeholders #clé# (génériques ou variables Bubble) réellement détectés dans subject/body_html/body_text — extraction par regex côté serveur, indépendante de ce que le LLM déclare. Les échappements du LLM (#synthese\_candidat#) sont retirés avant renvoi, pour que Bubble retrouve la clé exacte
safety.statusstringOK · BLOCKED · NEEDS_REVIEW
safety.reasonsstring[]Raisons du blocage ou des points à valider
tokensobjectinput_tokens, output_tokens, total_tokens, cost_usd
modelstringIdentifiant du modèle utilisé
✅ OK
Contenu conforme. subject et body_* remplis. Appliquer la politique FILL_EMPTY_ONLY avant d'écraser un brouillon existant.
🚫 BLOCKED
Contenu risqué détecté (discrimination, donnée sensible, illégal). subject et body_* sont null. Afficher les reasons à l'utilisateur.
⚠️ NEEDS_REVIEW
Contenu à valider manuellement. Champs remplis en brouillon avec badge « à valider ». Afficher les reasons.
Codes HTTP : 200 même si BLOCKED (le statut est dans le body) · 401 token invalide · 422 payload invalide · 502 erreur Bedrock ou JSON LLM non parsable

Traçabilité session

Tous les appels sont tracés via deux identifiants complémentaires, stockés dans generation_logs et session_entities.

ChampGéré parDescription
session_id Bubble ID temporaire généré par Bubble en début de recrutement. À passer sur tous les appels (generate, modify, synthèse, enrich, feedback).
bubble_offer_id Bubble via POST /session/finalize ID officiel de l'offre dans Bubble, rattaché a posteriori à tous les logs de la session en un seul appel.

Table session_entities

Chaque session peut être liée à une entité Bubble identifiée :

candidat
Via header X-Uid-Candidat sur /candidates/generate-synthese et /candidates/refine-synthese
entreprise
Via header X-Uid-Client sur /entreprises/enrich
offre
Futur usage — génération d'offres
client
Futur usage — fiche client agence

Feedback

Deux types de feedback, disponibles en version globale (toutes fonctionnalités) ou en version rattachée aux offres uniquement.

EndpointTypeChamps
/api/v1/feedback
/api/v1/offers/feedback
Étoiles (1-5) rating (1-5, obligatoire) · comment (max 250 car.) · session_id
/api/v1/feedback/thumbs
/api/v1/offers/feedback/thumbs
Pouce haut/bas thumb (up|down, obligatoire) · comment (max 250 car.) · session_id