Quand vous synchronisez des données depuis un système externe, il faut souvent créer de nouveaux enregistrements ou mettre à jour ceux qui existent. Le motif upsert réunit les deux en une seule requête idempotente. Ce guide explique comment fonctionnent les endpoints d’upsert de l’API Billabex.
Qu’est-ce qu’un upsert ?
« Upsert » est la contraction de update et insert. Une opération d’upsert :
- cherche si un enregistrement correspondant existe déjà ;
- le met à jour s’il est trouvé ;
- en crée un nouveau sinon.
Vous n’avez donc plus besoin de vérifier l’existence d’un enregistrement avant de décider s’il faut le créer ou le modifier.
Pourquoi l’utiliser ?
- Idempotence : rejouer la même requête produit le même résultat
- Simplicité : un seul endpoint au lieu d’une logique de création et d’une logique de mise à jour
- Fiabilité : plus d’accès concurrent entre la vérification et la création
- Adapté à la synchronisation : idéal pour un import périodique
Endpoints disponibles
Billabex propose l’upsert pour les personnes rattachées à un compte :
| Endpoint | Description |
|---|---|
PUT /accounts/{accountId}/contacts/upsert |
Upsert d’un contact |
Tous exigent le scope OAuth accounts:all.
Comment la correspondance est établie
Les endpoints d’upsert cherchent un enregistrement existant en deux étapes.
Étape 1 : correspondance par email (prioritaire)
Si la requête contient un email, le système cherche d’abord un contact existant portant la même adresse. La comparaison est insensible à la casse.
// Correspond au contact existant dont l'email est "john@acme.com"
{
"email": "John@ACME.com",
"fullName": "John Smith"
}
Étape 2 : correspondance par nom (repli)
Sans correspondance par email, ou si aucun email n’a été fourni, le système cherche une correspondance approximative sur le nom, fondée sur la distance de Levenshtein.
Un nom correspond si la distance d’édition ne dépasse pas :
- 2 caractères, ou
- 20 % de la longueur du plus court des deux noms,
la plus petite des deux valeurs faisant foi.
// Ces noms correspondent à "John Smith" :
'john smith'; // casse seulement
'John Smth'; // 1 caractère manquant
'Jon Smith'; // 1 caractère différent
// Ceux-ci ne correspondent pas :
'Jonathan Smith'; // trop de caractères en plus
'J. Smith'; // trop différent
Résultat
- Correspondance trouvée : l’enregistrement existant est mis à jour avec les champs fournis
- Aucune correspondance : un enregistrement est créé, ce qui exige
fullNameouemail
Format de la requête
Tous les endpoints d’upsert acceptent le même corps :
{
"fullName": "John Smith",
"email": "john.smith@acme.com",
"language": "fr"
}
| Champ | Obligatoire | Description |
|---|---|---|
fullName |
voir plus bas | Nom complet de la personne |
email |
voir plus bas | Adresse email, utilisée pour la correspondance |
language |
non | Code de langue ISO 639-1, par exemple en ou fr |
En mise à jour, tous les champs sont facultatifs : seuls ceux que vous fournissez sont modifiés. Envoyer fullName: null sur un contact que l’appel fait correspondre laisse le nom existant en place au lieu de l’effacer. Une synchronisation nocturne dont l’export ne porte pas de nom n’en efface donc jamais un : l’effacement passe par l’endpoint de mise à jour.
En création, fullName ou email est exigé, pas les deux : une adresse seule identifie un contact, et aucun nom n’en est déduit. language est facultatif et vaut par défaut la langue du premier contact actif du compte, puis la langue de contact par défaut de l’organisation, puis fr.
Exemples
Upsert d’un contact
const response = await fetch(
'[baseURL]/api/public/v1/accounts/ACCOUNT_ID/contacts/upsert',
{
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
fullName: 'John Smith',
email: 'john.smith@acme.com',
language: 'fr',
}),
},
);
const contact = await response.json();
Avec cURL
curl -X PUT "[baseURL]/api/public/v1/accounts/ACCOUNT_ID/contacts/upsert" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"fullName": "John Smith",
"email": "john.smith@acme.com",
"language": "fr"
}'
Réponse
Les endpoints d’upsert renvoient l’enregistrement créé ou mis à jour :
{
"id": "789e0123-e89b-12d3-a456-426614174000",
"fullName": "John Smith",
"email": {
"address": "john.smith@acme.com",
"status": "Valid"
},
"language": "fr"
}
Création ou mise à jour, selon le cas
| Situation | Résultat |
|---|---|
| L’email correspond à un enregistrement existant | Cet enregistrement est mis à jour |
| Le nom correspond, sans correspondance par email | Cet enregistrement est mis à jour |
Aucune correspondance, fullName ou email fourni |
Un enregistrement est créé |
Aucune correspondance, ni fullName ni email |
Erreur 400 |
Cas d’usage
Synchronisation périodique depuis un ERP
Synchroniser chaque jour les contacts clients de votre ERP :
for (const erpContact of erpContacts) {
await fetch(
`[baseURL]/api/public/v1/accounts/${erpContact.accountId}/contacts/upsert`,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
fullName: erpContact.name,
email: erpContact.email,
language: erpContact.language || 'fr',
}),
},
);
}
Traitement d’un webhook
Traiter un webhook entrant sans vous soucier de l’état de l’enregistrement :
app.post('/webhook/contact-updated', async (req, res) => {
const { accountId, contact } = req.body;
// L'upsert gère aussi bien un contact nouveau qu'existant
await fetch(`[baseURL]/api/public/v1/accounts/${accountId}/contacts/upsert`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(contact),
});
res.sendStatus(200);
});
Pour aller plus loin
- Contacts : la gestion des personnes rattachées à un compte
- Sources de compte : relier un compte à un système externe
- Référence de l’API : documentation complète des endpoints
Support
Une question sur les opérations d’upsert ? Écrivez-nous via le formulaire de contact.