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 droitpublic:companies:write. - Deux adresses :
PATCH …/companies/{externalId}envoie une entreprise et ses contacts ;GET …/fieldslit 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-Forajouté 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}/fieldsLa 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"] }
]
}
}| Champ | Contenu |
|---|---|
sizes | Valeurs acceptées par size |
companyStatuses | Statuts entreprise du compte : key (à envoyer dans status), label, category |
companyCustomFields | Champs personnalisés de l'entreprise : key, label, type, et options pour les champs à choix |
contactCustomFields | Champs 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.
| Champ | Type | Description |
|---|---|---|
name | string | Nom de l'entreprise, 200 caractères. Obligatoire pour créer l'entreprise |
siren | string | null | 9 chiffres (espaces tolérés) |
siret | string | null | 14 chiffres (espaces tolérés) |
website | string | null | Site web. https:// est ajouté s'il manque |
linkedinUrl | string | null | Page LinkedIn de l'entreprise (/company/…) |
size | string | null | Tranche d'effectif : 0-1, 2-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, 10001+ |
revenue | integer | null | Chiffre d'affaires en euros, de 0 à 2 147 483 647 |
address | string | null | Adresse postale en texte, 500 caractères |
status | string | Clé d'un statut entreprise du compte (companyStatuses du référentiel). Voir Statut |
signedAt | string | null | Date de signature du contrat : AAAA-MM-JJ ou date-heure ISO 8601. Voir Statut |
city | string | null | Ville, 100 caractères : renseigne la localisation de l'entreprise. Voir Localisation |
postalCode | string | null | Code postal, 20 caractères. Départage les villes homonymes, avec city. La mention CEDEX est ignorée |
country | string | null | Pays, code ISO à 2 lettres (FR, BE…), avec city. Absent : la France est privilégiée |
customFields | object | Champs personnalisés de l'entreprise, 100 au plus. Voir Champs personnalisés |
contacts | array | Contacts de l'entreprise, 50 au plus (voir ci-dessous) |
Chaque contact :
| Champ | Type | Description |
|---|---|---|
externalId | string | Obligatoire. Identifiant du contact dans votre CRM, unique dans l'envoi |
firstName | string | Prénom, 100 caractères. Obligatoire pour créer le contact |
lastName | string | Nom, 100 caractères. Obligatoire pour créer le contact |
email | string | null | E-mail. Au premier envoi, rapproche le contact d'une fiche qui existe déjà avec cette adresse |
phone | string | null | Téléphone, au format national français ou international |
jobTitle | string | null | Fonction, 150 caractères |
customFields | object | Champs 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
statusprend la clé exacte d'un statut entreprise du compte, telle que rendue parGET …/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 :nullet la chaîne vide sont refusés (400 validation_error). - Sans
signedAt, le premier passage à un statut de catégoriewondate la signature de l'entreprise du jour de l'envoi, y compris quand elle est créée directement avec ce statut. signedAtenvoyé l'emporte toujours : c'est la date de signature de l'entreprise,nullla vide. Format : une dateAAAA-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
signedAtavec 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
cityest rapprochée du référentiel des communes de Marvin.postalCodedépartage les homonymes,countryindique le pays. Sanscity,postalCodeetcountrysont 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: nullretire 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'avertissementlocation_not_found(field: cityoufield: postalCode). La mentionCEDEXd'un code postal est ignorée (75008 CEDEX 08vaut75008). - Seul un code pays mal formé est refusé (
400 validation_error,field: country). addressreste 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" } }]
}type | Valeur attendue |
|---|---|
text, textarea | Texte, 10 000 caractères au plus |
url | Texte commençant par http:// ou https:// |
email | Adresse e-mail |
number | Nombre (42, 3.5, -2), ou texte qui n'en contient qu'un ("42") |
boolean | true ou false (ou le texte "true" / "false") |
date | Texte AAAA-MM-JJ ("2026-09-01"), date du calendrier |
select | Texte égal à l'un des choix de options (la casse ne compte pas) |
multiselect | Tableau de choix (["RH", "Comptable"]), ou texte : un choix ("RH") ou plusieurs séparés par une virgule ("RH, Comptable") |
marker | Texte é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).fieldnomme le champ :customFields.secteur, oucontacts.0.customFields.rolepour 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
nullefface le champ. - Sur un contact rapproché d'une fiche qui existait déjà dans Marvin, seuls les
champs encore vides sont remplis, et
nullest 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,fieldnomme le champ) : relisezGET …/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 :
| Code | Signification |
|---|---|
contact_company_conflict | Le contact est déjà rattaché à une autre entreprise dans Marvin : il n'est pas déplacé |
contact_deleted | Le contact a été supprimé dans Marvin : il n'est pas recréé |
contact_email_ambiguous | Plusieurs fiches portent cet e-mail : un contact distinct a été créé |
contact_email_already_linked | La fiche de cet e-mail est déjà liée à un autre contact de votre CRM : un contact distinct a été créé |
location_not_found | Ville 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"? } }.
| Statut | code | Cause |
|---|---|---|
| 400 | validation_error | Champ invalide ou inconnu, nommé dans field (ex. siren, contacts.1.email, customFields.secteur, contacts.0.customFields.role) |
| 401 | unauthorized | Clé absente, mal formée, inconnue ou révoquée |
| 403 | insufficient_scope | La clé ne porte pas le droit public:companies:write |
| 403 | ip_not_allowed | La clé est restreinte à d'autres adresses IP que celle de l'appel. ip donne l'adresse vue par Marvin |
| 404 | not_found | Le slug n'est pas celui du cabinet de la clé |
| 409 | record_deleted | L'entreprise a été supprimée dans Marvin : elle n'est pas recréée |
| 409 | record_unresolved | La fiche liée dans Marvin est introuvable : ne réessayez pas, contactez le support Marvin |
| 409 | value_already_used | SIREN, SIRET ou LinkedIn déjà portés par une autre entreprise (field) |
| 409 | custom_field_settings_changed | Le réglage d'un champ personnalisé a changé pendant l'envoi (field) : relisez GET …/fields, puis rejouez |
| 422 | missing_required_field | Champ obligatoire pour une création (name, contacts.0.lastName…) |
| 422 | unknown_status | status n'est pas une clé de statut entreprise du compte (field: status) |
| 429 | rate_limited | Plus de 120 requêtes par minute avec la même clé, sur la même adresse (chaque adresse a sa propre limite) |
| 503 | temporarily_unavailable | Marvin 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
429ou de503: un envoi rejoué ne crée rien en double. Après un429ou un503, 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 …/fieldsdonne les clés de statut et de champs personnalisés à utiliser. Relisez-le quand un envoi est refusé avecunknown_statusou 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 …/fieldsne la consomme pas). Pour les entreprises déjà sous contrat, envoyezsignedAtavec 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é.
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
Versions et dépréciation
Ce qui peut changer sans préavis, comment un changement incompatible est publié et annoncé, et les limites de débit