Gestion des données5 min

Contacts

Gérer les contacts clients rattachés à un compte.

Les contacts sont les personnes de l’organisation cliente rattachées à un compte Billabex. Ce sont eux qui reçoivent les relances de paiement et les autres communications de l’agent.

Vue d’ensemble

Quand l’agent Billabex envoie une relance, il doit savoir qui contacter chez le client. Les contacts portent cette information.

Propriétés d’un contact

Propriété Type Description
id chaîne Identifiant unique (UUID)
fullName chaîne Nom complet de la personne, ou null
email objet Adresse email et statut de validation
phones tableau Tous les numéros de téléphone du contact
language chaîne Code ISO 639-1 de la langue de communication
jobTitle chaîne Fonction côté client, ou null

Statut de l’email

L’objet email contient l’adresse et son statut de validation :

{
  "value": "john@acme.com",
  "status": "Valid",
  "isValid": true
}

Valeurs possibles :

Statut Description
Valid L’email est délivrable
Bounced L’email a rebondi, il n’est pas délivrable
Complained Le destinataire a signalé les emails comme indésirables

Le statut et isValid sont en lecture seule et mis à jour automatiquement d’après les résultats de livraison.

Numéros de téléphone

Un contact peut porter autant de numéros que nécessaire, dans n’importe quel pays. Chaque entrée ressemble à ceci :

{
  "number": "+33612345678",
  "type": "Mobile",
  "country": "FR",
  "origin": "Manual"
}
Champ Description
number Au format E.164, la forme canonique que Billabex stocke et la clé sur laquelle il déduplique
type Mobile, Landline ou Unknown, déduit du numéro et en lecture seule
country ISO 3166-1 alpha-2, déduit lui aussi, null quand le numéro n’appartient à aucun pays unique
origin Manual pour ce que vous envoyez par l’API, Connector pour ce qu’une intégration a apporté

type et country sont calculés à partir des métadonnées d’opérateur, jamais de ce que vous envoyez. Unknown est une vraie réponse et non un échec : plusieurs plans de numérotation, dont celui d’Amérique du Nord, ne distinguent pas du tout les mobiles des fixes.

Écrire des numéros

Envoyez phones sous forme d’une liste d’objets { number, countryCode? } :

{
  "phones": [
    { "number": "+33612345678" },
    { "number": "06 12 34 56 78", "countryCode": "FR" },
    { "number": "0475 12 34 56", "countryCode": "BE" }
  ]
}

Un numéro commençant par + ou 00 porte son propre indicatif et n’a besoin de rien d’autre. Tout autre numéro est un numéro national et exige countryCode (ISO 3166-1 alpha-2). Il n’y a pas de pays par défaut : en deviner un reviendrait à enregistrer en silence un autre numéro que celui que vous vouliez.

La liste fait autorité. Omettez phones pour laisser les numéros du contact intacts, envoyez [] pour les effacer.

Numéros apportés par un connecteur

Si le compte est synchronisé avec un outil de facturation (Odoo, Pennylane, Qonto, Sellsy, Stripe, Zoho Books), les numéros que cet outil porte apparaissent avec origin: "Connector". Écrire phones ne les remplace jamais : cela ne remplace que les entrées Manual, parce que la synchronisation suivante écraserait de toute façon votre modification. Déliez le compte de sa source pour reprendre la main sur ses numéros.

Un client détenant le scope sync écrit lui-même dans la pile Connector : ses numéros n’entrent donc jamais en collision avec ce qu’une personne a saisi dans l’application. Voyez Scopes.

Écritures partiellement appliquées : le tableau warnings

Créer ou mettre à jour un contact renvoie 200 même quand Billabex n’a gardé qu’une partie de ce que vous avez envoyé. Dans ce cas, la réponse porte un tableau warnings, entièrement absent quand rien n’a été écarté :

"warnings": [
  { "field": "fullName", "value": "ACME Purchasing", "reason": "MANUALLY_SET" },
  { "field": "phones", "value": "voir Jean", "reason": "UNPARSEABLE" }
]
reason Ce qui s’est passé
MANUALLY_SET Une personne a saisi ce nom ou cette langue dans l’application, et un client sync ne l’écrase jamais
UNPARSEABLE Le numéro n’a pas pu être lu comme un numéro : il a été écarté, et le reste du contact a bien été écrit

UNPARSEABLE n’apparaît que pour un client détenant le scope sync. Sans ce scope, un numéro illisible donne un 400 sur toute la requête, parce qu’une personne qui saisit un numéro doit être avertie qu’il est erroné plutôt que de le voir disparaître en silence.

Joignabilité par SMS

Le canal SMS ne peut délivrer qu’à un mobile de France ou de Guyane : type: "Mobile" avec country: "FR" ou country: "GF". Tout autre numéro est stocké et renvoyé normalement, il ne peut simplement pas recevoir de message. L’indicateur hasValidPhoneContact du compte vous dit si au moins un contact actif est joignable de cette façon.

Endpoints

Méthode Endpoint Description
GET /accounts/{accountId}/contacts Lister les contacts
POST /accounts/{accountId}/contacts Créer un contact
PATCH /accounts/{accountId}/contacts/{contactId} Modifier un contact
DELETE /accounts/{accountId}/contacts/{contactId} Supprimer un contact
PUT /accounts/{accountId}/contacts/upsert Créer ou mettre à jour

Créer un contact

const response = await fetch(
  '[baseURL]/api/public/v1/accounts/ACCOUNT_ID/contacts',
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      fullName: 'John Smith',
      email: 'john.smith@acme.com',
      language: 'fr',
      jobTitle: 'Responsable comptabilité',
      phones: [{ number: '+33612345678' }],
    }),
  },
);

const contact = await response.json();

Avec cURL

curl -X POST "[baseURL]/api/public/v1/accounts/ACCOUNT_ID/contacts" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "fullName": "John Smith",
    "email": "john.smith@acme.com",
    "language": "fr",
    "phones": [{ "number": "06 12 34 56 78", "countryCode": "FR" }]
  }'

Champs

Champ Obligatoire Description
fullName l’un des deux Nom complet de la personne
email l’un des deux Adresse email
language non Code ISO 639-1 (en, fr, de…), voir la valeur par défaut plus bas
phones non Numéros de téléphone, voir plus haut
jobTitle non Fonction côté client, en texte libre

Un contact a besoin d’un nom ou d’une adresse email, pas des deux. Envoyez l’adresse seule quand c’est tout ce dont vous disposez : aucun nom n’en est déduit, et fullName se relit à null, ce qui vous permet de distinguer « pas de nom » d’un nom qui ressemblerait à une adresse. N’envoyer ni l’un ni l’autre renvoie 400.

language 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.

Modifier un contact

Utilisez PATCH pour ne mettre à jour que certains champs :

const response = await fetch(
  '[baseURL]/api/public/v1/accounts/ACCOUNT_ID/contacts/CONTACT_ID',
  {
    method: 'PATCH',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      fullName: 'John A. Smith',
    }),
  },
);

Seuls les champs fournis sont modifiés. Pour phones, cela signifie : omettre le champ laisse les numéros tels quels, envoyer la liste complète les remplace, envoyer [] les efface tous. Envoyer fullName: null efface le nom, ce qui est refusé si le contact n’a plus d’adresse email pour l’identifier.

Lister les contacts

const response = await fetch(
  '[baseURL]/api/public/v1/accounts/ACCOUNT_ID/contacts?first=10',
  {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  },
);

const data = await response.json();
// data.nodes contient le tableau des contacts
// data.pageInfo contient les informations de pagination

Supprimer un contact

const response = await fetch(
  '[baseURL]/api/public/v1/accounts/ACCOUNT_ID/contacts/CONTACT_ID',
  {
    method: 'DELETE',
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  },
);

// Renvoie 204 No Content en cas de succès

Langue

Le champ language détermine la langue dans laquelle Billabex communique avec le contact. Utilisez les codes ISO 639-1 à deux lettres :

Code Langue
en Anglais
fr Français
de Allemand
es Espagnol
it Italien
nl Néerlandais
pt Portugais

L’agent traduit automatiquement ses relances dans la langue préférée du contact.

Utiliser l’upsert pour synchroniser

Pour synchroniser des contacts depuis un système externe, l’upsert simplifie votre logique :

// Synchroniser les contacts depuis votre CRM
for (const crmContact of crmContacts) {
  await fetch(
    `[baseURL]/api/public/v1/accounts/${crmContact.billabexAccountId}/contacts/upsert`,
    {
      method: 'PUT',
      headers: {
        Authorization: `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        fullName: crmContact.name,
        email: crmContact.email,
        language: crmContact.preferredLanguage || 'fr',
        // Votre CRM sait à quel pays appartient chaque numéro ; Billabex ne le devinera pas.
        phones: crmContact.phoneNumbers.map((number) => ({
          number,
          countryCode: crmContact.countryCode,
        })),
      }),
    },
  );
}

Le guide des opérations upsert détaille la logique de correspondance.

Bonnes pratiques

Renseigner la bonne langue

Une langue correcte garantit que le client reçoit une communication qu’il comprend :

// Faire correspondre la langue réellement préférée du contact
"language": "fr"  // Pour un contact francophone

Traiter les rebonds d’email

Surveillez le champ email.status et mettez à jour vos contacts quand un email rebondit :

if (contact.email?.status === 'Bounced') {
  // Alerter votre équipe pour mettre à jour l'adresse du contact
  // Ou marquer automatiquement le contact comme à traiter
}

Trouver un contact quand personne n’est joignable

Un compte dont aucun contact n’a d’adresse email valide ne peut pas être relancé du tout. Le premier endpoint cherche des contacts possibles pour 0,25 crédit par résultat. Le second cherche leurs coordonnées pour 1 crédit pour une adresse email, 2 dès qu’un numéro de téléphone est trouvé, et 0 quand rien n’est trouvé.

1. Rechercher des candidats (facturé)

Renvoie jusqu’à dix personnes côté client qui semblent s’occuper du paiement des fournisseurs (comptabilité, recouvrement, trésorerie, direction financière), avec leur fonction et l’URL de leur profil public, et aucune coordonnée. L’appel dépense 0,25 crédit par résultat renvoyé et exige le scope accounts:all. Il échoue avant d’appeler le prestataire si le portefeuille contient moins de 1,25 crédit, soit le prix d’un résultat plus l’enrichissement le moins cher : une liste de noms qu’on ne peut convertir en aucune adresse email ne vaut rien. Pour la même raison, l’appel ne renvoie jamais plus de résultats que le portefeuille ne peut ensuite en enrichir : un petit solde donne donc une liste courte plutôt que dix noms et un portefeuille vide.

curl -X POST "[baseURL]/api/public/v1/accounts/ACCOUNT_ID/contact-enrichment/candidates" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
  "candidates": [
    {
      "fullName": "Marie Dupont",
      "jobTitle": "Responsable comptabilité",
      "companyName": "ACME SAS",
      "profileUrl": "https://www.linkedin.com/in/marie-dupont",
      "isLegalRepresentative": false,
      "officialRole": null
    }
  ],
  "creditsCharged": 0.25,
  "balanceAfter": 9.75,
  "searchScope": "Finance"
}

searchScope vous dit qui vous avez sous les yeux. Une entreprise de trois personnes n’emploie personne dont la fonction est « Finance » : c’est son gérant qui paie les factures, et son titre dit « Gérant ». Quand la recherche finance ne trouve personne, l’appel élargit la question de lui-même et dit jusqu’où il est allé :

searchScope Qui sont les candidats
Finance Des profils finance, assez seniors pour agir sur un paiement. La réponse visée.
FinanceWide Des profils finance de toute spécialité : le client n’étiquette rien précisément.
Decisionmakers Dirigeants et C-level de toute fonction : qui que ce soit qui dirige l’entreprise.

L’élargissement est gratuit et ne remplace jamais une vraie réponse : un seul profil finance l’arrête. Traitez toute valeur autre que Finance comme le signal que vous vous apprêtez à contacter un dirigeant plutôt qu’un comptable.

isLegalRepresentative est posé quand le registre public français désigne cette personne comme dirigeant légal de l’entreprise, et officialRole porte alors sa qualité (« Président de SAS », « Gérant »). Ces candidats sont listés en premier. L’indicateur est informatif : il ne coûte rien, ne change aucun prix, et une personne que le registre connaît mais que le prestataire ignore n’est simplement pas renvoyée.

2. Enrichir un contact (facturé)

Renvoyez le profileUrl du candidat que vous avez retenu, ou le nom complet d’une personne que vous connaissez déjà, sur un compte qui déclare un domaine d’entreprise. L’appel rend la main dès que le prestataire l’a accepté ; le contact apparaît sur le compte une minute plus tard environ, tout seul. Il n’y a rien à interroger en boucle, et fermer la connexion ne perd rien.

curl -X POST "[baseURL]/api/public/v1/accounts/ACCOUNT_ID/contact-enrichment" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "fullName": "Marie Dupont",
    "profileUrl": "https://www.linkedin.com/in/marie-dupont",
    "jobTitle": "Responsable comptabilité",
    "wantsPhone": true
  }'
Champ Obligatoire Description
wantsPhone oui Chercher aussi un numéro : 2 crédits au lieu d’1, et seulement si un numéro est trouvé
contactId non Compléter un contact existant plutôt qu’en ajouter un
fullName non Qui chercher, prénom et nom. Obligatoire sauf si contactId ou profileUrl identifie la personne
profileUrl non Profil LinkedIn, issu de la recherche de candidats ou de vos propres données
jobTitle non Fonction de la personne, quand la recherche de candidats en a renvoyé une

La recherche doit être ancrée sur le débiteur. L’appel exige soit un profileUrl, qui nomme une personne, soit un domain d’entreprise sur le compte, qui nomme une entreprise. Sans l’un des deux, il est refusé en 400 (app-invoicing.contact.enrichment.not-anchored) et rien n’est dépensé : un nom seul correspond à une entreprise homonyme n’importe où dans le monde, le prestataire répond avec son adresse, et rien dans cette réponse ne dit que ce n’est pas la bonne entreprise. Renseigner le domain du compte est en général le correctif le moins cher, et cela donne aussi son logo au compte.

Un nom, c’est un prénom et un nom. Le prestataire accepte soit les deux ensemble, soit une linkedin_url. Un mot unique est refusé en 400 (app-invoicing.contact.enrichment.identity-incomplete) plutôt qu’envoyé pour revenir vide. Passez un profileUrl quand vous ne détenez qu’un seul mot.

Une adresse professionnelle et une adresse personnelle sont cherchées à chaque appel, si bien qu’une personne joignable uniquement chez elle est trouvée aussi. Le prix ne change pas : 1 crédit pour une adresse, 2 dès qu’un numéro revient, et l’adresse professionnelle l’emporte quand les deux sont trouvées.

profileUrl est l’entrée la plus forte qu’on puisse donner au prestataire, et elle n’a pas à venir de la recherche de candidats : passez le profil LinkedIn que vous détenez déjà. Tout ce qui n’est pas une adresse LinkedIn est refusé en 400, plutôt que dépensé sur une recherche qui ne pouvait pas aboutir.

Les deux mêmes opérations facturées existent sur le serveur MCP sous les noms search-contact-candidates et enrich-contact. Toutes deux exigent mcp:write, et le client doit demander confirmation avant de dépenser des crédits.

search-contact-candidates exige en outre un domain d’entreprise sur le compte. Sans lui, l’appel renvoie app-invoicing.contact.candidate-search.not-anchored avant de facturer quoi que ce soit. Renseignez le vrai site du client par PATCH /accounts/:id, à partir d’une information fournie par l’utilisateur ou par un document, jamais d’un domaine deviné à partir du nom de l’entreprise.

Pour aller plus loin

Support

Une question sur la gestion des contacts ? Écrivez-nous via le formulaire de contact.