Spunto CMS
L'API

É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.

Rien n'est réservé à l'interface. Chaque écriture que fait l'application est une route qu'une clé API peut appeler.

Créer une carte

POST /workspaces/{workspaceId}/collections/{collectionId}/items
{
  "title": "Release notes, 2.4",
  "blocks": { "blocks": [ /* … */ ] },
  "statusId": "<statusId>",
  "parentId": null,
  "properties": { "<fieldKey>": "value" },
  "afterItemId": null
}

Seul title est requis. La collection dans laquelle vous postez devient la liste d'origine de la carte, et l'API compose sa clé lisible à partir du préfixe de cette liste et du numéro de la carte.

Deux façons de nommer une propriété, et ce ne sont pas les mêmes

properties est indexé par la key d'un champ — son nom court et stable. Une définition de vue, elle, référence un champ par son fieldId, son UUID. Les deux figurent sur l'objet Field que renvoie la collection ; les confondre vous vaut un 400 indiquant que la propriété est inconnue, ce qui a au moins le mérite d'échouer vite.

afterItemId place la carte dans l'ordre manuel ; omettez-le et la carte arrive en fin de liste.

Modifier une carte

PATCH /workspaces/{workspaceId}/items/{itemId}

properties est fusionné, pas remplacé. Modifier une propriété laisse les autres intactes — c'est ce qu'on attend d'un PATCH, et c'est ce qui permet à deux clients de modifier sans risque des propriétés différentes de la même carte.

archived: true retire une carte de toutes les vues et de la recherche, de façon réversible. DELETE est définitif et emporte le sous-arbre de la carte.

Le corps : jamais les deux

Envoyez blocks ou body, pas les deux.

blocks pose le document et recalcule sa projection texte. body pose le texte et fait de nouveau dériver le document de celui-ci. Voir le corps des cartes.

Ce que la validation refuse

Les valeurs des propriétés sont vérifiées contre le registre à l'écriture. Un 400 signifie que la valeur n'a atterri nulle part.

  • une clé absente du registre — les propriétés se déclarent avant d'être utilisées, elles ne s'inventent jamais au détour d'une écriture ;
  • une valeur du mauvais type pour sa propriété ;
  • une valeur de select ou de multi_select hors des options déclarées, quand des options sont déclarées ;
  • un person ou un multi_person qui n'est pas membre de cet espace de travail ;
  • un file qui n'est pas un fichier de cet espace de travail ;
  • une url qui n'est pas une URL absolue en http, https ou mailto ;
  • un document en blocs qui enfreint le format — type de bloc inconnu, marque qui ne fait pas partie des cinq, retrait hors limites.

Pourquoi cela ne peut pas être tolérant

Une valeur mal typée ne casse pas sa propre carte. Elle casse toute la requête de chaque vue qui caste cette propriété — un seul nombre erroné et c'est une liste entière qui ne se charge plus, pour tous ceux qui l'ouvrent. Aucune récupération à la lecture ne vaudrait mieux que de refuser l'écriture.

Déplacer des cartes

POST /workspaces/{workspaceId}/items/{itemId}/move
POST /workspaces/{workspaceId}/items/{itemId}/collections
DELETE /workspaces/{workspaceId}/items/{itemId}/collections/{collectionId}

move change la place d'une carte — son parent, son rang, sa liste d'origine.

Les deux autres rattachent une carte à des listes secondaires et l'en détachent : la même carte qui apparaît à un second endroit, pas une copie. Sa liste d'origine, et donc sa clé, ne change pas.

Dupliquer une carte

POST /workspaces/{workspaceId}/items/{itemId}/duplicate

Pas de corps de requête. La copie est renvoyée en entier, et elle se pose juste après l'original dans l'ordre manuel — pas en fin de liste, ce qui, sur une liste de quatre mille cartes, revient à nulle part.

Elle emmène le sous-arbre de la carte, en miroir de DELETE : les sous-tâches sont recréées sous la copie, chacune avec son propre numéro, et rattachées à elle. Les sous-tâches archivées restent où elles sont — une copie naît désarchivée, donc les copier remettrait dans une vue exactement ce que quelqu'un en avait retiré.

Ce qui voyage : le titre (suffixé de (copie) sur la racine seulement), l'icône, le statut, le corps (blocs et texte, tels qu'ils sont stockés), les propriétés et les rattachements à des listes secondaires. Ce qui ne voyage pas :

  • le numéro, et donc la clé. SPR-12 n'est jamais partagé entre deux cartes ;
  • externalSource / externalId. La copie ne vient pas de l'import dont vient l'original, et cette paire est une clé d'idempotence unique ;
  • archivedAt, qui part à null ;
  • createdBy, qui est l'appelant — la même règle que pour toute autre création.

Les propriétés sont copiées telles qu'elles sont stockées, sans être revalidées contre le registre. Elles l'ont été une fois, à l'entrée ; les revérifier refuserait de copier une carte que l'API accepte parfaitement de vous montrer, au motif qu'une option de select a été retirée entre-temps.

Un événement item.created par carte copiée. Celui de la racine porte une clé duplicatedFrom en plus, qui contient l'identifiant de la source — la seule trace du lien, puisque la copie elle-même ne garde aucune référence à son original.

Au-delà de 200 cartes dans le sous-arbre, la route refuse avec un 400 plutôt que de tenir une transaction ouverte sur des centaines d'insertions. Renommer la copie se fait avec un PATCH.

Modifier en masse

POST …/bulk-preview    { definition }
POST …/bulk-update     { definition, patch, expectedCount }

La sélection est une définition de vue — le filtre même que l'utilisateur avait sous les yeux, compilé par le même code. Sans cela, « ce qu'il voyait » et « ce que la mise à jour touche » finiraient par diverger.

La forme est volontairement malcommode à un endroit, et cela vaut la peine de comprendre pourquoi avant d'y brancher une interface :

  • d'abord l'aperçu. bulk-preview renvoie count — ce que le filtre sélectionne à cet instant — ainsi que confirmAbove et max ;
  • au-delà de confirmAbove (200 cartes), expectedCount est obligatoire. Envoyez le nombre que l'utilisateur a réellement vu. Si la réalité a bougé entre-temps, vous recevez un 409 plutôt qu'une modification silencieuse d'un autre ensemble ;
  • au-delà de max (5 000 cartes), l'API refuse. Elle ne met pas de tâche en file et ne prétend pas être atomique. Cette tâche n'existe pas encore, et le dire vaut mieux qu'une requête qui réussit à moitié.

Un seul UPDATE … WHERE, une seule transaction, un événement par carte, et une seule livraison de webhook pour le lot. La réponse porte le batchId, pour que vous puissiez relire exactement ce qu'elle a fait : GET /workspaces/{workspaceId}/events?batchId=….

Des imports que l'on peut rejouer

POST /workspaces/{workspaceId}/import

L'import est la seule écriture conçue autour de l'échec. Non qu'elle soit fragile — mais un import échoue toujours au moins une fois, et un import qu'on ne peut pas rejouer est un import qu'on ne peut pas réparer.

Le contenu venu d'ailleurs porte donc l'identité de son origine : externalSource et externalId, dans des colonnes indexées sur les cartes, les collections et les fichiers. C'est cette paire qui décide entre créer et mettre à jour, et ce qui n'a pas réellement changé n'est pas réécrit — ni écriture, ni événement, ni webhook. Relancer un import réussi ne fait rien, et cela se constate : dans le résultat, unchanged vaut le total.

Pourquoi l'identité externe n'est pas une propriété

Une clé d'idempotence qui vit dans la donnée utilisateur n'est pas une clé d'idempotence. N'importe quelle écriture ordinaire la modifierait, et supprimer un champ l'emporterait avec lui. Elle doit être une colonne, hors de portée de l'édition.

Les imports s'annoncent aussi comme tels. Leurs événements partagent tous un même batchId et portent cause: "import", pour qu'un fil d'activité puisse demander ?cause=edit et ne pas enterrer six mois de travail sous trois mille cartes importées. Les webhooks sont livrés par lot et par type, jamais un appel par carte.

Un lot porte au plus 500 éléments. Au-delà, rappelez la route avec le batchId qui vous a été remis : trois mille cartes restent une seule action dans le journal.

Sur cette page