Aller au contenu principal

API REST

Accès programmatique à sol2flow avec des clés d’API personnelles. Une clé agit au nom de la personne qui l’a créée, avec exactement ses permissions : l’API est une autre porte d’entrée soumise aux mêmes contrôles de permissions que l’application, jamais un moyen de les contourner.

  • URL de base : <APP_URL>/api/v1
  • Référence : chaque instance a sa propre Swagger UI à <APP_URL>/api/docs et le document OpenAPI 3.1 à <APP_URL>/api/v1/openapi.json, toujours à jour avec la version de l’instance (un administrateur peut les désactiver, voir Paramètres de l’instance). Dans Swagger UI, Authorize accepte une clé d’API et Try it out appelle l’API depuis votre navigateur.
  • Cette page explique les concepts ; Swagger UI liste chaque endpoint avec ses paramètres et ses schémas de réponse.
  • Pour utiliser sol2flow depuis un assistant IA (Claude, Cursor, VS Code, …), voir le serveur MCP.

Authentification​

  1. Dans l’application : menu du compte (en bas à gauche) → Clés d’API → Créer une clé d’API. Donnez-lui un nom, une durée de validité (30 jours, 90 jours (par défaut), 1 an, jamais) et son accès : Accès complet (par défaut) ou Lecture seule (Portées). Créer une clé exige une connexion datant de moins de 10 minutes (sinon « Confirmez votre identité »). La clé (sf_<prefix>_<secret>) n’est affichée qu’une seule fois : copiez-la dans votre coffre à secrets.
  2. Envoyez-la avec chaque requête :
curl -H "Authorization: Bearer $SOL2FLOW_KEY" https://flow.example.com/api/v1/me
Changement : rôles staff

GET /me renvoie staff_role ("OWNER", "ADMIN", "SUPPORT", "AUDITOR" ou null) au lieu du booléen is_instance_admin, qui a été supprimé. Les scripts qui testaient is_instance_admin doivent tester staff_role !== null, ou le rôle précis.

  • Seul l’en-tête Authorization: Bearer est accepté. Les cookies sont ignorés : une requête qui ne porte que le cookie de session du navigateur reçoit un 401. Il n’y a donc aucune surface CSRF, et un script ne peut pas emprunter une session de navigateur.
  • Une clé cesse de fonctionner quand elle expire, quand elle est révoquée (par vous, ou par un Propriétaire / Admin de l’instance), quand votre compte est désactivé ou supprimé, tant que votre compte ne peut pas se connecter (par exemple une inscription dont l’adresse e-mail n’est pas encore confirmée), et tant que les clés d’API sont désactivées sur l’instance (403 api_disabled).
  • Jusqu’à 25 clés actives par compte. Chaque création et révocation de clé envoie un e-mail de sécurité au compte.
  • La liste affiche pour chaque clé son nom, sf_<prefix>…<last 4>, son accès (Accès complet / Lecture seule), sa date de création, son expiration et sa dernière utilisation (mise à jour au plus une fois par minute).

Portées​

Une clé a un accès complet (tout ce que son propriétaire peut faire : c’est la valeur par défaut, et le cas de toutes les clés créées avant l’existence des portées) ou est en lecture seule :

  • Une clé en lecture seule ne peut appeler que GET et HEAD : lister, lire, rechercher, télécharger des pièces jointes (GET /files/{id}). Toute autre méthode répond 403 insufficient_scope, avant toute autre action.
  • Le contrôle est centralisé pour tous les endpoints.
  • Certaines lectures exigent aussi un accès complet : les endpoints de webhooks (leurs URL et journaux de livraison relèvent de la configuration d’administration).
  • Le document OpenAPI indique la portée requise par chaque opération dans x-required-scope (read / full), et la description du schéma bearer explique la règle.
  • Une portée n’élargit jamais rien : une clé en lecture seule ne voit toujours que ce que son propriétaire peut voir.

Conventions​

  • JSON en entrée et en sortie, en snake_case. Les horodatages sont en ISO 8601 et en UTC (2026-09-25T09:30:00.000Z) ; les jours calendaires sont au format YYYY-MM-DD. Les identifiants sont des UUID ; les espaces sont désignés par leur slug (comme dans /w/<slug>).
  • Succès : 200 avec la ressource, 201 pour les créations, 202 pour les invitations (envoyées par e-mail), 204 (sans corps) pour les suppressions. Les écritures renvoient la ressource mise à jour, un second GET est donc rarement nécessaire.
  • Les personnes sont représentées par { id, name, username }, jamais par leur e-mail (seul GET /me renvoie le vôtre), comme dans les sélecteurs de l’application. Exception, comme dans l’application : les membres du staff qui voient les e-mails (Propriétaire, Admin, Support ; voir Rôles staff) reçoivent email sur les membres et les personnes des tableaux ; les adresses des invitations suivent la règle de masquage ci-dessous.
  • Le texte enrichi (descriptions de tâches, commentaires) est accepté en texte brut (un paragraphe par ligne) ou sous forme de document JSON Tiptap ({"type":"doc","content":[…]}, le format de l’éditeur). Les deux sont nettoyés exactement comme une saisie dans l’application ; les mentions ({"type":"mention","attrs":{"id":"<user id>","label":"Name"}}) ne sont conservées que pour les personnes qui peuvent ouvrir le tableau, et celles-ci sont notifiées. Les réponses fournissent le JSON (description, doc) et le texte brut (description_text, text). Le Markdown fonctionne dans les deux sens : envoyez "format": "markdown" avec une chaîne, et ajoutez ?format=markdown pour le récupérer (voir Markdown).
  • Positions : les listes, les tâches et les éléments de checklist se déplacent entre leurs voisins, previous_id (l’élément qui se retrouve juste avant) et next_id (juste après). Déplacer une tâche sans l’un ni l’autre la place en bas de la liste.
  • Les écritures créent les mêmes entrées d’activité et les mêmes notifications que l’application, avec vous comme auteur.

Pagination​

Les endpoints de liste acceptent limit (1–100, 50 par défaut) et cursor, et répondent

{ "data": [ … ], "next_cursor": "eyJ1Ijoi…" }

Renvoyez next_cursor comme cursor jusqu’à ce qu’il vaille null. Les curseurs sont opaques (ne les construisez pas) ; un curseur mal formé donne un 400. GET /tasks est trié par dernière modification, de la plus ancienne à la plus récente : updated_since=<timestamp> combiné au curseur permet donc une synchronisation incrémentale.

Erreurs​

{ "error": { "code": "title_required", "message": "Title is required.", "field": "title" } }

code est stable : c’est sur lui qu’il faut brancher votre logique ; message est en anglais et destiné aux humains ; field désigne le champ concerné quand il est connu ; max donne la limite pour too_long / file_too_large / api_key_limit ; retry_after (en secondes) accompagne les 429 ; plan_limit est accompagné de kind (ce qui est épuisé, par ex. seats ou guests), limit et used.

StatutQuandCodes typiques
400Le corps n’est pas du JSON ; un paramètre de chemin ou de requête est invalide (UUID ou curseur incorrect)invalid_input
401Clé absente, mal formée, inconnue, expirée ou révoquée ; compte désactivéunauthenticated
403Votre rôle ne le permet pas ; la clé est en lecture seule ; clés d’API, webhooks ou liens publics désactivés ; non inclus dans votre forfaitforbidden, insufficient_scope, api_disabled, webhooks_disabled, public_links_disabled_workspace, plan_feature, guest_viewer_only
404N’existe pas ou vous ne pouvez pas l’ouvrir (l’API ne dit jamais lequel des deux)not_found
405Chemin connu, autre méthodemethod_not_allowed
409L’état actuel ne le permet pasboard_archived, label_scope, last_board_admin, last_admin_leave, last_private_admin, oidc_managed, instance_admin_leave, conflict, upload_incomplete, webhook_limit, plan_limit
413Fichier (ou corps JSON > 1 Mo) trop volumineuxfile_too_large
422Échec de la validationtitle_required, name_required, too_long, dates_order, duration_invalid, invalid_input, webhook_url_blocked, webhook_url_unresolved, webhook_url_invalid, events_required, …
429Limite de débit atteinte : attendez Retry-After secondesrate_limited
500Un bug ; aucune information interne n’est divulguéeinternal

Limites de débit​

Par clé : 600 requêtes / 10 minutes, dont au plus 120 écritures (POST, PUT, PATCH, DELETE). Les requêtes sans clé valide sont limitées par IP client (30 / 10 minutes ; ensuite 429 au lieu de 401). Les appels d’API comptent aussi dans les mêmes limites par utilisateur que l’application (commentaires 30 / min, recherche 240 / min, envois de fichiers 300 / h, invitations 100 adresses / h par personne qui invite et 200 / h par espace, révocations 100 / h). Au-delà d’une limite : 429 avec un en-tête Retry-After (en secondes) et retry_after dans le corps.

Endpoints​

La référence complète et toujours à jour (paramètres, corps, schémas de réponse, exemples) est la Swagger UI de votre instance, à /api/docs. Vue d’ensemble :

DomaineEndpoints
CompteGET /me : votre profil, y compris staff_role (OWNER · ADMIN · SUPPORT · AUDITOR · null) et time_zone (votre fuseau IANA enregistré dans Paramètres → Préférences, par ex. Europe/Berlin ; null = automatique, celui du navigateur ; en lecture seule : les horodatages de l’API sont toujours en UTC) et, depuis l’API 1.6.0, email_verified (true une fois votre adresse e-mail confirmée ; false sans adresse e-mail) et, à partir de l’API 1.8, api_key : la clé utilisée pour la requête (name, prefix, scope full / read, expires_at ; jamais son secret)
EspacesGET /workspaces · GET /workspaces/{slug} · GET /workspaces/{slug}/search?q=
TableauxGET /workspaces/{slug}/boards (archived, q) · POST /workspaces/{slug}/boards · GET /boards/{id} (avec les listes) · PATCH /boards/{id} (nom, accès, rôle par défaut) · POST /boards/{id}/archive · POST /boards/{id}/restore
ListesPOST /boards/{id}/lists · PATCH /boards/{id}/lists/{listId} · POST …/lists/{listId}/move · DELETE …/lists/{listId}
ÉtiquettesGET /workspaces/{slug}/labels · GET /boards/{id}/labels · POST /boards/{id}/labels
TâchesGET /tasks (board_id ou workspace ; list_id, assignee, label, q, due_from, due_to, updated_since, archived=true pour inclure les tâches archivées) · GET /tasks/{id} · POST /tasks · PATCH /tasks/{id} · POST /tasks/{id}/move · POST /tasks/{id}/move-board (board_id, list_id, position : un autre tableau du même espace ; nouvelle clé, l’ancienne redirige dans l’application) · POST /tasks/{id}/archive · tâches : estimate_minutes + estimate_display (temps de travail, Accept-Language), écriture avec estimate: "3d" ou estimate_minutes · POST /tasks/{id}/unarchive (une tâche archivée est en lecture seule : 409 task_archived) · DELETE /tasks/{id} · POST /tasks/{id}/restore · PUT /tasks/{id}/labels · PUT/POST /tasks/{id}/assignees, DELETE /tasks/{id}/assignees/{userId} · idem pour members
ChecklistPOST /tasks/{id}/checklist · PATCH /checklist/{itemId} (texte, terminé) · POST /checklist/{itemId}/move · DELETE /checklist/{itemId}
Liens entre tâchesGET /tasks/{id}/links (uniquement les liens vers des tâches que vous pouvez ouvrir) · POST /tasks/{id}/links (kind : relates_to, blocks, blocked_by, duplicates, duplicated_by – vu depuis cette tâche ; task_id : l’autre tâche, même espace de travail ; 409 link_exists, 422 link_self) · DELETE /tasks/{id}/links/{linkId}
CommentairesGET /tasks/{id}/comments (avec les réponses ; `order=newest
TempsGET /time-entries?workspace= (board_id, task_id, user_id, from, to) · POST /time-entries · PATCH /time-entries/{id} · DELETE /time-entries/{id} · GET /timer · POST /timer/start · POST /timer/stop
Pièces jointesGET /tasks/{id}/attachments · GET /files/{fileId} · envois de fichiers, ci-dessous
NotificationsGET /notifications (unread) · PATCH /notifications/{id} (read) · POST /notifications/read-all
PartageGET /boards/{id}/sharing · PATCH /boards/{id}/sharing (accès, rôle par défaut, lien public) · POST /boards/{id}/members · PATCH/DELETE /boards/{id}/members/{userId} · GET /boards/{id}/invitations
MembresGET /workspaces/{slug}/members (role, q, include=invitations) · POST /workspaces/{slug}/members · PATCH/DELETE /workspaces/{slug}/members/{userId} · POST /workspaces/{slug}/leave
InvitationsPOST /workspaces/{slug}/invitations (202) · GET /workspaces/{slug}/invitations · DELETE /invitations/{id} · GET /me/invitations · POST /me/invitations/{id}/accept · POST /me/invitations/{id}/decline
WebhooksGET/POST /workspaces/{slug}/webhooks · GET/PATCH/DELETE …/webhooks/{id} · POST …/{id}/rotate-secret · POST …/{id}/ping (202) · GET …/{id}/deliveries (status) · GET …/deliveries/{deliveryId} · POST …/deliveries/{deliveryId}/redeliver (202)

Saisies de temps : started_at est un instant ; le début est conservé à la minute près, comme pour les saisies faites dans l’application. Indiquez soit duration_sec (60 … 86 400), soit ended_at. La visibilité suit les règles de l’application (Suivi du temps). Les invités reçoivent 404 pour GET /time-entries, car ils n’ont pas de feuilles de temps.

Commentaires : les invités ne peuvent utiliser les points d’accès des commentaires (écrire, répondre, modifier et supprimer les leurs) que là où les commentaires des invités sont activés ; sinon 403.

Partage, membres et invitations​

Les mêmes règles que la boîte de dialogue Partager, la page Membres et la bannière d’invitation (Membres et partage).

EndpointQuiRemarques
GET /boards/{id}/sharingtoute personne qui peut ouvrir le tableauaccès, rôle par défaut, public_link.enabled ; pour les admins du tableau, également public_link.url, expires_at, password (true si un mot de passe est défini), visibility, allowed et blocked_by (plan ou workspace). people : les membres explicites du tableau et les admins de l’espace (locked), invités signalés
PATCH /boards/{id}/sharingadmins du tableauaccess, default_role, public_link : true crée / conserve le lien, false le supprime, ou un objet avec les paramètres (ci-dessous)
POST /boards/{id}/membersadmins du tableau{user_id, role?, workspace_role?} ; sans role : VIEWER pour les invités, EDITOR pour les autres. Les invités obtiennent au plus VIEWER (403 guest_viewer_only). Une personne de l’espace, ou un compte extérieur que vous pourriez choisir dans l’application (qui partage un espace avec vous) : ajouté à l’espace avec workspace_role (GUEST ; MEMBER exige un admin de l’espace). Toute autre personne : 404, invitez-la par e-mail
PATCH / DELETE /boards/{id}/members/{userId}admins du tableauuniquement les membres explicites (sinon 404) ; le dernier admin du tableau reste (409 last_board_admin) ; un invité obtient au plus VIEWER (403 guest_viewer_only)
GET /workspaces/{slug}/membersadmins et membres de l’espace (invités : 404)pagination par curseur, admins en premier ; role, q, include=invitations (les invitations en attente suivent alors). managed = synchronisation de groupes OIDC (lecture seule), automatic = accès accordé aux admins de l’instance
POST /workspaces/{slug}/membersadmins de l’espace{user_ids, role} : uniquement les personnes que vous pouvez voir (espace partagé ; staff Propriétaire / Admin : tout le monde) ; les autres sont ignorées → {added}
PATCH / DELETE /workspaces/{slug}/members/{userId}admins de l’espacepas vous-même (403, utilisez leave), pas les lignes gérées par OIDC (403), pas le dernier admin explicite d’un espace privé (409 last_private_admin)
POST /workspaces/{slug}/leavetout membre409 last_admin_leave, last_private_admin, oidc_managed, instance_admin_leave (règles)
POST /workspaces/{slug}/invitationsadmins de l’espace ; admins du tableau pour les invités avec board_id{emails, role, board_id?, board_role?} → 202 {sent}. Les invités obtiennent au plus VIEWER (EDITOR / ADMIN : 403 guest_viewer_only) ; au-delà de la limite d’invités du forfait : 409 plan_limit avec kind guests
GET /workspaces/{slug}/invitations, GET /boards/{id}/invitationsadmins et membres de l’espace (invités : 404) / toute personne du tableauinvitations en attente avec can_revoke, un élément par invitation (une personne invitée dans l’espace et à deux tableaux en a trois)
DELETE /invitations/{id}admins de l’espace, la personne qui l’a envoyée, admins de son tableau404 si elle n’est pas en attente
GET /me/invitations, POST /me/invitations/{id}/accept, …/declinevousrapprochées uniquement de votre e-mail confirmé ; accept et decline ne répondent qu’à cette invitation (accept rejoint l’espace si besoin et ajoute le tableau de cette invitation ; vos autres invitations restent en attente) ; accept renvoie l’espace

Les paramètres du lien public (Partager un tableau publiquement) vont dans un objet public_link ; chaque champ est facultatif :

ChampSignification
enabledtrue / false : activer ou désactiver le lien
expires_atune date et une heure futures (ISO 8601 avec décalage), ou null pour aucune expiration ; une date passée donne 422 expiry_past
password6 à 128 caractères, ou null pour le retirer ; un nouveau mot de passe ou son retrait déconnecte tous les visiteurs. En écriture seule : il n’est jamais renvoyé
regeneratetrue : un nouveau lien ; l’ancien cesse de fonctionner immédiatement
visibilityce que voient les visiteurs, par ex. {"assignees": true, "time": false} ; les clés omises restent inchangées. Clés : labels, dates, checklists, descriptions (activées pour les nouveaux liens), assignees, members, comments, attachments, time (désactivées)

Tout ce qui n’est pas la désactivation d’un lien est refusé avec 403 plan_feature si le forfait n’inclut pas les liens publics, et avec 403 public_links_disabled_workspace si Autoriser les liens publics est désactivé dans l’espace.

Confidentialité, exactement comme dans l’application :

  • E-mails : les adresses des autres personnes ne sont visibles que pour le staff qui voit les e-mails (email: null sinon). Les adresses des invitations ne sont affichées en entier que pour ce staff, les admins de l’espace et la personne qui a envoyé l’invitation ; tous les autres reçoivent j•••@client.io (email_masked: true), et q ne cherche que dans ce que vous voyez, il ne peut donc pas servir à sonder une adresse.
  • Comptes : une invitation répond toujours 202 {"sent": n}, qu’une adresse ait un compte ou non : personne n’est ajouté directement, chaque adresse reçoit l’e-mail (et une notification dans l’application si un compte confirmé l’utilise). POST /boards/{id}/members et POST /workspaces/{slug}/members n’atteignent que les personnes que vous pourriez déjà choisir dans l’application.
  • Limites de débit : les invitations comptent dans invite (par personne qui invite) et invite-workspace (par espace), chaque adresse une fois ; les révocations dans invite-revoke. Ce sont les mêmes compteurs que dans l’application, en plus de api / api-write.
curl -H "$H" -H 'Content-Type: application/json' -X PATCH "$BASE/boards/$BOARD/sharing" -d '{"access":"RESTRICTED","public_link":false}'
curl -H "$H" -H 'Content-Type: application/json' -X PATCH "$BASE/boards/$BOARD/sharing" \
-d '{"public_link":{"enabled":true,"expires_at":"2026-12-31T23:59:59Z","visibility":{"assignees":true}}}'
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/boards/$BOARD/members" -d '{"user_id":"…","role":"EDITOR"}'
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/workspaces/northwind/invitations" \
-d '{"emails":["[email protected]"],"role":"GUEST","board_id":"…","board_role":"VIEWER"}' # → 202 {"sent":1}
curl -H "$H" "$BASE/workspaces/northwind/members?include=invitations&limit=100"
curl -H "$H" -X POST "$BASE/me/invitations/$INVITE/accept"

Forfaits​

Là où les forfaits s’appliquent, GET /api/v1/public/plans (sans clé) liste les forfaits publics, et GET /organizations/{slug}/plan montre aux propriétaires et aux admins d’une organisation son forfait, ses limites et son utilisation.

  • features indique ce qu’un forfait inclut, entre autres guest_comments (les espaces peuvent activer Les invités peuvent commenter) et email_notifications (désactivé : les notifications des espaces de l’organisation restent dans l’application).
  • limits.guests est la limite d’invités facultative (null = illimité). Les invités n’occupent jamais de place.
  • Depuis l’API 1.5.0, guests_free et limits.guests_free sont obsolètes : toujours true, ils seront supprimés dans une version ultérieure. Ne vous y fiez pas.
  • highlights ne contiennent plus « Guests don't use a seat ».

Webhooks​

Les webhooks d’espace (Webhooks : événements, charges utiles, signatures, nouvelles tentatives, règles d’adresse) via l’API : le même service que Paramètres → Espace de travail → Webhooks. Réservé aux admins de l’espace disposant d’une clé à accès complet, y compris pour GET (clés en lecture seule : 403 insufficient_scope ; autres membres : 403 forbidden) ; 403 webhooks_disabled tant que l’instance les a désactivés.

curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/workspaces/northwind/webhooks" \
-d '{"url":"https://hooks.example.com/sol2flow","description":"CI","events":["task.created","comment.created"]}'
# → 201 {"id":"…","state":"active",…,"secret":"whsec_…"} (the secret is shown only here and by rotate-secret)
curl -H "$H" -H 'Content-Type: application/json' -X PATCH "$BASE/workspaces/northwind/webhooks/$HOOK" -d '{"active":false}'
curl -H "$H" -X POST "$BASE/workspaces/northwind/webhooks/$HOOK/ping" # → 202, the queued delivery
curl -H "$H" "$BASE/workspaces/northwind/webhooks/$HOOK/deliveries?status=failed"
curl -H "$H" -X POST "$BASE/workspaces/northwind/webhooks/$HOOK/deliveries/$D/redeliver"

ping et redeliver comptent dans webhook-manual (60 / 10 minutes par espace).

Imports​

Imports de tableaux depuis kan.bn et Trello (Imports : ce qui est importé et comment) : le même service que Paramètres → Espace de travail → Import et export. Réservé aux admins de l’espace : POST exige une clé à accès complet, GET une clé de lecture. L’exécution est une tâche d’arrière-plan : POST répond 202 avec l’import, interrogez ensuite GET jusqu’à ce que status vaille DONE ou FAILED. Un seul import à la fois par espace (409 import_running). Les identifiants de la source transitent une seule fois dans le corps, ne sont conservés (chiffrés) que le temps de l’exécution, et ne sont jamais renvoyés.

# Trello API (key + token) or kan.bn (api_key, base_url for a self-hosted Kan); boards = source board ids
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/workspaces/northwind/imports" \
-d '{"source":"trello","api_key":"…","token":"…","boards":["5f1b…"],"access":"RESTRICTED"}' # → 202 {"id":"…","status":"PENDING",…}
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/workspaces/northwind/imports" \
-d '{"source":"kanbn","base_url":"https://kan.example.com","api_key":"…","boards":["brd8Kq2mZx1a"],"people":{"mem1aB2cD3eF":"<user id>"}}'
# a Trello board JSON export (up to 50 MB) in `board`
jq -n --slurpfile b board.json '{source:"trello_file", board:$b[0]}' | curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/workspaces/northwind/imports" -d @-
curl -H "$H" "$BASE/workspaces/northwind/imports/$IMPORT" # status, counts, errors, results[].board_key

people associe les clés de personnes de la source à des membres de l’espace (null = laisser non assigné) ; les personnes non listées sont rapprochées automatiquement (e-mail → nom d’utilisateur → nom complet unique). Personne n’est invité. Relancer un import n’ajoute que ce qui est nouveau.

Envoyer une pièce jointe​

Le même envoi par morceaux, avec reprise possible, que dans l’application :

# 1. start: the answer says how to split the file
curl -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"board_id":"…","task_id":"…","name":"report.pdf","size":1048576,"mime_type":"application/pdf"}' \
$BASE/uploads # → {"upload_id":"…","chunk_size":8388608,"part_count":1,"size":1048576}
# 2. each part n = 1…part_count: exactly chunk_size bytes (the last one: the rest)
curl -X PUT -H "Authorization: Bearer $KEY" -H 'Content-Type: application/octet-stream' \
--data-binary @report.pdf $BASE/uploads/$UPLOAD_ID/parts/1
# 3. finish: the file is attached to the task
curl -X POST -H "Authorization: Bearer $KEY" $BASE/uploads/$UPLOAD_ID/complete

GET /uploads/{id} liste les parties déjà reçues (pour reprendre), DELETE /uploads/{id} annule. Limites de taille : Administration → Général → Envoi de fichiers (413 file_too_large, max en Mo).

Markdown​

Les descriptions de tâches, les commentaires et les réponses peuvent être écrits et lus en Markdown (variante GitHub) : la même conversion que celle de l’éditeur quand on colle du Markdown, suivie du même nettoyage que pour toute autre saisie.

  • Écriture : POST /tasks, PATCH /tasks/{id} (description) et POST /tasks/{id}/comments, PATCH /comments/{id} (text) acceptent "format": "markdown". Sans cela ("text", la valeur par défaut), une chaîne est du texte brut, un paragraphe par ligne : les clients existants ne sont pas affectés. Une description / un doc en JSON Tiptap ignore format.
  • Lecture : ?format=markdown sur GET /tasks/{id}, POST /tasks, PATCH /tasks/{id}, GET /tasks/{id}/comments (réponses comprises), POST /tasks/{id}/comments et PATCH /comments/{id} ajoute description_markdown (tâches ; null sans description) ou markdown (commentaires ; null s’il est supprimé). description / doc et le texte brut restent inchangés.
  • Syntaxe : #/## → grand titre, ###+ → titre ; **bold**, *italic*/_italic_, ~~strike~~, `code`, blocs de code délimités avec un langage, citations >, listes -/*/1. (imbriquées), listes de tâches - [ ]/- [x], ---, liens et liens automatiques, sauts de ligne conservés (un simple retour à la ligne est un saut de ligne). Les tableaux deviennent un paragraphe par rangée.
  • Mentions : @[Name](user:<user id>) en entrée comme en sortie ; les mêmes règles que pour les mentions JSON (seules les personnes qui peuvent ouvrir le tableau restent des mentions et sont notifiées ; pour toute autre personne, cela devient le texte « @Name »).
  • Sécurité : le HTML brut n’est jamais interprété : <b>, <script>, <img onerror=…> restent du texte littéral. Les liens doivent être en http(s), mailto ou relatifs à l’application (javascript: etc. ne gardent que leur texte) ; les images uniquement sous la forme /api/files/<id> (les autres deviennent des liens). Le soulignement n’a pas d’équivalent Markdown et disparaît en sortie.
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/tasks?format=markdown" \
-d '{"board_id":"…","list_id":"…","title":"Release 1.2","format":"markdown","description":"## Steps\n- [ ] Changelog\n- [x] Tag **v1.2**"}'
# → { …, "description": {"type":"doc",…}, "description_text": "Steps\nChangelog\nTag v1.2", "description_markdown": "## Steps\n\n- [ ] Changelog\n- [x] Tag **v1.2**" }
curl -H "$H" "$BASE/tasks/$TASK/comments?format=markdown"

Exemples​

BASE=https://flow.example.com/api/v1; KEY=sf_…
H="Authorization: Bearer $KEY"

curl -H "$H" "$BASE/workspaces/northwind/boards?q=website"
curl -H "$H" "$BASE/tasks?workspace=northwind&assignee=$ME&due_to=2026-10-31&limit=100"
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/tasks" \
-d '{"board_id":"…","list_id":"…","title":"Write release notes","description":"Cover the API.\nLink the docs.","due_date":"2026-10-01"}'
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/tasks/$TASK/comments" -d '{"text":"Shipped 🎉"}'
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/time-entries" \
-d '{"task_id":"…","started_at":"2026-09-25T08:00:00Z","duration_sec":5400,"note":"Pairing"}'

Paramètres de l’instance​

Administration → Général → API :

  • Autoriser les clés d’API (activé par défaut). Désactivé : l’entrée de menu « Clés d’API » disparaît et toutes les clés existantes reçoivent un 403 api_disabled ; le réactiver les rétablit (elles ne sont pas supprimées).
  • Autoriser les webhooks (activé par défaut) et Autoriser les webhooks vers des adresses réseau privées (désactivé par défaut) : voir Webhooks.
  • Documentation de l’API (activé par défaut). Désactivé : /api/docs (Swagger UI et ses ressources) et /api/v1/openapi.json répondent 404, par exemple pour une instance de production qui ne doit pas afficher son API. L’API elle-même continue de fonctionner.

Swagger UI est servie par l’instance elle-même (sans CDN externe) et suit le thème sombre / clair de l’application. « Authorize » accepte une clé ; « Try it out » appelle ensuite l’API depuis le navigateur avec celle-ci.

Gestion des versions​

  • La version figure dans le chemin (/api/v1). Au sein de la v1, les changements sont uniquement additifs : nouveaux endpoints, nouveaux champs de requête facultatifs, nouveaux champs de réponse, nouveaux codes d’erreur pour de nouvelles situations. Les clients doivent ignorer les champs inconnus.
  • Supprimer ou renommer un champ, changer un type ou un code de statut, ou durcir la validation d’un champ existant nécessite /api/v2 ; la v1 continue alors de fonctionner pendant au moins 6 mois, avec une annonce dans le journal des modifications.
  • info.version dans le document OpenAPI suit le versionnage sémantique pour le contrat v1.
  • À partir de l’API 1.8, chaque réponse sous /api/v1 porte l’en-tête Sol2flow-Api-Version (par exemple 1.8.0), la même version que info.version, pour qu’un client sache ce que l’instance prend en charge même quand la documentation de l’API est désactivée. Les instances plus anciennes n’envoient pas cet en-tête.