---
title: "Ajouter un service SaaS (exemple avec sci-facturation)"
weight: 40
description: "Le parcours complet pour brancher un webservice sur l'API : utilisateur de service, client OAuth smartauth, fiche produit autorisé, catalogue et vérifications."
---

# Ajouter un service SaaS (exemple avec sci-facturation)

Cette page déroule de bout en bout l'ajout d'un webservice appelant. Elle prend
sci-facturation pour exemple : remplacez son nom, son préfixe et l'URL de son
webhook par ceux de votre propre service. Comptez une quinzaine de minutes,
hors saisie du catalogue.

Les étapes se suivent dans l'ordre de leurs dépendances : chacune a besoin de
la précédente.

## Étape 1 : créer l'utilisateur de service côté Dolibarr

Dans **Accueil > Utilisateurs et groupes > Nouvel utilisateur**, créez un
utilisateur dédié, par exemple `svc-scifacturation`, avec :

- un mot de passe aléatoire que personne n'utilise, car cet utilisateur ne se
  connecte jamais à l'interface ;
- les droits sur les tiers (lecture et création), sur les produits et
  services, sur les commandes et sur les factures. C'est en effet lui qui
  porte les écritures comptables produites par l'API.

Pourquoi un utilisateur dédié ? D'abord parce qu'un compte administrateur ne
suffit pas : Dolibarr vérifie les droits bruts, module par module, au moment de
valider une commande ou une facture. Ensuite parce qu'un utilisateur partagé
entre plusieurs services rendrait le journal comptable illisible.

![Fiche de l'utilisateur de service avec les droits tiers, produits, commandes et factures cochés](screenshots/exemple-utilisateur-service.webp)

## Étape 2 : créer le client OAuth chez smartauth

Ouvrez la liste des clients OAuth par **Accueil > Configuration > Modules >
smartauth** (icône d'engrenage), onglet OAuth, puis **Nouveau client OAuth**.

![Formulaire de création d'un client OAuth dans smartauth](screenshots/exemple-nouveau-client-oauth.webp)

Renseignez le formulaire ainsi :

| Champ | Valeur | Pourquoi |
|---|---|---|
| Nom | `SCIfacturation M2M` | Un libellé libre, destiné à vous seul |
| Type | **Confidentiel** | Obligatoire pour le flux machine à machine (M2M) |
| PKCE obligatoire | **Non** | PKCE protège le flux à code d'autorisation et ne s'applique pas au flux M2M |
| Grants autorisés | **client_credentials seul** | Un seul webservice, donc un seul moyen de s'authentifier |
| Redirect URIs | `https://scifacturation.example/callback` | Jamais utilisé par ce flux ; le champ accepte une valeur de remplissage |
| Utilisateur de service | `svc-scifacturation` | L'utilisateur créé à l'étape 1 |
| Durée access token | **3600** (1 heure) | Le webservice met le jeton en cache et en redemande un à l'expiration |
| Durée refresh token | Valeur par défaut | Sans importance : ce flux n'émet jamais de refresh token |
| Scopes | Rien à cocher | Le périmètre du service est borné par sa fiche dans smartmakersaasbilling, et non par des scopes |
| Statut | Actif | |

À l'enregistrement, l'écran affiche **une seule fois** le `client_id` et le
**secret** en clair : copiez-les immédiatement. Le secret n'est conservé que
sous forme hachée, il ne pourra donc plus être relu, seulement régénéré.

Puis, **sur cette même fiche, cliquez sur "Autoriser sur l'API interne"**. Les
routes que les modules exposent en `oauth2` sont fermées par défaut ; sans
cela, n'importe quel client OAuth de l'instance pourrait appeler n'importe
quelle route de n'importe quel module avec les droits de son utilisateur de
service.

C'est l'étape la plus facile à oublier, parce que son absence ne ressemble pas
à un oubli : le webservice obtient son jeton sans difficulté, la route est bien
trouvée, et l'appel répond malgré tout 401. Seul le journal Dolibarr le dit,
sous la forme `SMARTAUTH_API_AUDIENCE not set - refusing <client_id>`. La fiche
du produit autorisé, à l'étape suivante, signale le cas et renvoie ici.

## Étape 3 : créer la fiche produit autorisé

Revenez dans smartmakersaasbilling, onglet **Produits SaaS autorisés**, puis
**Nouveau produit autorisé** :

| Champ | Valeur |
|---|---|
| Réf. | `scifacturation`, stable et jamais renommée par la suite |
| Libellé | Celui de votre choix |
| Client OAuth smartauth | Le client créé à l'étape 2 |
| Préfixe de références produit | `P-SCIFAC-` |
| URL du webhook de règlement | `https://<votre-webservice>/api/v1/webhooks/dolibarr` |
| Actif | Oui |

![Fiche de création du produit SaaS autorisé dans smartmakersaasbilling](screenshots/exemple-fiche-produit-autorise.webp)

Le **secret de signature du webhook** est généré à l'enregistrement, puis
affiché sur la fiche : transmettez-le au webservice par un canal sûr. C'est
avec lui qu'il vérifiera la signature des notifications de règlement.

Tant qu'aucune fiche active n'existe, **tout appel à l'API est rejeté** :
l'API refuse par défaut, ce qui rend cette étape indispensable.

## Étape 4 : créer le catalogue

Le service a besoin d'une fiche produit par acte facturable. Pour
sci-facturation, cela donne :

- `P-SCIFAC-ABO-T1` à `P-SCIFAC-ABO-T4` pour les abonnements annuels par
  tranche ;
- `P-SCIFAC-MES-T1` à `P-SCIFAC-MES-T4` pour les mises en service au prorata ;
- `P-SCIFAC-MUT` pour le forfait de mutation.

Seul le préfixe est configuré, à l'étape 3 ; le suffixe encode la nature de
l'acte et la tranche, et c'est le webservice qui le déduit. **Ne recopiez pas
ces références à la main** : un caractère erroné passe inaperçu jusqu'au
premier acte à commander, où la commande est refusée faute de produit
correspondant.

Demandez plutôt au webservice son **gabarit de catalogue**, un fichier JSON qui
énumère les fiches qu'il attend. L'annexe en décrit le format ; du côté de
sci-facturation, la commande est `php artisan sci:billing-catalogue-template`.
Sur la fiche du produit autorisé, le bouton **Importer un gabarit de catalogue**
affiche alors, ligne par ligne, ce qu'il créerait, avec le prix proposé par le
webservice, que vous pouvez modifier avant création.

Trois règles gouvernent cet écran :

- **une fiche déjà présente n'est jamais modifiée** : le prix relève d'une
  décision commerciale prise ici, et un gabarit capable de l'écraser ferait du
  fichier la source du barème. Un nouvel import ne peut donc rien détériorer ;
- **seules les références portant le préfixe du client sont reprises** ;
- **rien ne transite par l'API** : le fichier se dépose à la main, si bien
  qu'un jeton compromis reste incapable de créer la moindre fiche produit.

Les fiches créées sont de type **service** et actives à la vente. **Vérifiez
les prix avant de valider** : un service sans prix est refusé à la facturation,
délibérément, et le webservice le signale également de son côté.

Un service qui ne produit pas de gabarit se saisit à la main, dans **Produits
et services**, avec les mêmes caractéristiques.

## Étape 5 : vérifier la configuration de l'instance

Ouvrez l'onglet **Configuration** du module : il signale de lui-même ce qui
manque, à savoir

- les constantes `PAYMENT_SECURITY_TOKEN` et `PAYMENT_SECURITY_TOKEN_UNIQUE`
  du cœur de Dolibarr. Sans elles, le lien de paiement est construit sans sa
  clé de sécurité et la page de paiement refuse la commande ;
- l'absence de module de paiement en ligne, qu'il s'agisse de stripe ou de
  stancer.

![Écran de configuration du module avec les avertissements de constantes manquantes](screenshots/configuration-avertissements.webp)

## Étape 6 : remettre les informations au webservice

Le service appelant reçoit quatre informations, et pas une de plus :

1. le `client_id` et le **secret OAuth** de l'étape 2 ;
2. le **secret de signature du webhook** de l'étape 3 ;
3. l'**adresse de l'API** :
   `https://<votre-dolibarr>/custom/smartmakersaasbilling/pwa/api.php` ;
4. l'**adresse du endpoint de jeton**, qui ne se trouve **pas** sous cet hôte :
   `https://<votre-portail-sso>/oauth/token`.

Ces deux dernières adresses constituent le second piège du parcours. Le portail
SSO de smartauth est exposé sur un sous-domaine dédié, dont la racine web est
son propre répertoire `public/`, et il ne reconnaît un endpoint OAuth que sur
un chemin absolu commençant par `/oauth/`. Toute adresse de la forme
`https://<votre-dolibarr>/custom/smartauth/...` répond 404, en page HTML, sans
autre explication. La valeur exacte se lit sur l'instance :

```bash
curl -s https://<votre-portail-sso>/.well-known/openid-configuration | jq .token_endpoint
```

Deux secrets circulent donc, et **ils sont différents** : celui de l'étape 2
sert à obtenir un jeton, celui de l'étape 3 à vérifier la signature des
notifications de règlement. Les confondre côté webservice fait rejeter chaque
règlement en 401 ; l'accès du client ne se rouvre alors qu'au prochain
rapprochement, au lieu de la minute qui suit son paiement.

Le webservice obtient ses jetons contre le couple client_id/secret, met chaque
jeton en cache pour sa durée de vie diminuée d'une marge, et vérifie la
signature des notifications avec le secret de webhook.

## Vérifier que tout fonctionne

Le test le plus simple consiste à lire le catalogue. Avant la création de la
fiche de l'étape 3, l'appel est refusé ; après, il renvoie les fiches produit
du préfixe du service ainsi que la précision décimale des prix. Le webservice
peut également dérouler son propre écran de vérification de catalogue sur ce
même appel.

## En cas de refus

- **Tout appel refusé (403)** : aucune fiche active ne lie ce client OAuth.
  Revenez à l'étape 3, ou vérifiez que la fiche est bien active.
- **Refus d'authentification (401)** : le couple client_id/secret est erroné,
  ou le client OAuth est inactif, ou il n'est pas confidentiel.
- **401 alors que le jeton a bien été délivré** : le client n'est pas autorisé
  sur l'API interne. C'est le cas le plus déroutant, puisque tout fonctionne
  jusqu'au dernier moment. Le journal Dolibarr porte alors
  `SMARTAUTH_API_AUDIENCE not set` ou `not in SMARTAUTH_API_AUDIENCE
  allow-list`. Retournez sur la fiche du client OAuth (étape 2) et cliquez sur
  **Autoriser sur l'API interne**.
- **404 en page HTML sur la demande de jeton** : l'adresse du endpoint de jeton
  pointe sous l'hôte Dolibarr au lieu du portail SSO (étape 6).
- **Chaque règlement rejeté en 401 côté webservice** : celui-ci vérifie la
  signature avec le secret OAuth au lieu du secret de webhook (étape 6).
- **Erreur de droits à la validation d'une commande** : l'utilisateur de
  service ne dispose pas des droits sur les tiers, les produits, les commandes
  et les factures. Revenez à l'étape 1.
- **Commande refusée faute de produit correspondant** : une référence a été
  recopiée de travers, ou la fiche n'a jamais été créée. L'import de gabarit de
  l'étape 4 répond précisément à ce problème, et le webservice sait indiquer ce
  qui manque.

## Annexe : le format du gabarit de catalogue

Cette annexe s'adresse à qui développe le webservice appelant : elle décrit le
fichier qu'importe l'étape 4, et qu'il vous revient de produire.

```json
{
    "format": "smartmakersaasbilling.catalogue",
    "version": 1,
    "prefix": "P-SCIFAC-",
    "products": [
        {
            "ref": "P-SCIFAC-ABO-T1",
            "label": "Abonnement annuel, 1re location",
            "unit": "an",
            "type": "service",
            "vat_rate": "20",
            "suggested_price_ht": "24.90",
            "price_source": "default"
        }
    ]
}
```

| Champ | Obligatoire | Ce qu'il contient |
|---|---|---|
| `format` | oui | Exactement `smartmakersaasbilling.catalogue`, faute de quoi le fichier est refusé |
| `version` | oui | `1` à ce jour. Une version supérieure est refusée, car un fichier plus récent pourrait porter des champs que ce module laisserait tomber en silence |
| `prefix` | non | Comparé au préfixe du produit autorisé ; s'il en diffère, rien n'est importé |
| `products[].ref` | oui | La référence complète. Si elle sort du préfixe du client, la ligne est écartée et l'écran le signale. Lettres, chiffres, tirets, tirets bas et points |
| `products[].label` | non | Le libellé de la fiche. À défaut, la référence en tient lieu |
| `products[].suggested_price_ht` | non | **Une proposition**, affichée et modifiable avant création. En son absence, la ligne demeure et le prix est créé à zéro |
| `products[].vat_rate` | non | Le taux appliqué à la création. En son absence, zéro |
| `products[].unit` | non | Ignoré à la création, et ce délibérément : Dolibarr note les durées dans sa propre syntaxe (`1y`), qui vous reviendrait ensuite comme unité d'affichage. Conservez votre libellé de votre côté |
| `products[].type` | non | Toutes les fiches sont créées en **service** |
| `products[].price_source` | non | Informatif |

Deux limites encadrent l'import : **100 lignes** par fichier au maximum, et les
doublons de référence sont écartés, l'écran le mentionnant. Un fichier dont
aucune ligne n'est exploitable est refusé en bloc, plutôt qu'importé à moitié.

Rappelons enfin la règle essentielle : **une fiche déjà présente n'est jamais
mise à jour**. Votre gabarit ne peut donc pas devenir la source du barème, et
le prix reste décidé dans Dolibarr.
