Marvin Recruiter API

CRM — envoyer vos entreprises et vos contacts à Marvin

Créer et mettre à jour dans Marvin les entreprises et les contacts gérés dans votre CRM, depuis vos serveurs, avec les identifiants de votre CRM

Cette page s'adresse à la DSI ou au prestataire qui branche le CRM d'un cabinet sur Marvin. Le flux va de votre CRM vers Marvin, et votre CRM fait foi : une modification se fait dans le CRM, puis vous la renvoyez à Marvin.

Vue d'ensemble

  • Vous envoyez une entreprise et ses contacts avec les identifiants qu'ils ont dans votre CRM.
  • Au premier envoi, Marvin crée l'entreprise, ou la rapproche d'une fiche qui existe déjà dans le cabinet (même SIREN, puis même SIRET, puis même nom).
  • Aux envois suivants, Marvin met à jour la même fiche. Seuls les champs envoyés changent.
  • Rejouer un envoi ne crée jamais de doublon : vous pouvez réessayer sans risque.
  • Marvin ne supprime rien. Un contact absent d'un envoi reste rattaché. Une fiche supprimée dans Marvin par le cabinet n'est jamais recréée.

C'est vous qui décidez quand envoyer : par exemple quand une propriété de votre CRM passe à « oui », ou quand une entreprise change.

  • Base URL : https://api.marvins.ai
  • Authentification : header X-Marvin-Public-Key (voir Authentification), avec le droit public:companies:write.
  • Deux adresses : PATCH …/companies/{externalId} envoie une entreprise et ses contacts ; GET …/fields lit le référentiel du compte (statuts, tranches d'effectif, champs personnalisés) pour construire votre correspondance de champs.

Obtenir la clé

Un gestionnaire du cabinet génère la clé dans Marvin, depuis Paramètres → Intégrations → CRM. La clé n'est affichée qu'une fois. C'est un secret : gardez-la côté serveur, ne l'exposez jamais dans un navigateur, et ne l'envoyez pas par un mail en copie à plusieurs personnes. Le gestionnaire peut la révoquer à tout moment depuis la même page.

La même page donne l'adresse exacte à appeler, avec l'identifiant du cabinet (slug).

Restreindre la clé à vos adresses IP

C'est facultatif. Le gestionnaire peut limiter la clé aux adresses de vos serveurs, sur la même page (Paramètres → Intégrations → CRM), à la création de la clé ou plus tard avec Restreindre par IP. Transmettez-lui les adresses d'où partiront vos appels.

  • Une entrée est une adresse IPv4 (203.0.113.10), une adresse IPv6 (2001:db8::1) ou une plage (203.0.113.0/24, 2001:db8::/32). 20 entrées au plus par clé. Une plage /0 (0.0.0.0/0, ::/0) couvre toutes les adresses : elle est refusée.
  • Sans liste, la clé est acceptée depuis toutes les adresses.
  • Avec une liste, un appel venu d'une autre adresse est refusé avec 403 ip_not_allowed, sur l'envoi des entreprises comme sur la lecture du référentiel. Rien n'est lu ni écrit. La réponse donne l'adresse vue par Marvin (error.ip) : c'est celle à ajouter à la liste.
  • Marvin compare l'adresse publique d'où part l'appel. Derrière un proxy sortant ou une passerelle NAT, déclarez l'adresse de sortie de ce proxy. Un en-tête X-Forwarded-For ajouté par l'appelant n'est pas pris en compte.

Lire le référentiel de votre compte

GET https://api.marvins.ai/v1/public/crm/{slug}/fields

La même clé et la même restriction par IP que l'envoi des entreprises. Cette adresse rend la configuration du compte, pour écrire votre correspondance de champs sans nous demander les clés. Elle ne lit aucune fiche.

curl "https://api.marvins.ai/v1/public/crm/acme-rh/fields" \
  -H "X-Marvin-Public-Key: pk_live_…"

Réponse 200 :

{
  "data": {
    "sizes": [
      "0-1",
      "2-10",
      "11-50",
      "51-200",
      "201-500",
      "501-1000",
      "1001-5000",
      "5001-10000",
      "10001+"
    ],
    "companyStatuses": [
      { "key": "prospect", "label": "Prospect", "category": "interested" },
      { "key": "client", "label": "Client", "category": "won" }
    ],
    "companyCustomFields": [
      {
        "key": "secteur",
        "label": "Secteur",
        "type": "select",
        "options": ["Industrie", "Services"]
      },
      { "key": "date_contrat", "label": "Date du contrat-cadre", "type": "date" }
    ],
    "contactCustomFields": [
      { "key": "role", "label": "Rôle", "type": "select", "options": ["RH", "Comptable"] }
    ]
  }
}
ChampContenu
sizesValeurs acceptées par size
companyStatusesStatuts entreprise du compte : key (à envoyer dans status), label, category
companyCustomFieldsChamps personnalisés de l'entreprise : key, label, type, et options pour les champs à choix
contactCustomFieldsChamps personnalisés ouverts aux contacts, même forme. Les champs réservés aux candidats ne sont pas rendus

category vaut interested, in_progress, won ou lost. type vaut text, textarea, number, date, boolean, select, multiselect, url, email ou marker. options n'est rendu que pour select, multiselect et marker : c'est toujours un tableau (vide si le champ n'a aucun choix).

Le cabinet peut modifier ses statuts et ses champs dans Marvin à tout moment. Relisez le référentiel avant une reprise de l'existant, et quand un envoi est refusé avec unknown_status ou une clé de champ inconnue.

Créer ou mettre à jour une entreprise

PATCH https://api.marvins.ai/v1/public/crm/{slug}/companies/{externalId}

externalId est l'identifiant de l'entreprise dans votre CRM. Il accepte lettres, chiffres et . _ : @ -, 200 caractères au plus.

Corps de la requête (application/json)

Tous les champs sont facultatifs, sauf name quand l'entreprise est créée.

ChampTypeDescription
namestringNom de l'entreprise, 200 caractères. Obligatoire pour créer l'entreprise
sirenstring | null9 chiffres (espaces tolérés)
siretstring | null14 chiffres (espaces tolérés)
websitestring | nullSite web. https:// est ajouté s'il manque
linkedinUrlstring | nullPage LinkedIn de l'entreprise (/company/…)
sizestring | nullTranche d'effectif : 0-1, 2-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, 10001+
revenueinteger | nullChiffre d'affaires en euros, de 0 à 2 147 483 647
addressstring | nullAdresse postale en texte, 500 caractères
statusstringClé d'un statut entreprise du compte (companyStatuses du référentiel). Voir Statut
signedAtstring | nullDate de signature du contrat : AAAA-MM-JJ ou date-heure ISO 8601. Voir Statut
citystring | nullVille, 100 caractères : renseigne la localisation de l'entreprise. Voir Localisation
postalCodestring | nullCode postal, 20 caractères. Départage les villes homonymes, avec city. La mention CEDEX est ignorée
countrystring | nullPays, code ISO à 2 lettres (FR, BE…), avec city. Absent : la France est privilégiée
customFieldsobjectChamps personnalisés de l'entreprise, 100 au plus. Voir Champs personnalisés
contactsarrayContacts de l'entreprise, 50 au plus (voir ci-dessous)

Chaque contact :

ChampTypeDescription
externalIdstringObligatoire. Identifiant du contact dans votre CRM, unique dans l'envoi
firstNamestringPrénom, 100 caractères. Obligatoire pour créer le contact
lastNamestringNom, 100 caractères. Obligatoire pour créer le contact
emailstring | nullE-mail. Au premier envoi, rapproche le contact d'une fiche qui existe déjà avec cette adresse
phonestring | nullTéléphone, au format national français ou international
jobTitlestring | nullFonction, 150 caractères
customFieldsobjectChamps personnalisés ouverts aux contacts (contactCustomFields du référentiel), 100 au plus

Règles d'écriture de l'entreprise :

  • Un champ absent ne change pas. Envoyez seulement ce qui a changé si vous le souhaitez.
  • Une valeur envoyée remplace celle de Marvin.
  • null (ou une chaîne vide) vide le champ. Le nom ne peut pas être vidé.
  • Toute autre clé est refusée (400 validation_error), pour vous signaler une faute de frappe plutôt que d'ignorer le champ.

Règles d'écriture des contacts :

  • Un contact créé par votre CRM suit les mêmes règles que l'entreprise : votre CRM fait foi. Quand l'e-mail ou le téléphone change, l'ancienne valeur est gardée en secondaire.
  • Un contact rapproché d'une fiche qui existait déjà dans Marvin (même e-mail) est seulement complété : les champs vides sont remplis, rien n'est écrasé. Un nouvel e-mail ou téléphone est ajouté en secondaire.
  • L'e-mail principal d'un contact qui a un accès à l'espace personnel Marvin ne change jamais par cette API : la nouvelle adresse est ajoutée en secondaire.
  • Si le cabinet fusionne dans Marvin un contact créé par votre CRM avec une autre fiche, ou s'il en fait un candidat, votre CRM ne fait plus que compléter cette fiche.

Statut et date de signature

  • status prend la clé exacte d'un statut entreprise du compte, telle que rendue par GET …/fields (companyStatuses[].key), pas son libellé.
  • Une clé inconnue est refusée (422 unknown_status, field: status) : Marvin ne la remplace jamais par un statut par défaut. Le statut ne se vide pas : null et la chaîne vide sont refusés (400 validation_error).
  • Sans signedAt, le premier passage à un statut de catégorie won date la signature de l'entreprise du jour de l'envoi, y compris quand elle est créée directement avec ce statut.
  • signedAt envoyé l'emporte toujours : c'est la date de signature de l'entreprise, null la vide. Format : une date AAAA-MM-JJ ("2021-03-15") ou une date-heure ISO 8601 avec fuseau ("2021-03-15T10:00:00+01:00"). Une date qui n'existe pas est refusée (400 validation_error, field: signedAt).
  • Reprise des entreprises déjà sous contrat : envoyez signedAt avec la date de signature de leur contrat. Sans elle, elles seraient toutes datées du jour de la reprise.
  • Un statut reçu de votre CRM ne déclenche aucune automatisation réglée dans le compte. Le changement apparaît dans l'historique de la fiche.

Localisation

  • city est rapprochée du référentiel des communes de Marvin. postalCode départage les homonymes, country indique le pays. Sans city, postalCode et country sont ignorés.
  • Votre CRM ne gère que la localisation qu'il a posée. Les localisations saisies par le cabinet dans Marvin ne sont jamais retirées ni modifiées.
  • Premier envoi d'une ville : si l'entreprise a déjà cette ville dans Marvin, rien n'est dupliqué et cette localisation reste celle du cabinet ; sinon elle est ajoutée (en localisation principale si l'entreprise n'en a pas).
  • Nouvelle ville : celle que votre CRM avait posée est retirée, la nouvelle est ajoutée.
  • Renvoyer la même ville ne change rien et ne la recalcule pas.
  • city: null retire la seule localisation posée par votre CRM.
  • Ville non reconnue, ville ou code postal illisible (Cergy / Pontoise) : la localisation ne change pas, le reste de l'envoi est appliqué et la réponse porte l'avertissement location_not_found (field: city ou field: postalCode). La mention CEDEX d'un code postal est ignorée (75008 CEDEX 08 vaut 75008).
  • Seul un code pays mal formé est refusé (400 validation_error, field: country).
  • address reste un texte libre, distinct de la localisation.

Champs personnalisés

customFields est un objet dont les clés sont celles du référentiel (companyCustomFields pour l'entreprise, contactCustomFields pour un contact) :

{
  "customFields": {
    "secteur": "Industrie",
    "date_contrat": "2026-09-01",
    "effectif_cadres": 42,
    "client_strategique": true,
    "langues": ["Français", "Anglais"]
  },
  "contacts": [{ "externalId": "9876", "customFields": { "role": "RH" } }]
}
typeValeur attendue
text, textareaTexte, 10 000 caractères au plus
urlTexte commençant par http:// ou https://
emailAdresse e-mail
numberNombre (42, 3.5, -2), ou texte qui n'en contient qu'un ("42")
booleantrue ou false (ou le texte "true" / "false")
dateTexte AAAA-MM-JJ ("2026-09-01"), date du calendrier
selectTexte égal à l'un des choix de options (la casse ne compte pas)
multiselectTableau de choix (["RH", "Comptable"]), ou texte : un choix ("RH") ou plusieurs séparés par une virgule ("RH, Comptable")
markerTexte égal à l'un des identifiants de options

Toute valeur fait 10 000 caractères au plus.

Pour multiselect, préférez le tableau : un choix qui contient une virgule ("Conseil, audit") y reste entier. Chaque élément doit être l'un des choix de options (200 éléments au plus). Un tableau envoyé sur un champ d'un autre type est refusé.

Règles :

  • Une clé inconnue, un champ réservé aux candidats envoyé sur un contact ou une valeur invalide pour le type du champ est refusé (400 validation_error). field nomme le champ : customFields.secteur, ou contacts.0.customFields.role pour le premier contact. Rien n'est écrit.
  • null, la chaîne vide et le tableau vide valent tous « effacer ».
  • Sur l'entreprise et sur un contact créé par votre CRM, la valeur envoyée remplace celle de Marvin et null efface le champ.
  • Sur un contact rapproché d'une fiche qui existait déjà dans Marvin, seuls les champs encore vides sont remplis, et null est ignoré.
  • Un champ date ne crée, ne déplace ni n'annule aucun rappel, même si le cabinet a réglé un rappel automatique sur ce champ : un rappel est toujours assigné à un recruteur.
  • Si le cabinet modifie le réglage d'un champ pendant votre envoi (choix retiré, type changé), l'envoi est refusé (409 custom_field_settings_changed, field nomme le champ) : relisez GET …/fields, puis rejouez.
  • Pour indiquer le rôle d'un contact (RH, comptable…), le cabinet crée dans Marvin un champ personnalisé de type liste ouvert aux contacts. Sa clé et ses choix apparaissent alors dans contactCustomFields.

Exemple — premier envoi

curl -X PATCH \
  "https://api.marvins.ai/v1/public/crm/acme-rh/companies/1234567" \
  -H "X-Marvin-Public-Key: pk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Entreprise Exemple",
    "siren": "123456789",
    "website": "www.entreprise.example",
    "size": "51-200",
    "revenue": 4500000,
    "address": "1 rue de la Paix, 75002 Paris",
    "status": "prospect",
    "city": "Paris",
    "postalCode": "75002",
    "country": "FR",
    "customFields": { "secteur": "Industrie" },
    "contacts": [
      {
        "externalId": "9876",
        "firstName": "Marie",
        "lastName": "Dupont",
        "email": "marie.dupont@entreprise.example",
        "phone": "+33612345678",
        "jobTitle": "DRH",
        "customFields": { "role": "RH" }
      }
    ]
  }'

Exemple — mise à jour d'un seul champ

curl -X PATCH \
  "https://api.marvins.ai/v1/public/crm/acme-rh/companies/1234567" \
  -H "X-Marvin-Public-Key: pk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "revenue": 5200000 }'

Exemple — l'entreprise devient cliente

curl -X PATCH \
  "https://api.marvins.ai/v1/public/crm/acme-rh/companies/1234567" \
  -H "X-Marvin-Public-Key: pk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "status": "client" }'

Réponse

201 quand l'entreprise est créée, 200 quand elle est mise à jour :

{
  "data": {
    "companyId": "1f6c…",
    "created": true,
    "contacts": [{ "externalId": "9876", "personId": "7a2e…", "created": true }],
    "warnings": []
  }
}

warnings liste ce que Marvin n'a pas appliqué, sans bloquer le reste de l'envoi :

CodeSignification
contact_company_conflictLe contact est déjà rattaché à une autre entreprise dans Marvin : il n'est pas déplacé
contact_deletedLe contact a été supprimé dans Marvin : il n'est pas recréé
contact_email_ambiguousPlusieurs fiches portent cet e-mail : un contact distinct a été créé
contact_email_already_linkedLa fiche de cet e-mail est déjà liée à un autre contact de votre CRM : un contact distinct a été créé
location_not_foundVille non reconnue ou illisible (field: city), code postal illisible (field: postalCode) : la localisation n'a pas changé

Chaque avertissement porte un field (ex. contacts.0.externalId) et un message.

Erreurs

En cas d'erreur, aucune fiche n'est créée ni modifiée : vous pouvez rejouer l'envoi tel quel. Le corps a la forme { "error": { "code", "message", "field"? } }.

StatutcodeCause
400validation_errorChamp invalide ou inconnu, nommé dans field (ex. siren, contacts.1.email, customFields.secteur, contacts.0.customFields.role)
401unauthorizedClé absente, mal formée, inconnue ou révoquée
403insufficient_scopeLa clé ne porte pas le droit public:companies:write
403ip_not_allowedLa clé est restreinte à d'autres adresses IP que celle de l'appel. ip donne l'adresse vue par Marvin
404not_foundLe slug n'est pas celui du cabinet de la clé
409record_deletedL'entreprise a été supprimée dans Marvin : elle n'est pas recréée
409record_unresolvedLa fiche liée dans Marvin est introuvable : ne réessayez pas, contactez le support Marvin
409value_already_usedSIREN, SIRET ou LinkedIn déjà portés par une autre entreprise (field)
409custom_field_settings_changedLe réglage d'un champ personnalisé a changé pendant l'envoi (field) : relisez GET …/fields, puis rejouez
422missing_required_fieldChamp obligatoire pour une création (name, contacts.0.lastName…)
422unknown_statusstatus n'est pas une clé de statut entreprise du compte (field: status)
429rate_limitedPlus de 120 requêtes par minute avec la même clé, sur la même adresse (chaque adresse a sa propre limite)
503temporarily_unavailableMarvin traite un autre envoi du même compte : réessayez après le délai de l'en-tête Retry-After

GET …/fields peut rendre 401, 403 insufficient_scope, 403 ip_not_allowed, 404 et 429, avec le même corps.

Bonnes pratiques

  • Gardez l'identifiant de votre CRM stable. C'est lui qui relie votre fiche à celle de Marvin. Un nouvel identifiant crée une nouvelle fiche.
  • Rejouez en cas d'échec réseau, de 429 ou de 503 : un envoi rejoué ne crée rien en double. Après un 429 ou un 503, attendez avant de réessayer.
  • Envoyez l'une après l'autre : Marvin traite les envois d'un même compte un par un ; les paralléliser ne va pas plus vite et produit des 503.
  • Un identifiant reste attaché à sa fiche. Si deux fiches de votre CRM sont fusionnées, continuez d'envoyer l'identifiant de la fiche gardée ; au premier envoi, une fiche de Marvin déjà liée à un identifiant n'est jamais rapprochée d'un autre.
  • Lisez le référentiel avant de brancher : GET …/fields donne les clés de statut et de champs personnalisés à utiliser. Relisez-le quand un envoi est refusé avec unknown_status ou une clé de champ inconnue : le cabinet a pu modifier ses réglages.
  • Reprise de l'existant : pour envoyer les entreprises déjà suivies, appelez la même adresse pour chacune, en restant sous 120 requêtes par minute (la limite vaut pour chaque adresse séparément : GET …/fields ne la consomme pas). Pour les entreprises déjà sous contrat, envoyez signedAt avec la date de signature du contrat.
  • Testez sur un compte de test avant de brancher le compte réel : demandez à Marvin un compte de test et sa clé.

On this page