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/queryetPOST /v2/search: le contenu dedataest 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/querysuit le modèle hérité, en minuscules etsnake_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.Colonneaccepté. Sans tri, l'ordre par défaut suitcbMarq.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 connecteurAND(par défaut) ouORrelie 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,
invide,betweensans deux valeurs, JSON illisible,limitouoffsetnégatifs) :400VALIDATION.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érationsdeprecated. - Chaque réponse porte les en-têtes
Deprecation: trueetSunset(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 :
/swaggeret/test(outillage),/auth(point d'entrée commun aux deux versions) et quelques routes techniques locales.