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/docset 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
- 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. - Envoyez-la avec chaque requête :
curl -H "Authorization: Bearer $SOL2FLOW_KEY" https://flow.example.com/api/v1/me
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: Bearerest 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 403insufficient_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 formatYYYY-MM-DD. Les identifiants sont des UUID ; les espaces sont désignés par leur slug (comme dans/w/<slug>). - Succès :
200avec la ressource,201pour les créations,202pour 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 (seulGET /merenvoie 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çoiventemailsur 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=markdownpour 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) etnext_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.
| Statut | Quand | Codes typiques |
|---|---|---|
| 400 | Le corps n’est pas du JSON ; un paramètre de chemin ou de requête est invalide (UUID ou curseur incorrect) | invalid_input |
| 401 | Clé absente, mal formée, inconnue, expirée ou révoquée ; compte désactivé | unauthenticated |
| 403 | Votre 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 forfait | forbidden, insufficient_scope, api_disabled, webhooks_disabled, public_links_disabled_workspace, plan_feature, guest_viewer_only |
| 404 | N’existe pas ou vous ne pouvez pas l’ouvrir (l’API ne dit jamais lequel des deux) | not_found |
| 405 | Chemin connu, autre méthode | method_not_allowed |
| 409 | L’état actuel ne le permet pas | board_archived, label_scope, last_board_admin, last_admin_leave, last_private_admin, oidc_managed, instance_admin_leave, conflict, upload_incomplete, webhook_limit, plan_limit |
| 413 | Fichier (ou corps JSON > 1 Mo) trop volumineux | file_too_large |
| 422 | Échec de la validation | title_required, name_required, too_long, dates_order, duration_invalid, invalid_input, webhook_url_blocked, webhook_url_unresolved, webhook_url_invalid, events_required, … |
| 429 | Limite de débit atteinte : attendez Retry-After secondes | rate_limited |
| 500 | Un bug ; aucune information interne n’est divulguée | internal |
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 :
| Domaine | Endpoints |
|---|---|
| Compte | GET /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) |
| Espaces | GET /workspaces · GET /workspaces/{slug} · GET /workspaces/{slug}/search?q= |
| Tableaux | GET /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 |
| Listes | POST /boards/{id}/lists · PATCH /boards/{id}/lists/{listId} · POST …/lists/{listId}/move · DELETE …/lists/{listId} |
| Étiquettes | GET /workspaces/{slug}/labels · GET /boards/{id}/labels · POST /boards/{id}/labels |
| Tâches | GET /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 |
| Checklist | POST /tasks/{id}/checklist · PATCH /checklist/{itemId} (texte, terminé) · POST /checklist/{itemId}/move · DELETE /checklist/{itemId} |
| Liens entre tâches | GET /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} |
| Commentaires | GET /tasks/{id}/comments (avec les réponses ; `order=newest |
| Temps | GET /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 jointes | GET /tasks/{id}/attachments · GET /files/{fileId} · envois de fichiers, ci-dessous |
| Notifications | GET /notifications (unread) · PATCH /notifications/{id} (read) · POST /notifications/read-all |
| Partage | GET /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 |
| Membres | GET /workspaces/{slug}/members (role, q, include=invitations) · POST /workspaces/{slug}/members · PATCH/DELETE /workspaces/{slug}/members/{userId} · POST /workspaces/{slug}/leave |
| Invitations | POST /workspaces/{slug}/invitations (202) · GET /workspaces/{slug}/invitations · DELETE /invitations/{id} · GET /me/invitations · POST /me/invitations/{id}/accept · POST /me/invitations/{id}/decline |
| Webhooks | GET/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).
| Endpoint | Qui | Remarques |
|---|---|---|
GET /boards/{id}/sharing | toute personne qui peut ouvrir le tableau | accè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}/sharing | admins du tableau | access, 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}/members | admins 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 tableau | uniquement 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}/members | admins 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}/members | admins 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’espace | pas 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}/leave | tout membre | 409 last_admin_leave, last_private_admin, oidc_managed, instance_admin_leave (règles) |
POST /workspaces/{slug}/invitations | admins 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}/invitations | admins et membres de l’espace (invités : 404) / toute personne du tableau | invitations 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 tableau | 404 si elle n’est pas en attente |
GET /me/invitations, POST /me/invitations/{id}/accept, …/decline | vous | rapproché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 :
| Champ | Signification |
|---|---|
enabled | true / false : activer ou désactiver le lien |
expires_at | une date et une heure futures (ISO 8601 avec décalage), ou null pour aucune expiration ; une date passée donne 422 expiry_past |
password | 6 à 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é |
regenerate | true : un nouveau lien ; l’ancien cesse de fonctionner immédiatement |
visibility | ce 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: nullsinon). 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çoiventj•••@client.io(email_masked: true), etqne 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}/membersetPOST /workspaces/{slug}/membersn’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) etinvite-workspace(par espace), chaque adresse une fois ; les révocations dansinvite-revoke. Ce sont les mêmes compteurs que dans l’application, en plus deapi/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.
featuresindique ce qu’un forfait inclut, entre autresguest_comments(les espaces peuvent activer Les invités peuvent commenter) etemail_notifications(désactivé : les notifications des espaces de l’organisation restent dans l’application).limits.guestsest la limite d’invités facultative (null= illimité). Les invités n’occupent jamais de place.- Depuis l’API 1.5.0,
guests_freeetlimits.guests_freesont obsolètes : toujourstrue, ils seront supprimés dans une version ultérieure. Ne vous y fiez pas. highlightsne 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) etPOST /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. Unedescription/ undocen JSON Tiptap ignoreformat. - Lecture :
?format=markdownsurGET /tasks/{id},POST /tasks,PATCH /tasks/{id},GET /tasks/{id}/comments(réponses comprises),POST /tasks/{id}/commentsetPATCH /comments/{id}ajoutedescription_markdown(tâches ;nullsans description) oumarkdown(commentaires ;nulls’il est supprimé).description/docet 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.jsonré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.versiondans le document OpenAPI suit le versionnage sémantique pour le contrat v1.- À partir de l’API 1.8, chaque réponse sous
/api/v1porte l’en-têteSol2flow-Api-Version(par exemple1.8.0), la même version queinfo.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.