Lire le contenu
Lire des cartes par l'API REST avec une définition de vue — filtres, tris, curseurs keyset, pages groupées, comptages et recherche.
Lire du contenu, c'est envoyer une définition de vue et recevoir une page keyset en retour.
C'est un POST plutôt qu'un GET, parce qu'un filtre est un arbre, et qu'un
arbre ne survit à une query string sous aucune forme lisible. Malgré le verbe,
ces routes ne font que lire — et c'est précisément pour cela que la table des
scopes qui encadre les apps installées les nomme une à une au lieu de deviner
d'après la méthode.
Une page de cartes
POST /workspaces/{workspaceId}/collections/{collectionId}/query{
"definition": {
"filter": { "op": "eq", "field": { "fieldId": "sys:status" }, "value": "<statusId>" },
"sort": [{ "field": { "fieldId": "sys:order" }, "direction": "asc" }],
"includeSecondaryLists": true
},
"cursor": null,
"limit": 50
}limit vaut 50 par défaut et plafonne à 200. La réponse est :
{ "rows": [ /* Item */ ], "nextCursor": "…" }Bouclez jusqu'à ce que nextCursor vaille null, en le renvoyant chaque fois
comme cursor.
Les cartes d'une page n'ont pas de corps
rows contient des Item, et Item n'a ni body ni blocks. Récupérez une
carte seule avec GET /workspaces/{workspaceId}/items/{itemId} pour obtenir un
ItemDetail, qui les porte. Une page de 50 lignes ne transporte pas 50
documents.
Filtres
Un filtre est soit un prédicat, soit un nœud booléen :
{ "and": [ <filter>, <filter> ] }
{ "or": [ <filter>, <filter> ] }Un prédicat nomme un champ et un opérateur :
| Opérateurs | Forme |
|---|---|
eq neq lt lte gt gte | { op, field, value } |
contains starts_with | { op, field, value } — valeur chaîne |
in not_in | { op, field, values } — tableau |
is_empty is_not_empty | { op, field } |
Un champ s'écrit { "fieldId": "…" } — soit l'identifiant d'une propriété du
registre, soit un champ système comme sys:title, sys:status, sys:order ou
sys:created_at. La collection publie les deux listes, si bien qu'un client n'a
jamais à les coder en dur.
Trois comportements faciles à mal deviner de l'extérieur, et tranchés ici :
neqgarde les valeurs vides. Il se compile enis distinct from, donc une carte sans valeur pour la propriété correspond bien à « différent de X ». Tout autre choix surprend quiconque l'essaie.containsetstarts_withsont littéraux. Les%et_de votre valeur sont échappés. Un terme de recherche est une donnée, pas un motif.inavec une liste vide ne correspond à rien ;not_inavec une liste vide correspond à tout. C'est la lecture qui permet de construire un filtre à partir d'une sélection sans traiter zéro comme un cas particulier.
Sur une propriété tableau (multi_select, multi_person), eq et contains
signifient tous deux l'inclusion, neq signifie « ne contient pas celle-ci », et
in / not_in sont une chaîne de or sur des inclusions. Tout le reste vaut
un 400, avec un message qui nomme les trois qui fonctionnent.
Tris
sort est une liste ordonnée de { field, direction }, asc ou desc.
On ne peut pas trier sur une propriété tableau, et le demander renvoie un 400
plutôt qu'un ordre arbitraire. Un select se trie alphabétiquement, pas dans
l'ordre de ses options — un piège connu.
Curseurs
Un curseur encode la position et le tri pour lequel il a été émis. En renvoyer un avec un tri différent échoue bruyamment plutôt que de rendre une page fausse mais plausible.
Ne les analysez pas. Ne les fabriquez pas. Leur seule promesse est qu'en renvoyer un vous donne la page suivante de la même requête.
Lectures groupées
Deux routes servent les mises en page groupées, et toutes deux compilent la même définition — c'est ce qui fait que la somme des colonnes vaut la vue :
POST …/group-counts — nombre de cartes par groupe
POST …/grouped-query — une page par groupe, chacune avec son propre curseurToutes deux exigent un groupBy dans la définition, et toutes deux renvoient
null comme une clé de groupe ordinaire : les cartes sans valeur.
grouped-query prend les groups que vous voulez afficher, dans cet ordre,
y compris les vides — un GROUP BY ne renvoie que les valeurs qui ont des
lignes, et une colonne vide doit tout de même exister pour qu'on puisse y
déposer une carte (voir pourquoi).
Omettez-les et vous obtenez les groupes que les données se trouvent contenir.
Là, limit s'entend par groupe, pas par réponse, et vaut 20 par défaut.
Chaque groupe revient avec son propre nextCursor et avec count — la taille
totale du groupe, pas le nombre de lignes renvoyées. Charger la suite d'une
colonne, c'est envoyer le curseur de ce groupe dans cursors, indexé par
groupe.
Fenêtres de dates
query comme grouped-query acceptent un range — la fenêtre que regarde un
calendrier ou une chronologie. Elle s'ajoute au filtre de la vue, et n'est
jamais enregistrée avec elle :
{ "range": { "field": { "fieldId": "<fieldId date>" }, "endField": { "fieldId": "<fieldId date>" }, "from": "2026-11-01", "to": "2026-11-30" } }from et to sont inclus tous les deux. Sans endField, une carte est dans la
fenêtre quand sa date y tombe ; avec, quand sa durée la chevauche (une fin
absente, ou antérieure au début, vaut un jour). field doit être une propriété
date ou sys:created_at ; tout le reste est un 400, comme une fenêtre qui
finit avant de commencer. Pour une grille de mois, groupez par la même date et
passez les jours de la grille en groups — la plage borne aussi les compteurs.
Index suggérés
POST …/index-hintsÀ partir d'une définition de vue, l'API indique quels index la serviraient bien, chacun avec la raison pour laquelle il est souhaité. C'est la réponse honnête à « pourquoi cette vue est-elle lente » — un filtre sur une propriété JSON sans index d'expression derrière ne se lit pas du tout comme un filtre qui en a un.
Décrits, pas créés
Cette route informe. Elle ne crée rien : la tâche qui appliquerait les suggestions n'existe pas encore.
Recherche
GET /workspaces/{workspaceId}/search?q=…&limit=…La recherche traverse toutes les listes de l'espace de travail et renvoie deux tableaux, jamais mélangés : les cartes et les collections qui correspondent.
Ce qu'elle couvre est étroit, et c'est dit plutôt que sous-entendu :
- les titres des cartes et les noms des listes — jamais le corps des cartes ;
- une clé lisible exacte (
DOC-12) fait passer cette carte en tête, même si le titre ne correspond pas ; - les cartes archivées sont exclues ;
- les
%et_de votre requête sont échappés.
Elle ne passe pas par le compilateur de vues : elle ne porte ni filtre, ni tri, ni curseur. L'y faire passer aurait exigé un champ virtuel que rien d'autre n'utilise.
S'il vous faut une recherche dans le corps des cartes, aucun index ne la permet — voir ce qu'il ne fait pas.
Utiliser l'API
URL de base, authentification, erreurs et client généré — ce qui vaut pour toutes les routes de l'API REST du CMS headless Spunto.
Écrire du contenu
Créer et modifier des cartes par l'API REST, ce que la validation refuse, l'édition en masse et des imports que l'on peut rejouer sans risque.