Architecture
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
session_entities.session_entities.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
la requête
Bearer token
de référence
(−50% tokens)
Bedrock
+ coûts
à Bubble
Offres d'emploi — champs d'input
| Champ | Type | Description | Obligatoire |
|---|---|---|---|
job_title | string | Métier / intitulé du poste (max 500 car.) | ✅ |
section | SectionName | Section à générer — generate-section uniquement | ✅ (generate-section) |
type_contrat | string | Type 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. | — |
competences | list[string] | Compétences essentielles du candidat | — |
qualifications | list[string] | Diplômes, certifications, habilitations (ex. "CAP Plomberie", "CACES 3") | — |
localisation | string | Ville / région du poste | — |
experience_requise | string | Expérience requise (ex. "2 ans minimum") | — |
date_debut | string | Date de début de mission | — |
date_fin | string | Date de fin de mission | — |
favoris_clients | list[string] | Textes d'offres favorites — référence stylistique | — |
session_id | string | ID de session Bubble pour traçabilité | — |
style | ModifyStyle | Style de rédaction | — |
language_type | LanguageType | Type de langage | — |
translation | TranslationLanguage | Langue de sortie | — |
inclusive | InclusiveOption | Langage inclusif oui/non | — |
user_prompt | string | Instruction 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
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.
| Champ | Type | Description | Obligatoire |
|---|---|---|---|
section | SectionName | Rubrique à affiner — description, profil, responsabilites, elements_complementaires | ✅ |
current_text | string | Version actuelle de la rubrique à retravailler (HTML TinyMCE) | ✅ |
instruction | string | Instruction de retravail en langage naturel (ex. « utilise un ton plus dynamique, mets en avant les valeurs de l'entreprise »), max 500 car. | ✅ |
session_id | string | ID 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.
request_type=refine_section dans offer_generation_log.
SectionName, donc l'API rejette la requête en 422 si Bubble tente de l'affiner.
Offres d'emploi — options de ton
/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é | Source | Critère métier |
|---|---|---|
| 1 | Agence fille (X-Uid-Agence-Fille) | Même métier |
| 2 | Agence fille | N'importe quel métier |
| 3 | Agence mère (X-Uid-Agence-Mere) | Même métier |
| 4 | Agence mère | N'importe quel métier |
Sections récupérées : description · profil · responsabilites. Format TOON pour économie de tokens.
Candidats — flux synthèse
cv_data (JSON)
+ headers UID
(courte / longue)
Bedrock
CANDIDAT
à Bubble
Candidats — champs d'input
| Champ | Type | Description | Obligatoire |
|---|---|---|---|
cv_data | dict | Données du candidat en JSON (parsing CV ou fiche Bubble). Champs attendus : nom, titre, experience, formation, competences, langues | ✅ |
longueur | SyntheseLongueur | courte — résumé <1 000 car. · longue — synthèse <2 000 car. | ✅ |
source | SyntheseSource | cv_parsing (premier affichage, pas d'updated_at) · fiche_candidat (mise à jour, retourne updated_at) | — |
session_id | string | ID de session Bubble | — |
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.
| Champ | Type | Description | Obligatoire |
|---|---|---|---|
cv_data | dict | Mêmes données que /generate-synthese — source de vérité pour éviter toute invention | ✅ |
current_synthese | string | Version actuelle de la synthèse à affiner (HTML TinyMCE) | ✅ |
instruction | string | Instruction de retravail en langage naturel (ex. « Rends ça plus court », « Ton plus dynamique », « Infos transports »), max 500 car. | ✅ |
longueur | SyntheseLongueur | courte (<1 000 car.) · longue (<2 000 car.) | ✅ |
source | SyntheseSource | cv_parsing · fiche_candidat (défaut — un affinage est une mise à jour, updated_at renseigné) | — |
session_id | string | ID de session Bubble | — |
/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
SIRET / SIREN / nom
(OFFICIEL)
site + LinkedIn
(robots.txt)
(description, pages, tél.)
_ONLY
ENTREPRISE
+ summary
Entreprises — champs d'input
| Champ | Type | Description | Obligatoire |
|---|---|---|---|
siren | string | SIREN de l'entreprise (9 chiffres) | Au moins un des trois |
siret | string | SIRET de l'entreprise (14 chiffres) — les 9 premiers sont utilisés comme SIREN | |
raison_sociale | string | Nom de l'entreprise (recherche par texte si SIREN absent) | |
website_url | string | URL 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_existants | dict | Champs déjà renseignés dans Bubble — structure { "web": {...}, "identity": {...}, "recruiting": {...} }. Les champs non-null ici ne seront jamais écrasés (FILL_EMPTY_ONLY). | — |
session_id | string | ID de session Bubble pour traçabilité | — |
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
| Champ | Type valeur | Description |
|---|---|---|
website_url | string | URL du site corporate (ex : https://www.acme.fr) |
linkedin_company_url | string | URL de la page LinkedIn entreprise |
careers_page_url | string | URL de la page carrières / recrutement |
contact_page_url | string | URL de la page contact |
legal_notice_url | string | URL des mentions légales |
Bloc identity
| Champ | Type valeur | Description |
|---|---|---|
description | string | Description textuelle de l'entreprise (max 1 000 car.) |
logo_url | string | URL du logo |
phone_number | string | Standard téléphonique de l'entreprise |
last_known_revenue_amount | integer | Dernier CA connu en EUR (ex. 46394000000) |
last_known_revenue_year | integer | Année du CA (ex. 2023) |
Bloc recruiting
| Champ | Type valeur | Description |
|---|---|---|
active_job_ads_count | number | Nombre d'offres d'emploi actives (toujours null côté API — voir POST /offers) |
active_job_ads_search_url | string | URL de recherche d'offres (Indeed généré automatiquement si absent) |
sources | string[] | Plateformes de recrutement utilisées (toujours null côté API) |
top_qualifications | string | Toujours null côté API — voir POST /offers (endpoint dédié) pour les dernières offres et le top 5 des qualifications demandées |
"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.
"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
}
null (pas {"value": null, ...}). Cela signifie "ne pas mettre à jour ce champ dans Bubble".
Blocs et types de chaque champ
| Bloc | Champ | Type value | Source |
|---|---|---|---|
| officiel | |||
officiel | siret | string | OFFICIEL |
officiel | siren | string | OFFICIEL |
officiel | siege_adresse | string | OFFICIEL |
officiel | tranche_effectif | string | OFFICIEL |
officiel | forme_juridique | string | OFFICIEL |
officiel | code_naf | string | OFFICIEL |
officiel | dirigeant | string | OFFICIEL |
officiel | annee_creation | string | OFFICIEL |
| web | |||
web | website_url | string | PUBLIC_WEB |
web | linkedin_company_url | string | PUBLIC_WEB |
web | careers_page_url | string | PUBLIC_WEB |
web | contact_page_url | string | PUBLIC_WEB |
web | legal_notice_url | string | PUBLIC_WEB |
| identity | |||
identity | description | string (max 1 000 car.) | PUBLIC_WEB |
identity | logo_url | string | PUBLIC_WEB |
identity | phone_number | string | PUBLIC_WEB |
identity | last_known_revenue_amount | integer (EUR) | OFFICIEL |
identity | last_known_revenue_year | integer (ex. 2023) | OFFICIEL |
| recruiting | |||
recruiting | active_job_ads_count | integer — toujours null ici | ESTIMATED |
recruiting | active_job_ads_search_url | string (URL Indeed générée) | ESTIMATED |
recruiting | sources | string — toujours null ici | ESTIMATED |
recruiting | top_qualifications | string — toujours null ici, voir POST /offers | ESTIMATED |
| enrichment_summary | |||
enrichment_summary.fields_filled | integer | — | |
enrichment_summary.fields_needs_review | integer | — | |
enrichment_summary.status | string : succès | partiel | erreur | — | |
enrichment_summary.sources_used | array — voir ci-dessous | — | |
| tokens | |||
tokens.input_tokens · output_tokens · total_tokens | integer (0 si sans appel Bedrock) | — | |
tokens.cost_usd | float | — | |
model | string (ex. claude-sonnet-4-6) | — | |
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
| Source | Mode | Auth | Donné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.
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.
{
"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).
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
| Champ | Type | Description | Requis |
|---|---|---|---|
siren | string | SIREN (9 chiffres) | au moins un des trois |
siret | string | SIRET (14 chiffres) | identifiants |
raison_sociale | string | Nom de l'entreprise. Si absent, résolu via open-data depuis siret/siren | — |
ville | string | Ville pour affiner la recherche (optionnel) | — |
limit | integer | Nombre max d'offres, 1–20 (défaut 5) | — |
session_id | string | ID de session Bubble (traçabilité) | — |
Réponse
| Champ | Type | Description |
|---|---|---|
offers | object[] | 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. |
count | integer | Nombre d'offres retournées |
top_qualifications | string[] | 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_available | bool | false si le grounding n'était pas disponible/configuré (offres vides, aucune erreur) |
raison_sociale | string | Raison sociale résolue utilisée pour la recherche |
tokens | object | input_tokens, output_tokens, total_tokens (Gemini) ; cost_usd = 0 |
model | string | Modèle Gemini utilisé (ou none si recherche indisponible) |
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
type d'email + paramètres
+ headers UID
anti-discrimination
verrouillé
Bedrock
sortie LLM
email_ai_log
à Bubble
/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_type | Intention |
|---|---|---|
CANDIDAT | RELANCE_CANDIDAT | Suivi après envoi d'offre |
DEMANDE_DISPO | Disponibilité pour mission | |
PROPOSITION_MISSION | Offre ciblée candidat | |
PRESENTATION_CANDIDAT | Détails du candidat | |
CONFIRMATION_MISSION | Détails de la mission | |
AUTRE | Message libre | |
CONTACT | PRISE_CONTACT | Premier contact avec le prospect |
RELANCE_COMMERCIALE | Suivi après échange resté sans réponse | |
COMPTE_RENDU_RDV | Synthèse et prochaines étapes | |
PROPOSITION_COLLABORATION | Présentation de l'offre de service | |
AUTRE | Message libre | |
PROPOSITION_ACTIVE | PROPOSITION_CANDIDATS | Valoriser un ou plusieurs profils |
RELANCE_PROPOSITION | Suivi d'une proposition envoyée | |
DISPONIBILITE_PROFIL | Signaler un candidat disponible | |
AUTRE | Message libre | |
RECRUTEMENT | REPONSE_COMMANDE | Proposer des candidats pour le poste |
PRESENTATION_CANDIDAT | Détails du candidat retenu | |
AUTRE | Message libre |
Emails — paramètres communs (tous types)
| Champ | Type | Valeurs / Description | Obligatoire |
|---|---|---|---|
context | string | 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_type | string | 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 |
✅ |
tone | string | Professionnel · Chaleureux · Dynamique · Formel · Bienveillant — défaut Professionnel si omis (Att_04), modifiable par l'utilisateur |
— |
recipients[].type | string | CLIENT — client existant · PROSPECT — pas encore client · CANDIDAT — candidat en base |
✅ |
recipients[].company_name | string | Nom de l'entreprise ou agence destinataire | ✅ |
recipients[].contact_name | string | Nom du contact (optionnel, utilisé pour la personnalisation) | — |
recipients[].contact_role | string | Fonction du contact (ex : Responsable RH) | — |
recipients[].vouvoiement | bool | true = vouvoiement (défaut) · false = tutoiement |
— |
variables | dict | 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_id | string | ID de session Bubble pour traçabilité | — |
X-Uid-User · X-Uid-Agence-Mere · X-Uid-Agence-FilleHeader 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).
| Champ | Type | Description | Obligatoire |
|---|---|---|---|
candidates[].uid | string | UID Bubble du candidat — tracé dans candidate_ids du log | — |
candidates[].first_name | string | Prénom du candidat (utilisé via le placeholder CANDIDATE_FIRSTNAME) | — |
candidates[].last_name | string | Nom du candidat | — |
candidates[].profile_title | string | Intitulé du profil (ex : Soudeur TIG N2) | ✅ |
candidates[].job_main | string | Métier principal (champ jobMainLowercase Bubble) | — |
candidates[].years_exp_or_level | string | Expérience ou niveau (ex : 4 ans, Confirmé) | — |
candidates[].skills[] | string[] | Compétences clés | — |
candidates[].certifications[] | string[] | Certifications et habilitations | — |
candidates[].availability | string | Disponibilité (ex : Immédiate, 01/06/2026) | — |
candidates[].disponibilite_os | string | Disponibilité opérationnelle (champ Bubble complémentaire) | — |
candidates[].mobility_area | string | Zone de mobilité (ex : Grand Lyon) | — |
candidates[].distance_acceptee | string | Distance de déplacement acceptée | — |
candidates[].grand_deplacement | bool | true si le candidat accepte les grands déplacements | — |
candidates[].private_note | string | Note 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_title | string | Intitulé du poste | — |
job.job_location | string | Lieu de la mission | — |
job.job_contract | string | Type de contrat (CDI, Intérim, CDD…) | — |
job.job_text | string | Descriptif complet du poste. Si absent, le LLM reste générique sans inventer. | — |
context_text | string | Contexte libre complémentaire (étape 3, facultative — saisi ou issu des suggestions cliquables, max 800 car.) | — |
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 :
| Champ | Type | Description | Obligatoire |
|---|---|---|---|
current_subject | string | Sujet de la version à affiner | ✅ |
current_body_html | string | Corps HTML de la version à affiner | ✅ |
current_body_text | string | Corps texte brut de la version à affiner | ✅ |
instruction | string | Instruction de modification en langage naturel (ex. « raccourcis », « ajoute les détails de la mission »), max 500 car. | ✅ |
/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é.
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 pertinent3. La réponse contient
#clé# bruts — Bubble fait ses propres remplacements4.
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)
"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 :
syntheseCandidatsen camelCase est reconnu commesynthese_candidats. Sivariablesn'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_namesont 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.
/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éponse | Type | Description |
|---|---|---|
subject | string | null | Sujet de l'email généré. null si safety.status = BLOCKED |
body_html | string | null | Corps HTML simple (<p>, <br>, <ul>, <li>). null si BLOCKED |
body_text | string | null | Corps texte brut — garanti sans balise ni entité HTML : le balisage éventuellement laissé par le LLM (<br>, <li>, <p>, …) est converti en sauts de ligne / tirets côté serveur. null si BLOCKED |
placeholders_used | string[] | 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.status | string | OK · BLOCKED · NEEDS_REVIEW |
safety.reasons | string[] | Raisons du blocage ou des points à valider |
tokens | object | input_tokens, output_tokens, total_tokens, cost_usd |
model | string | Identifiant du modèle utilisé |
subject et body_* remplis. Appliquer la politique FILL_EMPTY_ONLY avant d'écraser un brouillon existant.subject et body_* sont null. Afficher les reasons à l'utilisateur.reasons.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.
| Champ | Géré par | Description |
|---|---|---|
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 :
X-Uid-Candidat sur /candidates/generate-synthese et /candidates/refine-syntheseX-Uid-Client sur /entreprises/enrichFeedback
Deux types de feedback, disponibles en version globale (toutes fonctionnalités) ou en version rattachée aux offres uniquement.
| Endpoint | Type | Champs |
|---|---|---|
/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 |