Site web — diffuser vos offres et recevoir les candidatures
Afficher les offres Marvin sur le site du client via un flux XML, renvoyer les candidatures du site dans Marvin via l'API, et faire remonter les demandes de contact (entreprises, particuliers) dans le CRM
Le canal « Site web » permet à un client Marvin de diffuser ses offres sur son propre site et d'y récupérer les candidatures directement dans Marvin. Cette page s'adresse au webmaster / intégrateur qui câble le site du client.
Vue d'ensemble
Trois briques indépendantes :
- Flux d'offres (public) — le site pull un flux XML des offres ouvertes et les affiche avec son propre template. Aucune clé requise.
- Envoi des candidatures (authentifié) — quand un visiteur postule sur le site, celui-ci POST la candidature à Marvin via une API protégée par une clé. La candidature remonte dans Marvin, rattachée à l'offre concernée — ou sans offre (candidature spontanée).
- Demandes de contact (authentifié) — un formulaire « entreprise » (audit RH, recrutement, coaching…) ou « particulier » (bilan, reconversion…) crée un contact et son entreprise dans le CRM Marvin, sans passer par le suivi de recrutement. Voir Demandes de contact.
Les trois se câblent séparément : on peut très bien n'afficher que les offres, puis brancher l'apply ou les demandes de contact dans un second temps.
- Base URL :
https://api.marvins.ai - Authentification (apply et demandes de contact) : header
X-Marvin-Public-Key(voir Authentification) - Pas d'environnement de test : il n'existe pas de clé « sandbox ». Voir Tester votre intégration.
Consommer le flux d'offres
GET https://api.marvins.ai/public/feeds/site-web/{slug}.xmlPublic, par slug — aucune clé. Le webmaster pull cette URL à intervalle régulier (au build, ou via un cron) et rend les offres avec son propre template.
Le {slug} et l'URL exacte sont fournis dans Marvin :
Configuration → Multidiffusion → carte « Site web » → bouton Configurer → champ « URL du flux à transmettre à votre webmaster ».
Format
Content-Type: text/xml; charset=utf-8Cache-Control: public, max-age=300(cache 5 min — vous pouvez pull aussi souvent que vous voulez)- Racine
<source>, un<job>par offre, valeurs en CDATA.
| Balise | Description |
|---|---|
title | Intitulé de l'offre |
date | Date de publication |
jobid | ⚠️ Identifiant de l'offre — à reporter dans l'URL de candidature |
url | Page de l'offre (lien « Voir l'offre » / « Postuler ») |
company | Nom de l'entreprise |
city | Ville |
state | Région / département |
country | Code pays ISO (FR…) |
description | Description de l'offre (HTML) |
salary | Rémunération |
jobtype | Type de contrat (permanent contract…) |
remotetype | Télétravail (Fully remote / Hybrid remote / No remote) |
| champs perso | Chaque champ personnalisé de l'offre, en balise au nom slugifié |
Exemple
<?xml version="1.0" encoding="UTF-8"?>
<source>
<job>
<title><![CDATA[Développeur React Senior (H/F)]]></title>
<date><![CDATA[2026-04-01]]></date>
<jobid><![CDATA[01919c41-7a2b-7c3d-8e4f-1a2b3c4d5e6f]]></jobid>
<url><![CDATA[https://www.acme-corp.com/carrieres/dev-react-senior]]></url>
<company><![CDATA[Acme Corp]]></company>
<city><![CDATA[Paris]]></city>
<state><![CDATA[Île-de-France]]></state>
<country><![CDATA[FR]]></country>
<description><![CDATA[<p>Nous cherchons un dev <b>React</b>…</p>]]></description>
<salary><![CDATA[55K - 75K]]></salary>
<jobtype><![CDATA[permanent contract]]></jobtype>
<remotetype><![CDATA[Hybrid remote]]></remotetype>
<niveau-d-etudes><![CDATA[Bac +5]]></niveau-d-etudes>
</job>
<!-- … un <job> par offre ouverte -->
</source>Conservez la valeur de jobid : c'est elle que vous passerez à l'API de candidature ({ID_OFFRE} ) pour rattacher chaque candidature à la bonne offre.
Envoyer les candidatures
Quand un visiteur soumet le formulaire de candidature de votre site, postez-le à :
POST https://api.marvins.ai/v1/public/careers/{slug}/jobs/{ID_OFFRE}/applyoù {ID_OFFRE} est le <jobid> lu dans le flux.
Authentification
Cet endpoint requiert une clé d'API candidatures dans le header :
X-Marvin-Public-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxLa clé se génère dans Marvin :
Configuration → Multidiffusion → Site web → Configurer → « Générer une clé d'API candidatures ».
La clé pk_live_… est un secret côté serveur. Ne l'exposez jamais dans le code exécuté
par le navigateur (le POST de candidature doit partir de votre back-end). Elle n'est affichée
qu'une seule fois à la génération — copiez-la immédiatement.
Corps de la requête
Le corps peut être envoyé en multipart/form-data (recommandé, permet le CV)
ou en application/json (sans pièce jointe). Les noms de champs sont
identiques à Marvin V1.
| Champ | Type | Requis | Description |
|---|---|---|---|
firstName | string | oui | Prénom |
lastName | string | oui | Nom |
email | string | oui | Email du candidat |
phoneNumber | string | non | Téléphone |
linkedin | string | non | URL du profil LinkedIn |
motivationText | string | non | Message / lettre de motivation |
resume | file | non | CV (resume ou cv) — multipart requis |
resumeUrl | string | non | Alternative en JSON : URL publique du CV, téléchargé par Marvin |
utm_source, utm_medium, utm_campaign, utm_term, utm_content | string | non | Provenance (formulaire, campagne) — visible sur la candidature |
| champs perso | — | non | Champs personnalisés de l'offre |
Champs obligatoires : firstName, lastName, email — plus tout champ marqué
« requis » dans le formulaire de candidature configuré par le client dans Marvin
(Paramètres → Carrières). Un champ requis manquant renvoie 422 avec le nom du
champ (error.field).
CV : fichier binaire en multipart/form-data, champ resume (ou cv). Formats
acceptés : PDF, DOC, DOCX, JPEG, PNG, WEBP, GIF. Taille maximale : 10 Mo. Un
type non supporté renvoie 400.
Provenance : toute candidature envoyée par cette API est marquée « Site web »
côté Marvin. Pour distinguer plusieurs formulaires ou campagnes, passez les champs
utm_* (ex. utm_medium=formulaire-spontanee).
Doublons : une candidature reçue pour une même offre avec un e-mail déjà
candidat (candidature encore active) n'est pas refusée : elle est acceptée (201)
et mise en quarantaine côté Marvin pour arbitrage par le recruteur. Aucun doublon
de profil n'est créé.
Exemple — multipart avec CV
curl -X POST \
"https://api.marvins.ai/v1/public/careers/acme-corp/jobs/01919c41-7a2b-7c3d-8e4f-1a2b3c4d5e6f/apply" \
-H "X-Marvin-Public-Key: pk_live_…" \
-F "firstName=Linda" \
-F "lastName=Kedin" \
-F "email=linda.kedin@example.com" \
-F "phoneNumber=+33612345678" \
-F "linkedin=https://www.linkedin.com/in/linda-kedin" \
-F "motivationText=Bonjour, je suis très intéressée par ce poste…" \
-F "resume=@/chemin/vers/cv-linda-kedin.pdf"Exemple — JSON sans CV
curl -X POST \
"https://api.marvins.ai/v1/public/careers/acme-corp/jobs/01919c41-7a2b-7c3d-8e4f-1a2b3c4d5e6f/apply" \
-H "X-Marvin-Public-Key: pk_live_…" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Linda",
"lastName": "Kedin",
"email": "linda.kedin@example.com",
"phoneNumber": "+33612345678",
"linkedin": "https://www.linkedin.com/in/linda-kedin",
"motivationText": "Bonjour, je suis très intéressée par ce poste…"
}'Réponses
Succès — 201 avec l'identifiant de la candidature créée :
{ "data": { "id": "0192a3b4-…" } }Erreurs — corps toujours de la forme { "error": { "code": "…", "message": "…" } }
(+ field quand un champ est en cause). Le code est stable : c'est lui qu'il faut
tester pour afficher un message fiable au visiteur.
| Statut | code | Cas |
|---|---|---|
201 | — | Candidature reçue — CV parsé, profil rattaché ou créé |
400 | validation_error | Champ invalide (e-mail malformé, type de fichier refusé…) |
401 | unauthorized | Clé absente, malformée ou révoquée |
403 | insufficient_scope | Scope manquant sur la clé |
404 | job_not_found | Offre inconnue, ou slug d'un autre client que la clé |
410 | job_removed | Offre fermée, dépubliée ou supprimée |
422 | missing_required_field | Champ requis manquant — error.field nomme le champ |
429 | rate_limited | Trop de requêtes (30 par minute et par clé) |
Pour le détail des schémas et tester les requêtes, voir la Référence API — Careers (Public).
Candidature spontanée (sans offre)
Un visiteur peut postuler sans être rattaché à une offre précise (candidature
libre). C'est le même endpoint que ci-dessus, sans le {ID_OFFRE} :
POST https://api.marvins.ai/v1/public/careers/{slug}/applyTout le reste est identique à la candidature sur offre :
- Même clé — header
X-Marvin-Public-Key: pk_live_…(le même secret, scope candidatures). Rien de nouveau à générer. - Mêmes champs —
multipart/form-data(avec CV) ouapplication/json, mêmes noms qu'au tableau ci-dessus (firstName,lastName,email,phoneNumber,linkedin,motivationText,resume/cv). - Mêmes réponses —
201reçue ·400champ invalide ·401clé absente/invalide ·403scope manquant ·404slugd'un autre client que la clé ·422champ requis manquant. (Pas de410ici : aucune offre à retirer.)
Côté Marvin, la candidature arrive sans offre rattachée (le CV est parsé, le profil rattaché ou créé), et le recruteur la traite comme une candidature spontanée.
Exemple — multipart avec CV
curl -X POST \
"https://api.marvins.ai/v1/public/careers/acme-corp/apply" \
-H "X-Marvin-Public-Key: pk_live_…" \
-F "firstName=Marie" \
-F "lastName=Dupont" \
-F "email=marie.dupont@example.com" \
-F "phoneNumber=+33612345678" \
-F "motivationText=Bonjour, je vous soumets ma candidature…" \
-F "resume=@/chemin/vers/cv-marie-dupont.pdf"Cet endpoint est toujours disponible dès que la clé est valide : il ne dépend pas du réglage « candidatures spontanées » de la page carrière hébergée par Marvin (ce réglage ne pilote que le bouton de cette page). C'est donc à vous de décider si votre site expose, ou non, un formulaire de candidature spontanée.
Demandes de contact (entreprises et particuliers)
Votre site porte souvent d'autres formulaires que la candidature : une demande d'entreprise (audit RH, recrutement, coaching, devis…) ou une demande de particulier (bilan de compétences, reconversion…). Ces demandes ne sont pas des candidatures : elles remontent dans le CRM Marvin — l'entreprise est retrouvée ou créée, la personne devient un contact (jamais un candidat), et la demande apparaît dans l'historique du contact et de l'entreprise, avec une notification aux recruteurs. Rien n'entre dans le suivi de recrutement.
POST https://api.marvins.ai/v1/public/crm/{slug}/contact-requestsAuthentification
Même header X-Marvin-Public-Key: pk_live_… que pour les candidatures. La clé doit
porter le droit « demandes de contact » (scope public:contacts:write) : toute
clé générée depuis Configuration → Multidiffusion → Site web → Configurer le
porte. Une clé plus ancienne qui ne l'a pas est signalée dans cette même liste —
générez-en une nouvelle (et révoquez l'ancienne une fois le site basculé).
Corps de la requête (application/json)
| Champ | Type | Requis | Description |
|---|---|---|---|
kind | string | oui | company (entreprise) ou individual (particulier) |
firstName | string | oui | Prénom du demandeur (100 caractères) |
lastName | string | oui | Nom du demandeur (100 caractères) |
email | string | oui | E-mail du demandeur — clé de rapprochement avec une fiche existante |
companyName | string | si kind=company | Nom de l'entreprise, 200 caractères (retrouvée par nom, sinon créée) |
companyWebsite | string | non | Site web — https:// ajouté si absent ; ignoré (jamais bloquant) s'il reste invalide |
phone | string | non | Téléphone — national FR (06 12 34 56 78) ou E.164 (+33612345678) ; ignoré (jamais bloquant) si non reconnu |
jobTitle | string | non | Fonction du demandeur (150 caractères) |
topic | string | non | Objet de la demande — ex. Audit RH, Bilan de compétences (200 caractères) |
message | string | non | Message du visiteur (5 000 caractères max) |
formName | string | non | Nom du formulaire d'origine — ex. contact-entreprise (100 caractères) |
utm_source, utm_medium, utm_campaign, utm_term, utm_content | string | non | Provenance (200 caractères chacun) |
Toute autre clé est refusée (400 validation_error, error.field = la clé
inconnue). Pas de pièce jointe sur ce formulaire.
Exemple — demande d'entreprise
curl -X POST \
"https://api.marvins.ai/v1/public/crm/acme-corp/contact-requests" \
-H "X-Marvin-Public-Key: pk_live_…" \
-H "Content-Type: application/json" \
-d '{
"kind": "company",
"firstName": "Marie",
"lastName": "Dupont",
"email": "marie.dupont@entreprise.example",
"phone": "+33612345678",
"jobTitle": "DRH",
"companyName": "Entreprise Exemple",
"companyWebsite": "https://www.entreprise.example",
"topic": "Audit RH",
"message": "Nous souhaitons un audit de nos process de recrutement.",
"formName": "contact-entreprise",
"utm_source": "site",
"utm_campaign": "landing-2026"
}'Exemple — demande de particulier
curl -X POST \
"https://api.marvins.ai/v1/public/crm/acme-corp/contact-requests" \
-H "X-Marvin-Public-Key: pk_live_…" \
-H "Content-Type: application/json" \
-d '{
"kind": "individual",
"firstName": "Paul",
"lastName": "Martin",
"email": "paul.martin@example.com",
"topic": "Bilan de compétences",
"message": "Je souhaite être rappelé pour un bilan.",
"formName": "contact-particulier"
}'Réponses
Succès — 201 :
{
"data": {
"id": "0192a3b4-…",
"personId": "0192a3b5-…",
"companyId": "0192a3b6-…"
}
}companyId est null pour un particulier, ou quand plusieurs entreprises du client
portent déjà ce nom (Marvin ne devine pas : le contact est créé sans entreprise, le
nom saisi reste sur la demande et le recruteur tranche).
| Statut | code | Cas |
|---|---|---|
201 | — | Demande reçue |
400 | validation_error | Champ invalide ou clé inconnue — error.field nomme le champ |
401 | unauthorized | Clé absente, malformée ou révoquée |
403 | insufficient_scope | La clé n'a pas le droit « demandes de contact » |
404 | not_found | Client (slug) introuvable ou ne correspondant pas à la clé |
422 | missing_required_field | companyName manquant pour une demande d'entreprise (error.field) |
429 | — | Trop de requêtes (30 par minute et par clé) |
Ce qui se passe côté Marvin
- Entreprise (
kind=company) : retrouvée si une entreprise du client porte le même nom (comparaison insensible à la casse et aux accents), sinon créée avec le statut par défaut du client. - Personne : retrouvée par e-mail dans la base du client, sinon créée comme contact (jamais comme candidat). Si l'e-mail correspond à un candidat déjà connu, la fiche existante devient aussi un contact — aucun doublon.
- Provenance : toujours « Site web » ;
formNameet lesutm_*permettent de distinguer vos formulaires. - Rejeux : une même demande soumise deux fois en moins de 10 minutes (même
e-mail, même nature, même objet, même entreprise, même message, même formulaire)
renvoie
201avec les identifiants de la première — vous pouvez réessayer un envoi (ou subir un double clic) sans créer de doublon. Deux envois simultanés sont sérialisés côté Marvin : une seule fiche, une seule demande. - E-mail partagé : si l'e-mail est déjà porté par plusieurs fiches du client
(boîte générique
contact@…), Marvin ne devine pas et crée un contact distinct. - Notification : les responsables de l'entreprise dans Marvin, sinon les administrateurs du client. Le nom affiché est celui saisi dans le formulaire.
- Ce qui est conservé : la demande garde l'identité saisie (nom, e-mail, téléphone, fonction), l'objet, le message et la provenance, rattachée au contact et à l'entreprise ; elle suit les fiches en cas de fusion, et disparaît avec la fiche contact lors de sa suppression définitive (la suppression de l'entreprise seule la laisse rattachée au contact).
Pour le détail des schémas et tester les requêtes, voir la Référence API — CRM (Public).
Tester votre intégration
Il n'existe pas d'environnement de test ni de clé « sandbox » : les clés pk_live_…
écrivent dans l'espace réel du client. Pour valider sans polluer :
- Utilisez des données clairement identifiables — e-mails de type
test+<date>@votre-domaine,formNameouutm_source=test. - Vérifiez côté Marvin que la candidature / le contact / l'entreprise apparaissent avec la provenance attendue.
- Supprimez ensuite ces fiches de test depuis Marvin (le client ou son interlocuteur Marvin peut le faire en quelques clics).
Les rejeux sont sans risque : un doublon de candidature part en quarantaine, une demande de contact identique n'est pas recréée.
Checklist de bascule (client migré depuis Marvin V1)
Si le site consommait déjà le canal « Site web » de Marvin V1, l'intégration reste quasi identique — seules les URLs changent :
- Remplacer l'ancienne URL de flux V1 par la nouvelle URL
…/public/feeds/site-web/{slug}.xml. - Repointer le POST de candidature sur la nouvelle URL
…/v1/public/careers/{slug}/jobs/{ID_OFFRE}/apply. - Ajouter le header
X-Marvin-Public-Keysur le POST de candidature (nouveau en V2). - Vérifier que les noms de champs restent identiques à V1
(
firstName,lastName,email,resume…) — aucun renommage à prévoir.