Gestion des données8 min

Sources de compte

Relier et délier une source de données externe à un compte, par API.

Quand vous synchronisez des comptes depuis un système externe (ERP, CRM, plateforme de facturation), vous pouvez relier chaque compte Billabex à son enregistrement d’origine par une référence de source. Ce guide explique comment elles fonctionnent et comment les gérer par l’API.

Qu’est-ce qu’une source ?

Une source est une référence qui relie un compte Billabex à un système externe. Elle dit à Billabex :

  • d’où viennent les données du compte, par l’identifiant de votre intégration ;
  • à quel enregistrement il correspond, par l’identifiant dans le système externe ;
  • quand il a été synchronisé pour la dernière fois.

Quand un compte a une source, Billabex le considère comme géré de l’extérieur, ce qui change la façon dont il peut être modifié et supprimé.

La source d’un compte et le sourceId d’un document sont deux choses différentes

Une organisation n’a pas une source de données. Elle peut détenir plusieurs connexions de connecteur à la fois (Odoo, Pennylane, Qonto, Sellsy, Stripe, Zoho Books) pendant que son équipe saisit des factures dans la webapp et que votre intégration en dépose d’autres par cette API.

La source décrite ici appartient à un compte. Les factures et les avoirs ne portent pas de référence de source, mais chacun peut porter un sourceId à plat : votre identifiant pour ce document dans votre système. Même mot, deux espaces de noms, deux portées :

Champ Porté par Portée de l’unicité Posé par
source.sourceId un compte par connexion PUT /accounts/{accountId}/source
sourceId une facture ou un avoir par organisation POST /invoices, POST /credit-notes

Envoyez sourceId à la création d’un document et vous obtenez une poignée d’idempotence gratuite : une seconde création avec le même sourceId est refusée en 400 avec app-invoicing.invoice.duplicate-source-id (ou app-invoicing.credit-note.duplicate-source-id), au lieu de produire un doublon en silence après une réponse HTTP perdue. Récupérez le document avec GET /invoices?filters=sourceId:is:VOTRE-ID. La valeur est figée à la création, jamais modifiable ensuite, opaque pour Billabex, et sensible à la casse : sourceId n’accepte que les opérateurs de filtre is, isAnyOf, isEmpty et isNotEmpty, jamais contains ni equals.

Les numéros de documents sont uniques par compte, pas par organisation : deux de vos clients peuvent chacun avoir leur propre AVKA0011527, et les deux sont acceptés. Une recherche par numéro restreinte à un compte répond le document de ce compte ; une recherche par numéro sur toute une organisation peut répondre n’importe lequel des homonymes, alors passez le compte dès que vous le connaissez. Le rapprochement sur sourceId évite entièrement la question.

Quelques conséquences à intégrer :

  • Un compte sans source est parfaitement normal. Cela signifie simplement qu’aucun connecteur ne le pilote. Ce n’est ni un enregistrement incomplet ni un enregistrement cassé.
  • La source d’un compte ne dit jamais d’où vient l’une de ses factures. Chaque facture et chaque avoir porte son propre channel, posé une fois à la création et jamais modifié ensuite : PublicApi pour une création par cette API, Manual pour une saisie dans la webapp, AdminApi ou Mcp pour les autres points d’entrée programmatiques, ou le nom du connecteur pour un import par connecteur. Les documents créés avant que ces points d’entrée soient distingués portent la valeur historique Api. Lisez channel sur le document, pas source sur le compte.
  • Un document importé par un connecteur porte aussi un connectionId, qui nomme la connexion exacte dont il vient. Il vaut null sur tout ce que vous créez, et sur les documents antérieurs à ce champ.
  • Ne déduisez jamais « puis-je écrire ceci » de channel ni de source. Chaque compte, facture et avoir porte un readOnlyReason : null signifie qu’il accepte les écritures, sinon il nomme le connecteur qui les écraserait. Il est renvoyé par les endpoints de ressource unique et par chaque élément de GET /accounts, GET /invoices et GET /credit-notes : paginer une liste vous dit donc déjà quelles lignes vous pouvez modifier ou supprimer. Une écriture tentée alors qu’il est renseigné échoue avec app-invoicing.document.managed-action-forbidden.
  • source: null seul ne signifie pas « jamais connecté ». Un compte porte aussi lastUnlinkedSource, la source dont il a été détaché. Les deux à null signifient que le compte a été créé à la main ou par cette API ; source: null avec lastUnlinkedSource renseigné signifie que le lien vers un outil de facturation a été rompu, et donc que les chiffres ne suivent plus cet outil. Lisez les deux ensemble avant de considérer un compte comme géré manuellement.

Propriétés d’une source

Une référence de source contient quatre propriétés :

Propriété Type Description
connectionId chaîne Identifiant de votre intégration, par exemple my-erp-sync
sourceId chaîne Identifiant du compte dans votre système externe, par exemple CUST-00123
connectorType chaîne Toujours Custom pour une source gérée par API
lastUpdate datetime Date du dernier lien ou de la dernière mise à jour

connectionId regroupe les comptes d’une même intégration. Utilisez une valeur cohérente pour tous les comptes venant du même système source.

sourceId identifie le compte de façon unique dans votre système externe. Il devrait correspondre à la clé primaire ou à l’identifiant unique de votre base source.

Relier une source

Utilisez le PUT pour relier une source à un compte existant :

PUT /api/public/v1/accounts/{accountId}/source

Cet endpoint est idempotent : appelé avec la même source (mêmes connectionId et sourceId), il met simplement à jour la date lastUpdate au lieu de renvoyer une erreur.

Requête

const response = await fetch(
  '[baseURL]/api/public/v1/accounts/ACCOUNT_ID/source',
  {
    method: 'PUT',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      connectionId: 'my-erp-integration',
      sourceId: 'CUST-00123',
      // Facultatif : préciser une date lastUpdate
      // lastUpdate: '2024-01-15T10:30:00.000Z',
    }),
  },
);

const account = await response.json();

Avec cURL

curl -X PUT "[baseURL]/api/public/v1/accounts/ACCOUNT_ID/source" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "connectionId": "my-erp-integration",
    "sourceId": "CUST-00123"
  }'

Corps de la requête

Propriété Type Obligatoire Description
connectionId chaîne oui Identifiant de votre intégration
sourceId chaîne oui Identifiant du compte dans votre système externe
lastUpdate datetime non Date de la dernière synchronisation, maintenant par défaut

Réponse

L’endpoint renvoie le compte mis à jour, avec sa référence de source :

{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "fullName": "Acme Corporation",
  "source": {
    "connectionId": "my-erp-integration",
    "sourceId": "CUST-00123",
    "connectorType": "Custom",
    "lastUpdate": "2024-01-15T10:30:00.000Z"
  }
}

connectorType vaut toujours Custom. Si lastUpdate n’est pas fourni, il prend l’heure courante du serveur.

Mettre à jour la date de synchronisation

Utilisez le PATCH pour ne mettre à jour que la date lastUpdate d’une source existante :

PATCH /api/public/v1/accounts/{accountId}/source

Pratique pour marquer un compte comme « récemment synchronisé » sans relier à nouveau toute la source.

Requête

const response = await fetch(
  '[baseURL]/api/public/v1/accounts/ACCOUNT_ID/source',
  {
    method: 'PATCH',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      // Facultatif : préciser une date lastUpdate
      // Omise, elle prend l'heure courante
      lastUpdate: '2024-01-15T10:30:00.000Z',
    }),
  },
);

const account = await response.json();

Avec cURL

# Mise à jour à l'heure courante
curl -X PATCH "[baseURL]/api/public/v1/accounts/ACCOUNT_ID/source" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

# Mise à jour à une date précise
curl -X PATCH "[baseURL]/api/public/v1/accounts/ACCOUNT_ID/source" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "lastUpdate": "2024-01-15T10:30:00.000Z"
  }'

Corps de la requête

Propriété Type Obligatoire Description
lastUpdate datetime non La nouvelle date de synchronisation, heure courante par défaut

Réponse

L’endpoint renvoie le compte mis à jour :

{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "fullName": "Acme Corporation",
  "source": {
    "connectionId": "my-erp-integration",
    "sourceId": "CUST-00123",
    "connectorType": "Custom",
    "lastUpdate": "2024-01-15T10:30:00.000Z"
  }
}

Note : cet endpoint ne fonctionne que sur un compte qui a déjà une source. Sans source, vous recevez une erreur 400 Bad Request.

Créer un compte avec sa source

Vous pouvez aussi inclure la source à la création :

const response = await fetch('[baseURL]/api/public/v1/accounts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    organizationId: 'YOUR_ORG_ID',
    fullName: 'Acme Corporation',
    currencyCode: 'EUR',
    billingAddress: {
      street: '123 rue Principale',
      city: 'Paris',
      postalCode: '75001',
      country: 'FR',
    },
    source: {
      connectionId: 'my-erp-integration',
      sourceId: 'CUST-00123',
    },
  }),
});

Le compte est créé et la source reliée en une seule requête.

Délier une source

Pour retirer le lien de source d’un compte, utilisez le DELETE :

DELETE /api/public/v1/accounts/{accountId}/source

Requête

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

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

Avec cURL

curl -X DELETE "[baseURL]/api/public/v1/accounts/ACCOUNT_ID/source" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

L’endpoint renvoie 204 No Content en cas de succès.

Ce que cela implique dans Billabex

Relier une source à un compte a plusieurs conséquences.

1. Protection contre la suppression

Un compte avec une source ne peut pas être supprimé depuis l’interface Billabex. Il faut d’abord délier la source. Cela évite la suppression accidentelle de données synchronisées.

2. Exclusion désactivée dans l’interface

Un compte avec une source Custom ne peut pas être exclu depuis l’interface Billabex. La fonction d’exclusion est prévue pour les comptes pilotés par un connecteur, où l’utilisateur peut vouloir filtrer ce qui est synchronisé. Odoo v1 synchronise volontairement tous les comptes et n’expose pas encore ce filtre.

Pour un compte géré par API, c’est votre intégration qui décide directement quels comptes existent dans Billabex.

3. Statut orphelin après déliaison

Quand vous déliez une source, le compte devient « orphelin » : il n’a plus de lien avec aucun système externe. Il reste dans Billabex et peut alors être :

  • supprimé manuellement depuis l’interface ;
  • relié à une autre source par l’API ;
  • géré de façon indépendante.

4. Seules les sources Custom se délient par API

L’endpoint de déliaison ne fonctionne que pour les comptes dont la source est Custom. Les comptes reliés par un connecteur Billabex (Odoo, Pennylane, Qonto, Sellsy, Stripe, Zoho Books) se gèrent depuis les réglages de leur connexion.

Bonnes pratiques

Des identifiants de connexion cohérents

Choisissez un connectionId parlant, qui identifie votre intégration :

// Bien : l'intégration est clairement identifiée
connectionId: 'salesforce-billing-sync';
connectionId: 'netsuite-production';

// À éviter : trop générique ou incohérent
connectionId: 'sync';
connectionId: 'integration-1';

Des identifiants de source stables

sourceId devrait être la clé primaire ou l’identifiant stable de votre système :

// Bien : des identifiants stables
sourceId: 'CUST-00123'; // Numéro client
sourceId: 'acc_1234567890'; // Identifiant en base

// À éviter : des valeurs susceptibles de changer
sourceId: 'acme-corp'; // Nom de société, qui peut changer

Relier avant de créer les données liées

Lors d’un import, reliez d’abord la source, puis créez les factures et les contacts :

  1. créer ou mettre à jour le compte avec sa source ;
  2. créer les contacts ;
  3. créer les factures référençant le compte.

Toutes les données sont ainsi correctement associées à la référence du système externe.

Gérer proprement une nouvelle liaison

Le PUT est idempotent. Appelé avec les mêmes connectionId et sourceId :

  • la date lastUpdate est mise à jour ;
  • aucune erreur n’est renvoyée.

Pour faire pointer la source vers un autre enregistrement externe, il faut d’abord délier la source courante, puis relier la nouvelle :

// 1. Délier la source courante
await fetch('[baseURL]/api/public/v1/accounts/ACCOUNT_ID/source', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${accessToken}` },
});

// 2. Relier la nouvelle source
await fetch('[baseURL]/api/public/v1/accounts/ACCOUNT_ID/source', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    connectionId: 'new-erp-integration',
    sourceId: 'NEW-CUST-456',
  }),
});

Pour aller plus loin

Support

Une question sur les sources de compte ? Écrivez-nous via le formulaire de contact.