Marvin · Developers

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 :

  1. Flux d'offres (public) — le site pull un flux XML des offres ouvertes et les affiche avec son propre template. Aucune clé requise.
  2. 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).
  3. 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}.xml

Public, 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-8
  • Cache-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.
BaliseDescription
titleIntitulé de l'offre
dateDate de publication
jobid⚠️ Identifiant de l'offre — à reporter dans l'URL de candidature
urlPage de l'offre (lien « Voir l'offre » / « Postuler »)
companyNom de l'entreprise
cityVille
stateRégion / département
countryCode pays ISO (FR…)
descriptionDescription de l'offre (HTML)
salaryRémunération
jobtypeType de contrat (permanent contract…)
remotetypeTélétravail (Fully remote / Hybrid remote / No remote)
champs persoChaque 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}/apply

où {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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

La 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.

ChampTypeRequisDescription
firstNamestringouiPrénom
lastNamestringouiNom
emailstringouiEmail du candidat
phoneNumberstringnonTéléphone
linkedinstringnonURL du profil LinkedIn
motivationTextstringnonMessage / lettre de motivation
resumefilenonCV (resume ou cv) — multipart requis
resumeUrlstringnonAlternative en JSON : URL publique du CV, téléchargé par Marvin
utm_source, utm_medium, utm_campaign, utm_term, utm_contentstringnonProvenance (formulaire, campagne) — visible sur la candidature
champs perso—nonChamps 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.

StatutcodeCas
201—Candidature reçue — CV parsé, profil rattaché ou créé
400validation_errorChamp invalide (e-mail malformé, type de fichier refusé…)
401unauthorizedClé absente, malformée ou révoquée
403insufficient_scopeScope manquant sur la clé
404job_not_foundOffre inconnue, ou slug d'un autre client que la clé
410job_removedOffre fermée, dépubliée ou supprimée
422missing_required_fieldChamp requis manquant — error.field nomme le champ
429rate_limitedTrop 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}/apply

Tout 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) ou application/json, mêmes noms qu'au tableau ci-dessus (firstName, lastName, email, phoneNumber, linkedin, motivationText, resume/cv).
  • Mêmes réponses — 201 reçue · 400 champ invalide · 401 clé absente/invalide · 403 scope manquant · 404 slug d'un autre client que la clé · 422 champ requis manquant. (Pas de 410 ici : 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-requests

Authentification

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)

ChampTypeRequisDescription
kindstringouicompany (entreprise) ou individual (particulier)
firstNamestringouiPrénom du demandeur (100 caractères)
lastNamestringouiNom du demandeur (100 caractères)
emailstringouiE-mail du demandeur — clé de rapprochement avec une fiche existante
companyNamestringsi kind=companyNom de l'entreprise, 200 caractères (retrouvée par nom, sinon créée)
companyWebsitestringnonSite web — https:// ajouté si absent ; ignoré (jamais bloquant) s'il reste invalide
phonestringnonTéléphone — national FR (06 12 34 56 78) ou E.164 (+33612345678) ; ignoré (jamais bloquant) si non reconnu
jobTitlestringnonFonction du demandeur (150 caractères)
topicstringnonObjet de la demande — ex. Audit RH, Bilan de compétences (200 caractères)
messagestringnonMessage du visiteur (5 000 caractères max)
formNamestringnonNom du formulaire d'origine — ex. contact-entreprise (100 caractères)
utm_source, utm_medium, utm_campaign, utm_term, utm_contentstringnonProvenance (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).

StatutcodeCas
201—Demande reçue
400validation_errorChamp invalide ou clé inconnue — error.field nomme le champ
401unauthorizedClé absente, malformée ou révoquée
403insufficient_scopeLa clé n'a pas le droit « demandes de contact »
404not_foundClient (slug) introuvable ou ne correspondant pas à la clé
422missing_required_fieldcompanyName 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 » ; formName et les utm_* 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 201 avec 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 :

  1. Utilisez des données clairement identifiables — e-mails de type test+<date>@votre-domaine, formName ou utm_source=test.
  2. Vérifiez côté Marvin que la candidature / le contact / l'entreprise apparaissent avec la provenance attendue.
  3. 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-Key sur 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.

On this page