Aller au contenu

Conventions de payload v2

En bref

Une seule enveloppe pour tous les succès, un seul format pour toutes les erreurs, une seule façon de cibler un dossier Sage. Ces trois règles sont le contrat stable du protocole v2 : elles ne dépendent d'aucune opération particulière et ne changent pas sans préavis.

Ce sont les règles à implémenter une fois dans votre client. Le détail opération par opération est dans les groupes.

Enveloppe des réponses en succès

Toute réponse 2xx est emballée par le connecteur ; aucun contrôleur ne construit l'enveloppe à la main, la forme est donc rigoureusement identique partout.

{
  "success": true,
  "data": {},
  "meta": {
    "correlationId": "8f2a…",
    "dossiers": ["DEMO"],
    "pagination": { "limit": 50, "offset": 0, "total": 312 },
    "durationMs": 128,
    "warnings": [ { "code": "DOSSIER.IGNORED", "message": "…" } ]
  }
}
Champ de meta Type Contenu
correlationId string identifiant de corrélation, égal à l'en-tête X-Correlation-Id de la réponse
dossiers string[] dossiers Sage réellement interrogés ; null pour les opérations mono-objet
pagination objet { limit, offset, total } sur les lectures par critères paginées ; total compte les lignes de tous les dossiers pour les mêmes filtres
durationMs entier durée de traitement côté connecteur
warnings tableau dégradations non bloquantes { code, message }

warnings remplace les échecs silencieux de l'ancien protocole : l'appel aboutit, mais le client est prévenu. Deux codes y apparaissent aujourd'hui :

Code Situation
DOSSIER.IGNORED une cible inconnue ou non autorisée a été écartée parmi des cibles valides
QUERY.COLUMN_IGNORED une colonne demandée à /v2/query n'existe pas dans un dossier et a été retirée du résultat (un avertissement par dossier, colonnes listées dans message)

PascalCase métier, camelCase sur le fil

La règle prête à confusion, elle mérite d'être dite précisément :

  • côté connecteur, les propriétés des DTO sont déclarées en PascalCase (Reference, CorrelationId) : c'est la convention du code métier ;
  • sur le fil, le sérialiseur les expose en camelCase (reference, correlationId). La convention est épinglée explicitement dans la configuration du connecteur, elle ne dépend pas d'un réglage d'environnement.

Ce choix est aligné sur les erreurs : le format ProblemDetails impose des champs en minuscules, et forcer le PascalCase aurait cassé cette conformité.

Deux exceptions, assumées

  • POST /v2/query et POST /v2/search : le contenu de data est produit par un sérialiseur interne et ses clés sont les noms de colonnes SQL Sage bruts (CT_Num, AR_Ref…), non transformés : c'est un retour fidèle du schéma Sage. Seule l'enveloppe qui les entoure est en camelCase.
  • Le corps de POST /v2/query suit le modèle hérité, en minuscules et snake_case (model, columns, where, order_by, target_base, limit…).

Lectures par critères

Chaque objet métier exposé par un contrôleur dédié se lit par critères, sans pagination en mémoire : les filtres, le tri et la pagination sont transmis à Sage sous forme paramétrée.

Route Rôle
GET /v2/<objet> liste ; paramètres targets, limit, offset, sort, where ; résultat indexé par dossier
POST /v2/<objet>/recherche même lecture, critères dans le corps : { "where": […], "orderBy": […], "limit": …, "offset": … } (clés insensibles à la casse)
GET \| POST /v2/<objet>/count comptage selon les filtres seuls ; data = { "<dossier>": n }
  • sort : colonnes SQL séparées par des virgules, préfixe - pour l'ordre décroissant, Table.Colonne accepté. Sans tri, l'ordre par défaut suit cbMarq.
  • where : tableau JSON de filtres { column, operator, value | values, connector?, table? }.
  • Opérateurs : =, <>, !=, <, <=, >, >=, like, not like, in, not in (values), is null, is not null, between (values à deux éléments). Le connecteur AND (par défaut) ou OR relie un filtre au précédent, sans groupes imbriqués.
  • Colonnes : noms SQL de la table de l'objet (CT_Num, AR_Ref, DO_Piece…), jamais de SQL brut. Un filtre qui vise une autre table que celle lue est ignoré.
  • Erreurs : critères invalides (opérateur hors liste, identifiant non conforme, in vide, between sans deux valeurs, JSON illisible, limit ou offset négatifs) : 400 VALIDATION.PARAMETER, avant tout appel à Sage.
curl -s -G "$HOTE/v2/tiersbanque" -H "Authorization: Bearer $JETON" \
  --data-urlencode "targets=DEMO" \
  --data-urlencode "limit=20" \
  --data-urlencode "sort=-cbMarq" \
  --data-urlencode 'where=[{"column":"CT_Num","operator":"like","value":"C%"}]'

Corps des requêtes

Écritures par DTO

Les écritures d'objets métier (/v2/tiers, /v2/articles, /v2/entites/{entite}, et toutes les routes /v2/<objet>) attendent un JSON partiel du DTO.

Règle Conséquence pour l'intégrateur
Reconnaissance par nom de propriété DTO, insensible à la casse {"reference": …} et {"Reference": …} sont équivalents
Les noms de colonnes SQL Sage ne sont pas reconnus {"AR_Ref": …} est ignoré : utilisez reference
Liaison partielle sûre seules les propriétés présentes dans le corps sont marquées comme modifiées ; les autres champs Sage ne sont pas écrasés
Aller-retour garanti un objet lu en camelCase peut être réécrit tel quel

C'est le point le plus important pour éviter les dégâts : n'envoyez que les champs que vous voulez modifier. Envoyer un objet complet relu puis modifié fonctionne aussi, mais réécrit tout.

Les écritures des contrôleurs par objet et de /v2/entites renvoient un ResultatEcriture : { cle, proprieteCle, resultat, entite }. cle est la clé attribuée par Sage quand il en renvoie une (numéro de pièce d'un document, par exemple), sinon la clé portée par le DTO ; entite est le DTO soumis complété de cette clé, pas une relecture de Sage. Les contrôleurs /v2/articles et /v2/tiers/… renvoient, eux, l'objet relu après écriture.

Valeurs forcées

Les filtres de l'appelant peuvent forcer la valeur d'une colonne à l'enregistrement. Le connecteur l'applique avant d'écrire, sur POST et PUT /v2/<objet>, POST /v2/<objet>/save, POST /v2/entites/{entite}, les articles et les tiers :

  • en création, la valeur forcée remplace la valeur envoyée ;
  • en modification, elle n'est écrite que si la colonne est vide en base ou porte déjà cette valeur ; sinon, la valeur de la base est conservée et la valeur envoyée pour cette colonne est ignorée ;
  • jamais en suppression, et seulement sur l'objet principal, pas sur les lignes imbriquées d'un document.

Pour connaître la valeur réellement enregistrée, relisez l'objet après l'écriture.

Documents commerciaux

Les en-têtes et les lignes de documents passent par des objets spécialisés par domaine, qui imposent le domaine et ses types, exigent le numéro de pièce en modification et limitent leurs lectures au domaine :

Domaine En-têtes Lignes
Vente /v2/documentvente /v2/lignedocumentvente
Achat /v2/documentachat /v2/lignedocumentachat
Stock /v2/documentstock /v2/lignedocumentstock
Interne /v2/documentinterne /v2/lignedocumentinterne

/v2/document et /v2/lignedocument redirigent vers ces objets. Les droits sur les documents (type, statut, lignes, échéancier) s'appliquent à toutes ces écritures. L'échéancier complet d'un document se remplace par PUT /v2/documents/{doType}/{doPiece}/echeances (une liste vide vide l'échéancier). Les transformations et l'application des barèmes se font en POST ; leurs anciennes formes GET sont marquées obsolètes dans le contrat.

Écritures comptables

Route Rôle
POST /v2/ecriturepiece saisit une pièce équilibrée : { codeJournal, date, numeroPiece, referencePiece?, lignes: [ … ] } ; au moins deux lignes, total des débits égal au total des crédits, sinon 400 VALIDATION.MODEL avant tout appel à Sage ; numéro de pièce obligatoire ; réponse 201 avec cle = numéro de pièce, proprieteCle = NumeroPiece
GET /v2/ecriturepiece/{codeJournal}/{numeroPiece} lit les écritures de la pièce (404 si aucune)
DELETE /v2/ecriturepiece/{codeJournal}/{numeroPiece} supprime la pièce
GET, /recherche, /count et PUT /v2/ecriture lecture et modification d'une écriture, sans création ni suppression (405 sur POST et DELETE)

Chaque ligne de pièce porte un compte général (compteG) ou un tiers (numeroTiers), un sens (0 débit, 1 crédit), un montant positif et un libellé, plus des champs facultatifs (échéance, référence, règlement, devise).

La réponse 201 ne rend pas les numéros internes des lignes créées : lisez la pièce par GET /v2/ecriturepiece/{codeJournal}/{numeroPiece}. Sage n'enregistre pas la pièce d'un bloc : les lignes sont écrites une à une et, si une ligne est refusée, les lignes déjà écrites de la pièce sont retirées. Un refus de Sage (date hors exercice, compte inconnu…) répond 422 SAGE.COM.PROCESS, avec le message de Sage dans sageDetail.

En modification (PUT /v2/ecriture, corps { "id": <numéro interne de l'écriture>, … }), seules les colonnes non structurelles sont modifiables : libellé, références, échéance, date de saisie, quantité, devise, taxe, contrepartie, informations libres, et à partir de la version 11 de Sage la pièce de trésorerie et la date de rapprochement. Une colonne structurelle (compte, tiers, montant, sens, journal, date) renvoyée à l'identique est acceptée ; modifiée, elle est refusée en 422 SAGE.COM.PROCESS, avec la liste des colonnes dans sageDetail. Pour la changer, supprimez puis ressaisissez la pièce. Les refus de Sage (écriture lettrée, période close…) arrivent aussi en 422 avec le message de Sage.

Ces routes exigent le droit des profils sur les écritures comptables ; un refus répond 403 AUTH.FORBIDDEN.

Modèles hérités

Route Modèle attendu
POST /v2/query champs en minuscules / snake_case : model, alias, columns, columns_array, where, join, order_by, aggregate, computed_columns, limit, offset, unfilter, parameters, target_base, mediaSize, escapeDate, sqlQuery
POST /v2/search models, searchTerm, limit

La liaison est insensible à la casse. Ces modèles sont partagés avec la surface historique : leur casse n'est pas alignée sur le reste de la v2, c'est une dette assumée jusqu'au retrait de celle-ci.

POST /v2/query : validation stricte

Même moteur que la route historique, mais aucun repli silencieux : là où l'ancienne route ignore une anomalie ou rend [], la v2 refuse en 400 QUERY.INVALID, avec un detail qui localise l'anomalie.

Cas Réponse v2
Champ JSON inconnu (group_by, table, filters…) 400, avec le nom attendu (table devient model ; le groupement est implicite)
Table ou vue inconnue (model, join[].model) 400
Colonne demandée absente du dossier (columns, columns_array, join[].columns, colonnes calculées) 200 : colonne retirée du résultat et avertissement QUERY.COLUMN_IGNORED ; 400 seulement si aucune colonne demandée n'existe
Colonne inconnue dans un where, un order_by, une jointure ou un agrégat 400 : la retirer changerait le sens du résultat
Opérateur ou connecteur inconnu, parenthèses déséquilibrées, clause mal formée 400, avec la position (where[2], join[0].where[1]…)
Erreur SQL à l'exécution 400 avec le message nettoyé ; base injoignable 503 SAGE.COM.UNAVAILABLE, délai dépassé 504
Corps absent ou non-objet 400 VALIDATION.MODEL

Une colonne peut exister dans un dossier et pas dans un autre (version de Sage, informations libres) : c'est pourquoi elle est retirée et signalée au lieu de faire échouer toute la requête multi-dossier.

Clauses where : ["col", "valeur"] (égalité), ["col", "op", "valeur"] ou ["col", "op", "valeur", "connecteur"]. Le connecteur (and par défaut, or, and not, or not) relie la clause à la précédente. Les éléments d'une clause sont des chaînes ("6", pas 6). Opérateurs, insensibles à la casse : =, <, >, <=, >=, <>, !=, like, not like, in, is, is not, &, |, ^, ~. Pour in, les valeurs sont séparées par des virgules, sans parenthèses ("A,B,C") ; pour is null, écrire ["col", "is", "null"].

Groupes : ["("] ouvre un groupe (ou ["(", "and"], ["(", "or"] pour le relier à ce qui précède), [")"] le ferme.

"where": [["CT_Type","=","0"], ["(","and"], ["CT_Intitule","like","%a%"], ["CT_Ville","like","%b%","or"], [")"]]

Groupement : il n'y a pas de group_by. Dès qu'un aggregate est présent, le regroupement porte sur toutes les colonnes simples demandées.

Le champ sqlQuery (requête SQL libre) n'est jamais exécuté tel quel : voir Sécurité, requêtes SQL brutes.

Drapeau des écritures

Les écritures v2 (POST, PUT, DELETE) sont derrière une option globale du connecteur (AutoriserEcrituresV2, interrupteur Autoriser les écritures API v2 de l'écran Serveur Web, redémarrage du service requis), activée par défaut. Désactivée, toute écriture répond 503 avec le code SAGE.COM.UNAVAILABLE, avant toute validation du corps. Il n'existe aucun réglage par ressource. Une opération que la couche d'interopérabilité refuse répond 409 (SAGE.READONLY), avec la voie à utiliser dans detail et sageDetail.

Paramètres transverses

Paramètre Rôle Détail
targets dossier(s) Sage cible Forme canonique v2, séparateur virgule : ?targets=DEMO,DEMO2. Sans valeur, tous les dossiers autorisés. Alias hérités acceptés : target, target_base, base, bases (séparateurs , ou \|). Toujours filtré par les autorisations de l'utilisateur.
limit nombre maximum de lignes 0 = illimité
offset décalage pagination, à combiner avec limit

Ils sont résolus au même endroit pour toute l'API, et honorés par /v2/query, /v2/search, les indicateurs, la balance, le cadencier, les tarifs, le tableau de bord et les listes d'objets (GET /v2/<objet>), dont le résultat est alors indexé par dossier avec meta.dossiers renseigné.

?targets= prime sur le corps

Quand la requête porte ?targets= (ou un alias), il fait foi. Une cible écrite dans le corps (target_base de /v2/query, TargetBase de POST /v2/documents/transform/{type}, dossier de POST /v2/documents/pdf/{type}/{piece}) n'est acceptée que si elle en fait partie (casse indifférente) ; sinon, la réponse est 400 DOSSIER.CONFLICT. Sans ?targets=, le corps reste lu.

Écriture = un seul dossier

Une écriture doit viser un dossier unique. Plusieurs valeurs dans targets renvoient DOSSIER.AMBIGUOUS (400) ; aucune valeur alors que l'utilisateur a plusieurs dossiers autorisés renvoie DOSSIER.REQUIRED (400) ; aucune cible valide renvoie DOSSIER.NOT_AUTHORIZED (403). En lecture, le dossier implicite reste toléré.

Erreurs

Format

Une réponse ≥ 400 ne passe pas par l'enveloppe. Elle sort en application/problem+json (ProblemDetails, RFC 9457), avec deux extensions SenSaaS : code et correlationId.

{
  "type": "https://sensaas.fr/errors/sage-readonly",
  "title": "Objet en lecture seule",
  "status": 409,
  "detail": "…",
  "instance": "/v2/tierstype",
  "code": "SAGE.READONLY",
  "correlationId": "8f2a…"
}

Les erreurs venant d'un processus métier Sage, ou d'un refus de la couche d'interopérabilité, portent en plus une extension sageDetail : le message renvoyé par Sage ou par la couche elle-même (par exemple "L'article X existe déjà !").

Les refus d'authentification 401 portent aussi l'en-tête WWW-Authenticate: Bearer, complété de error="invalid_token" dès qu'un jeton a été présenté : c'est le signal pour renouveler le jeton.

Aucune trace d'exécution n'est exposée ; sur une erreur 5xx le détail est masqué et ne se retrouve que dans les journaux du connecteur, repérable par le correlationId.

Branchez-vous sur code, pas sur title

title et detail sont des textes destinés à l'humain et peuvent évoluer. code appartient à une taxonomie stable : un code peut être ajouté, jamais renommé ni supprimé.

Taxonomie CodesErreur

Code Situation
AUTH.INVALID_CREDENTIALS identifiants refusés
AUTH.INVALID_TOKEN jeton absent, mal signé ou refusé (401)
AUTH.TOKEN_EXPIRED jeton expiré (401) : le renouveler, puis rejouer l'appel
AUTH.UTILISATEUR_INCONNU jeton du serveur d'identité valide, sans utilisateur actif du connecteur pour son email (403) : ne pas renouveler le jeton
AUTH.FORBIDDEN droit retiré à l'appelant (403) : opération de l'API, document, écritures comptables ; detail nomme le droit
Code Situation
VALIDATION.MODEL corps absent, vide, invalide, ou pièce comptable non équilibrée (400)
VALIDATION.PARAMETER paramètre de requête invalide, dont les critères de lecture (400)
Code Situation
DOSSIER.UNKNOWN dossier inexistant (404)
DOSSIER.NOT_AUTHORIZED dossier non autorisé à l'utilisateur (403)
DOSSIER.AMBIGUOUS plusieurs dossiers alors qu'un seul est attendu (400)
DOSSIER.REQUIRED écriture sans cible explicite alors que plusieurs dossiers sont autorisés (400)
DOSSIER.CONFLICT cible du corps absente de ?targets= (400)
DOSSIER.IGNORED cible écartée parmi des cibles valides : avertissement, pas une erreur
Code Situation
SAGE.NOT_FOUND référence, pièce ou clé introuvable (404)
SAGE.LOCK.RECORD enregistrement verrouillé (409)
SAGE.LOCK.CONTROLLER contrôleur Sage verrouillé
SAGE.COM.PROCESS échec d'un processus métier ou refus de Sage : transformation, lettrage, colonne non modifiable… (422)
SAGE.COM.UNAVAILABLE Sage injoignable, ou écritures v2 désactivées (503)
SAGE.READONLY opération refusée par la couche d'interopérabilité (409) ; sageDetail indique la voie à utiliser
SAGE.IN_USE suppression d'un client ou d'un fournisseur qui porte des documents, lignes, écritures ou règlements (409)
Code Situation
CONFIG.INCOMPLETE paramétrage du connecteur manquant, par exemple les modèles PDF (409) ; ou installation incomplète, composant absent du dossier du connecteur (503)
PDF.GENERATION échec de génération d'un PDF à partir de son modèle (422) ; detail donne l'étape
Code Situation
LICENCE.EXPIRED licence du connecteur expirée
LICENCE.QUOTA quota de licence atteint
QUERY.INVALID requête /v2/query refusée (400)
QUERY.FORBIDDEN requête interdite par les filtres
QUERY.COLUMN_IGNORED colonne inconnue retirée du résultat : avertissement dans meta.warnings
INTERNAL erreur non prévue ; le détail est dans les journaux, repérable par correlationId

Les URI du champ type sont construites sur la base https://sensaas.fr/errors/.

SageErreur, côté connecteur

Les erreurs métier remontées par la passerelle Sage voyagent dans une exception dédiée qui porte déjà son code de taxonomie et son statut HTTP. Le gestionnaire d'exceptions global la traduit directement en ProblemDetails. Concrètement, pour l'intégrateur : le statut HTTP et le code sont décidés au plus près de Sage, ils sont donc fiables et stables d'une version à l'autre.

Fabrique Code Statut
Introuvable SAGE.NOT_FOUND 404
Verrou d'enregistrement SAGE.LOCK.RECORD 409
Processus métier en échec SAGE.COM.PROCESS 422 (+ extension sageDetail)
Sage indisponible SAGE.COM.UNAVAILABLE 503
Lecture seule SAGE.READONLY 409 (+ extension sageDetail)
Objet utilisé SAGE.IN_USE 409
Dossier non autorisé DOSSIER.NOT_AUTHORIZED 403
Dossier ambigu DOSSIER.AMBIGUOUS 400
Dossier requis DOSSIER.REQUIRED 400
Conflit de dossier DOSSIER.CONFLICT 400
Dossier inconnu DOSSIER.UNKNOWN 404
Requête invalide QUERY.INVALID 400
Corps invalide VALIDATION.MODEL 400
Configuration incomplète CONFIG.INCOMPLETE 409
Installation incomplète CONFIG.INCOMPLETE 503
Génération PDF PDF.GENERATION 422

Deux voies d'accès aux objets métier

L'API v2 offre deux chemins complémentaires vers la même surface Sage.

Un contrôleur par objet métier, rangé par catégorie et étiqueté Catégorie.Objet : c'est ce qui donne les groupes de cette documentation (Catalogue.Article, Tiers.TiersBanque, Comptabilite.Journal…). La plupart sont générés à partir de l'inventaire de la bibliothèque API100 ; chaque objet porte le nom de sa table Sage.

Routes : GET | POST | PUT | DELETE /v2/<objet>, plus POST /v2/<objet>/save, POST /v2/<objet>/recherche et GET | POST /v2/<objet>/count. Certains objets sont en mode édition (lecture et PUT seulement, comme /v2/ecriture), d'autres en lecture seule.

C'est la voie privilégiée : routes stables, clé dans l'URL pour les objets majeurs (/v2/articles/{reference}, /v2/tiers/clients/{ctNum}), DTO typé.

Repli universel pour la longue traîne et la découverte.

Route Rôle
GET /v2/entites catalogue des objets accessibles : aucun accès à Sage, idéal pour explorer
GET /v2/entites/{entite}/{cle} lecture par clé, pour les objets qui l'exposent (Article, Tiers client et fournisseur, Document achat et vente) ; sinon 400, passer par /v2/query
POST /v2/entites/{entite} création ou mise à jour, derrière le drapeau des écritures

POST /v2/entites/{entite} applique les mêmes droits métier que les contrôleurs par objet (documents, lignes, échéances, écritures comptables) et les valeurs forcées. Une écriture comptable isolée n'y est pas créée : sans identifiant, POST /v2/entites/ecriture répond 409 et renvoie vers POST /v2/ecriturepiece.

Routes historiques

Toutes les routes hors /v2 (/Query, /Search, /Object, /doc, /Media, /kpi, /dashboard…) sont obsolètes : conservées pour la compatibilité avec l'ancienne version du site, elles ne doivent plus être appelées par une nouvelle intégration.

  • Leur contrat (/swagger/v1/swagger.json) marque toutes les opérations deprecated.
  • Chaque réponse porte les en-têtes Deprecation: true et Sunset (RFC 8594), avec la date de retrait prévue, le 1er janvier 2027, révisable selon le calendrier de déploiement.
  • Ne sont pas concernées : /swagger et /test (outillage), /auth (point d'entrée commun aux deux versions) et quelques routes techniques locales.

Voir aussi

Documentation de SenSaaS v2. Chaque page est écrite à partir du produit livré ; ce qui reste à vérifier y est signalé.