Gestion des données5 min

Opérations upsert

Créer ou mettre à jour une ressource en une seule requête idempotente.

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 :

  1. cherche si un enregistrement correspondant existe déjà ;
  2. le met à jour s’il est trouvé ;
  3. 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 fullName ou email

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

Support

Une question sur les opérations d’upsert ? Écrivez-nous via le formulaire de contact.