TL;DR
- La v2 de Pipedrive est stricte : plus de coercition de type implicite, plus de
nulltoléré, pagination par curseur au lieu de offset/limit. - Réécrire le métier pour une migration d'API, c'est une erreur. On l'enveloppe, on ne le réécrit pas.
- Une couche de normalisation à la frontière I/O : le métier parle une seule forme, seul l'adaptateur connaît v1 vs v2.
- Un audit champ par champ des custom fields → leur type v2 attendu, construit avant de basculer.
- Résultat : zéro régression côté métier, et une API stricte qui révèle les hypothèses molles qu'on ignorait.
Le contexte : une API CRM tissée dans tout le produit
Le produit est un SaaS B2B qui s'appuie sur Pipedrive comme CRM de fond. Pas « une intégration optionnelle » : le cœur du métier crée et met à jour des deals, des persons, des organizations et une bonne dizaine de custom fields propres au client. Et ces appels partent de partout : du backend pour les automatisations, mais aussi directement du frontend pour les actions utilisateur en temps réel.
Quand Pipedrive a publié son API v2 à côté de la v1, le message était clair : la v1 finira par disparaître. La v2 est mieux pensée, plus cohérente, et surtout beaucoup plus stricte. Le problème : on ne peut pas mettre le produit en maintenance le temps de tout réécrire. Les clients bookent, vendent, relancent leurs prospects en ce moment même. La migration doit se faire à chaud, par petits incréments, sans casser ce qui tourne.
La vraie contrainte n'était pas technique, elle était temporelle : migrer un système que personne n'a le droit d'arrêter. On ne change pas le moteur d'un avion en vol, on construit une nacelle autour.
Les pièges de la rigueur v2
La v1 était permissive. Trop. Elle « devinait » ce qu'on voulait dire : une chaîne vide pour un champ numérique ? Elle l'avalait. Une option de liste passée à plat ? Elle s'en arrangeait. La v2 refuse tout ça, et chaque tolérance perdue est un piège à découvrir :
- Pas de coercition de type implicite. Un custom field numérique qui reçoit une chaîne
""renvoie un400silencieux. La v1 l'aurait accepté comme « vide ». - Les champs multi-options doivent être des tableaux d'IDs d'option, pas une valeur à plat.
"3"devient[3], et ce sont de vrais entiers. - Les dates sont strictes :
YYYY-MM-DD, point. Pas de timestamp, pas de format local toléré. - Les champs monétaires veulent une forme
{ value, currency }, plus un simple nombre posé à côté d'un champ devise. - Les
nullsont rejetés. En v1, envoyernull« effaçait » un champ. En v2, il faut omettre la clé. - La pagination a changé de paradigme : adieu
start/limitpar offset, bonjour la pagination par curseur opaque.
Pris isolément, chacun est anodin. Pris ensemble, dispersés dans des dizaines de points d'appel répartis entre front et back, ils forment un champ de mines. La tentation naturelle, corriger chaque appel sur place, est exactement le mauvais réflexe.
La stratégie : une couche de normalisation à la frontière
Le principe directeur tient en une phrase : abstraire la différence de version à la frontière I/O, pas dans la logique métier. Le métier ne doit jamais savoir qu'il existe une v1 et une v2. Il continue de parler une seule forme, celle qu'il connaît déjà, et seul l'adaptateur, au bord du système, traduit dans les deux sens.
C'est un anti-corruption layer au sens DDD : un sas qui empêche les bizarreries d'un système externe de contaminer le vôtre. Concrètement, trois fonctions de traduction, toutes posées sur le seul chemin par lequel les données entrent et sortent de Pipedrive :
normalizeCustomFieldsForPipedriveV2(), en sortie : retire lesnullet les vides, coerce les IDs en vrais nombres, formate les dates, transforme les multi-selects en tableaux d'IDs, emballe le monétaire en{ value, currency }.mapPersonV2ToV1Shape(), en entrée : la v2 renvoieemailsetphonescomme des tableaux d'objets ; on les aplatit vers le.email/.phoneuniques que tout le reste de l'appli attend. Aucun code en aval ne change.fetchAllPagesV2(), enveloppe la pagination par curseur : le métier demande « toutes les pages », l'adaptateur boucle sur le curseur opaque jusqu'à épuisement.
La version cible est lue depuis la config, par endpoint. L'adaptateur regarde « cet endpoint est-il en v1 ou en v2 ? » et applique la bonne transformation. On peut donc basculer un endpoint à la fois, valider en prod, puis passer au suivant, exactement l'incrémentalité qu'imposait la contrainte.
if, pas une chasse au trésor dans cinquante fichiers métier.Avant / après : un payload que la v2 refuse
Voici le genre de payload que la v1 avalait sans broncher et que la v2 rejette d'un 400. À gauche le brut, à droite ce que la couche de normalisation produit :
// ❌ brut, refusé par la v2
{
"title": "Deal Acme",
"value": 4990, "currency": "EUR", // monétaire à plat
"a1b2c3_close_date": "2026-06-11T00:00:00Z", // date non stricte
"d4e5f6_segment": "3", // multi-option à plat
"vin": "", // chaîne vide → champ numérique
"g7h8i9_notes": null // null rejeté
}
// ✅ normalisé, accepté par la v2
{
"title": "Deal Acme",
"value": { "value": 4990, "currency": "EUR" }, // forme monétaire
"a1b2c3_close_date": "2026-06-11", // YYYY-MM-DD strict
"d4e5f6_segment": [3] // tableau d'IDs (entiers)
// vin et g7h8i9_notes : OMIS, pas mis à null
}
La règle qui sous-tend la transformation : un champ qu'on ne peut pas remplir proprement, on l'omet. On ne le force pas à null, on ne lui colle pas une chaîne vide. Absence de clé = « ne touche pas à ce champ ». C'est la sémantique exacte qu'attend la v2.
L'anecdote : un vin: "" qui partait dans le vide
Le bug le plus instructif a été le plus bête. Quelque part dans le code, on envoyait vin: "" vers un champ de type texte court quand le véhicule n'avait pas de numéro de châssis. En v1 : aucun problème, champ vide, on passe. En v2 : 400, et comme l'erreur remontait au milieu d'un lot d'appels, elle se perdait dans le bruit.
Le correctif n'était pas « corriger ce champ », c'était exclure systématiquement les champs vides à la frontière. Et la façon de l'attraper avant qu'il ne casse en prod : confronter chaque champ envoyé à la doc officielle de migration v2 de Pipedrive. Pas deviner, vérifier, type attendu par type attendu.
a1b2c3…) à son type v2 attendu, numérique, date, monétaire, set, enum. Rien ne part « à l'aveugle ». C'est le document qu'on lit en même temps que le code de normalisation pour garantir qu'ils sont d'accord.La pagination par curseur, isolée elle aussi
Le passage de offset/limit à un curseur opaque est typiquement le genre de changement qui, s'il fuite dans le métier, le pourrit. Chaque endroit qui faisait for (start = 0; …; start += limit) aurait dû apprendre l'existence d'un curseur. Mauvaise idée.
À la place, fetchAllPagesV2() absorbe toute la mécanique : elle appelle l'endpoint, lit le curseur de la réponse, rappelle avec ce curseur, accumule, et s'arrête quand il n'y a plus de page suivante. Le métier reçoit une liste complète, exactement comme avant. Il ne sait même pas que la pagination a changé de nature.
Ce que je retiens
- Ne réécrivez jamais le métier pour une migration d'API, enveloppez-le. La logique de votre produit n'a rien à voir avec le caprice de version d'un tiers. Si elle doit changer, c'est que la frontière a fuité.
- Un anti-corruption layer à la frontière, c'est l'assurance la moins chère. Une poignée de fonctions de traduction valent mieux que cent corrections dispersées et impossibles à retrouver le jour du nettoyage.
- Construisez l'audit champ par champ avant de basculer. Mapper chaque custom field à son type v2 attendu transforme une migration risquée en checklist. On vérifie, on ne devine pas.
- Une API stricte révèle vos hypothèses molles. Le
vin: ""traînait depuis toujours. La v2 ne l'a pas créé, elle l'a juste rendu visible. C'est un cadeau déguisé.
Cette discipline, isoler le changement à la frontière, laisser le cœur du système intact, auditer avant d'agir, c'est exactement la même que j'applique aux intégrations d'agents IA en production. Là aussi, un modèle ou un fournisseur change sous vos pieds ; et là aussi, c'est l'adaptateur qui doit encaisser le choc, jamais le métier.
Une migration d'API risquée sur un SaaS en prod ?
Migrer un système que personne n'a le droit d'arrêter, ça se prépare. Si vous avez une API tierce qui durcit, une intégration tissée partout, ou juste un doute sur la stratégie, parlons-en avant le big-bang.
Discutons de votre migration →