Skip to content

API Cloud ​

L'API publique du cloud Runeya permet à un système tiers d'alimenter les tableaux kanban d'une organisation : une intégration continue qui ouvre une carte quand un build casse, un formulaire de support qui dépose une demande, une supervision qui commente la carte qu'elle avait créée.

Elle est indépendante de l'API du serveur local : aucun poste n'a besoin d'être allumé, et la clé ne dépend d'aucune session utilisateur.

Obtenir une clé ​

Dans l'application cloud, Paramètres → Organisation → Clés API. L'écran est réservé aux propriétaires et administrateurs de l'organisation.

Une clé comporte :

Un nomIl sert à la reconnaître, par exemple le jour où il faudra la révoquer.
Des droitskanban:read, kanban:write, kanban:comment, kanban:delete. Chaque droit se coche séparément.
Des tableauxTous ceux de l'organisation, ou seulement ceux qu'on choisit.

Le secret n'est montré qu'une fois

Le cloud n'en garde qu'un condensat. Une clé perdue ne peut pas être retrouvée : il faut la révoquer et en émettre une nouvelle.

Une clé dépend de la personne qui l'a émise

Elle agit au nom de cette personne. Si celle-ci quitte l'organisation, la clé cesse de fonctionner. Ainsi, aucun accès en écriture ne reste actif par oubli après son départ. Pour une intégration qui doit durer, émettez la clé depuis un compte qui restera dans l'organisation.

Premier appel ​

bash
curl https://api.runeya.dev/api/v1/boards \
  -H "Authorization: Bearer rk_…"

La réponse liste les tableaux que la clé peut atteindre, chacun avec ses colonnes. Le titre d'une colonne sert ensuite à y déposer une carte.

Ajouter une carte ​

bash
curl -X POST https://api.runeya.dev/api/v1/boards/<boardId>/cards \
  -H "Authorization: Bearer rk_…" \
  -H "Content-Type: application/json" \
  -d '{
    "column": "À faire",
    "title": "Build #4821 en échec",
    "markdown": "`yarn test` échoue sur `kanban-quota.test.ts`.",
    "priority": 8,
    "groups": ["ci"]
  }'

column accepte l'identifiant de la colonne ou son titre, comparé sans tenir compte de la casse ni des espaces de bord.

L'écriture est unitaire : elle ajoute une carte sans réécrire le tableau. Deux appels simultanés ajoutent bien leurs deux cartes, et un appel n'écrase pas les modifications d'une personne en train d'éditer le tableau. La carte apparaît immédiatement chez tous ceux qui regardent le tableau.

Toutes les routes ​

Portée
GET/meaucune
GET/memberskanban:read
GET/boardskanban:read
POST/boardskanban:write
GET PATCH/boards/{b}kanban:read / kanban:write
POST/boards/{b}/columnskanban:write
POST/boards/{b}/columns/reorderkanban:write
PATCH/boards/{b}/columns/{id}kanban:write
DELETE/boards/{b}/columns/{id}kanban:delete
POST/boards/{b}/cardskanban:write
GET/boards/{b}/cards/{c}kanban:read
PATCH/boards/{b}/cards/{c}kanban:write
DELETE/boards/{b}/cards/{c}kanban:delete
POST/boards/{b}/cards/{c}/movekanban:write
GET/boards/{b}/archivekanban:read
POST/boards/{b}/cards/{c}/archivekanban:write
POST/boards/{b}/cards/{c}/unarchivekanban:write
POST/boards/{b}/cards/{c}/parentkanban:write
GET/boards/{b}/cards/{c}/placementskanban:read
POST/boards/{b}/placementskanban:write
DELETE/boards/{b}/placements/{c}kanban:write
POST/boards/{b}/cards/{c}/commentskanban:comment
POST/boards/{b}/cards/{c}/attachmentskanban:write, ou kanban:comment avec ?comment=
GET/boards/{b}/cards/{c}/attachments/{id}kanban:read
DELETE/boards/{b}/cards/{c}/attachments/{id}kanban:delete

Points à connaître avant d'écrire un script :

  • L'assignation se fait par identifiant. GET /members donne les identifiants. Le nom affiché est toujours lu dans l'organisation, et une personne qui n'en fait pas partie ne peut pas être attachée à une carte.
  • null retire un champ. {"priority": null} retire la priorité de la carte, ce qui est différent d'une priorité de 1. La règle vaut aussi pour color et deadline.
  • deadline est une date sans heure (AAAA-MM-JJ). Une équipe fixe une échéance « pour vendredi ». Avec une heure, l'échéance s'afficherait la veille pour les personnes qui travaillent à l'ouest de Greenwich, et la carte serait en retard d'un jour pour elles. Sans deadline, la carte n'a pas d'échéance et n'est jamais en retard.
  • Un tableau créé par l'API n'est pas accessible à une clé restreinte. La liste des tableaux d'une clé ne s'élargit pas automatiquement. La personne qui a émis la clé doit y ajouter le tableau.

Importer depuis un autre outil ​

POST /boards/{b}/cards accepte createdAt, une date et heure ISO complète qui ne peut pas être dans le futur. C'est le seul champ de date que l'appelant fixe, et seulement à la création. Sans lui, un tableau importé de Jira ou de Trello aurait toutes ses cartes créées le même jour, et l'ordre d'ancienneté des cartes serait perdu.

bash
curl -X POST "https://api.runeya.dev/api/v1/boards/<boardId>/cards" \
  -H "Authorization: Bearer rk_…" -H 'Content-Type: application/json' \
  -d '{"column":"À faire","title":"Refonte du header","createdAt":"2024-03-04T09:30:00Z"}'

L'entrée « créée » du journal porte la même date et indique qu'elle a été importée : le serveur n'a pas constaté cette date lui-même. updatedAt reste la date réelle de l'écriture. Seule la date de création peut venir d'ailleurs. PATCH n'accepte pas createdAt : antidater une carte que d'autres ont déjà vue modifierait son historique sans le signaler. createdBy ne peut être fixé ni à la création ni par PATCH : l'appelant peut reprendre une date, mais il ne choisit pas l'auteur.

Archiver une carte ​

Une carte archivée disparaît des colonnes, mais reste en base et lisible par son adresse. unarchive la remet dans la colonne d'où elle est partie, ou dans la première colonne du tableau si celle-ci n'existe plus. L'archivage est réversible : c'est pourquoi il demande kanban:write et non kanban:delete.

bash
# Ranger une carte
curl -X POST "https://api.runeya.dev/api/v1/boards/<boardId>/cards/<cardId>/archive" \
  -H "Authorization: Bearer rk_…"

# Fouiller l'archive
curl "https://api.runeya.dev/api/v1/boards/<boardId>/archive?q=refonte" \
  -H "Authorization: Bearer rk_…"

# La reprendre
curl -X POST "https://api.runeya.dev/api/v1/boards/<boardId>/cards/<cardId>/unarchive" \
  -H "Authorization: Bearer rk_…"

Chaque carte porte archivedAt et archivedFrom. Ces deux champs valent null tant que la carte est au tableau.

Une carte archivée est en lecture seule. Modifier ses champs, y joindre un fichier, la commenter, la déplacer ou lui donner un parent répond 400 KANBAN_CARD_ARCHIVED, car ces changements ne seraient visibles par personne sur le tableau. Seules deux actions restent possibles : unarchive et la suppression.

L'archive renvoie au plus 200 cartes par appel, les plus récemment archivées d'abord. Quand il en reste, la réponse porte truncated: true et un nextCursor à repasser dans cursor pour obtenir la page suivante. La lecture est terminée quand nextCursor vaut null. Le nombre de cartes rendues ne permet pas de le savoir. Le curseur est opaque : il se repasse tel quel et ne se construit pas à la main. limit permet de demander une page plus petite. Une valeur supérieure à 200 est ignorée, sans erreur. La recherche q se fait côté serveur, sur le titre, le corps, le texte des commentaires et les conversations de support suivies par la carte (titre, messages et nom de leurs auteurs), sans distinction de casse ni d'accents. Pour rester sous la limite de 200 cartes, on affine avec cette recherche au lieu de paginer. Les commentaires sont inclus parce qu'ils contiennent souvent l'essentiel de l'historique d'une carte. Quand ils étaient exclus, ce qui s'y était dit restait introuvable.

Les données supprimées sont conservées. DELETE sur une carte ou sur une colonne, et la suppression d'un tableau, retirent la donnée de toutes les lectures. La donnée reste en base, avec son journal et ses fichiers. Une suppression ne peut donc pas être annulée par l'API. Elle peut l'être en base, par exemple si quelqu'un a supprimé la mauvaise carte.

Poser une carte sur plusieurs tableaux ​

Une carte peut apparaître sur d'autres tableaux que le sien sans être copiée. On parle alors de référence. La carte reste sur son tableau d'origine. Chaque tableau où elle est posée lui attribue sa propre colonne et son propre rang.

bash
# Poser la carte <cardId> dans la colonne « En cours » du tableau <boardId>
curl -X POST "https://api.runeya.dev/api/v1/boards/<boardId>/placements" \
  -H "Authorization: Bearer rk_…" -H "Content-Type: application/json" \
  -d '{"cardId": "<cardId>", "column": "En cours"}'

# Où se trouve-t-elle ?
curl "https://api.runeya.dev/api/v1/boards/<boardId>/cards/<cardId>/placements" \
  -H "Authorization: Bearer rk_…"

# L'en retirer
curl -X DELETE "https://api.runeya.dev/api/v1/boards/<boardId>/placements/<cardId>" \
  -H "Authorization: Bearer rk_…"

Une fois posée, la carte peut aussi être désignée depuis ce tableau. move ne déplace que la référence. Modifier, commenter, archiver ou supprimer agit sur la carte elle-même, donc sur tous ses tableaux. Lue depuis un tableau où elle est en référence, la carte porte origin: { boardId, boardName }. Sur son tableau d'origine, origin vaut null.

Retirer une référence ne modifie pas la carte. Cette action demande donc kanban:write et non kanban:delete. Le tableau d'origine de la carte doit être dans la portée de la clé. Sinon, la requête répond 404, comme pour une carte qui n'existe pas. Elle répond 400 si la carte est archivée, si ce tableau est déjà son tableau d'origine ou si elle y est déjà posée.

Joindre un fichier ​

La même route sert pour une carte et pour un commentaire. Sans paramètre, le fichier est joint à la carte. Avec ?comment=<id>, il est joint à ce commentaire.

bash
curl -X POST "https://api.runeya.dev/api/v1/boards/<boardId>/cards/<cardId>/attachments?name=build.log" \
  -H "Authorization: Bearer rk_…" \
  --data-binary @build.log

Le corps de la requête est le fichier brut, sans multipart. Pour un script, --data-binary @fichier est plus simple à écrire qu'un corps composite.

La carte, et le commentaire s'il est visé, sont vérifiés avant l'envoi du fichier au stockage. Si le rattachement échoue, le fichier est retiré du stockage. Aucun fichier n'est donc stocké sans être référencé. Le type sous lequel le fichier est stocké est déterminé par son contenu, jamais par l'en-tête Content-Type déclaré. Sinon, un fichier déclaré text/html pourrait être exécuté comme script par un aperçu.

Limites : 55 Mo par fichier, 50 par carte, 20 par commentaire. Une instance sans stockage objet répond 501 storage_not_configured.

Pour relire un fichier, on passe par sa carte, qui porte déjà le contrôle d'accès :

bash
curl "https://api.runeya.dev/api/v1/boards/<boardId>/cards/<cardId>/attachments/<id>" \
  -H "Authorization: Bearer rk_…" -o build.log

Un fichier que cette carte ne porte pas répond 404, même s'il existe ailleurs.

Faire avancer une carte ​

bash
curl -X POST https://api.runeya.dev/api/v1/boards/<boardId>/cards/<cardId>/move \
  -H "Authorization: Bearer rk_…" \
  -H "Content-Type: application/json" \
  -d '{"column": "En cours"}'

Réponses d'erreur ​

json
{ "error": { "code": "insufficient_scope", "message": "This API key lacks the \"kanban:write\" scope." } }
Code HTTPCe qui s'est passé
401Clé absente, mal formée, inconnue ou révoquée. Ou key_owner_inactive : la personne qui l'a émise a quitté l'organisation.
403La clé n'a pas la portée demandée, ou le forfait de l'organisation l'interdit (code en PLAN_LIMIT_*).
404Tableau inexistant, ou hors de la portée de la clé. Vu de l'extérieur, l'API ne distingue pas les deux cas.
409La colonne visée a été supprimée entre la lecture et l'écriture (KANBAN_COLUMN_GONE), ou l'id de carte proposé est déjà pris (KANBAN_CARD_EXISTS). Rien n'a été écrit.
413Le corps dépasse la limite. Le message dit laquelle des deux : 256 ko pour un JSON, 55 Mo pour un fichier.

Le message d'un refus pour portée manquante nomme cette portée, par exemple This API key lacks the "kanban:write" scope.. On sait ainsi quelle case cocher sur la clé.

GET /api/v1/me renvoie ce que porte la clé présentée. Cela permet de comprendre un 403 sans deviner.

Intégrations de support (Intercom, HubSpot) ​

Réglage : Paramètres › Organisation › Intégrations (propriétaires et administrateurs). Une intégration porte le client secret de l'app Intercom (signature des webhooks) et son access token (recherche et lecture, lecture seule), tous deux obligatoires, la région de l'espace Intercom, et l'option création automatique. Seule cette option demande un tableau et une colonne de création.

Modèle. Une conversation est stockée une fois (integrationConversation, messages dans integrationMessage) ; une carte n'en garde qu'un lien (integrationCardLink). Une conversation peut être suivie par plusieurs cartes, une carte peut en suivre plusieurs. Les messages ne sont pas des commentaires : ils se lisent dans l'onglet du fournisseur, sur la carte.

Chez le fournisseurEffet
Nouvelle conversationConversation enregistrée ; si la création automatique est active et que personne ne la suit, carte dans la colonne choisie (lien auto)
Réponse, note interneMessage enregistré, visible sur toutes les cartes qui la suivent
RésolueCarte auto archivée, seulement si elle est toujours dans sa colonne de création
Rouverte, ou le client réécritCarte auto désarchivée, même condition

Une carte liée à la main (recherche depuis l'onglet de la carte, ou outil MCP) n'est jamais archivée ni désarchivée par l'intégration. Lier importe tout le fil déjà échangé. Les fichiers (pièces jointes et images insérées) sont copiés dans le stockage des pièces jointes de l'organisation (les liens d'Intercom expirent) : ils s'ouvrent comme ceux d'une carte et se téléchargent avec kanban_download_attachment. Seuls les hôtes d'Intercom sont interrogés ; un fichier trop gros, injoignable ou sans stockage S3 configuré est signalé comme non récupéré, et la raison (avec le seul domaine) est écrite dans les logs du cloud. Le bouton Synchroniser d'une conversation relit son fil chez Intercom, ajoute les messages manquants et retente les fichiers non récupérés. La recherche porte sur le titre de la conversation ; un numéro ou une URL de conversation Intercom l'ouvre directement. Tout membre qui peut écrire sur le kanban peut lier ou délier ; la recherche signale les cartes qui suivent déjà un résultat. Le lien Ouvrir mène à la conversation dans Intercom : il ne fonctionne que pour un compte de cet espace Intercom.

Webhook. POST /api/integrations/webhook/<jeton>. Cette URL est affichée dans l'écran et se trouve hors de /api/v1. Le jeton désigne la connexion, et la signature (X-Hub-Signature, HMAC-SHA1 sous le client secret) prouve l'origine. Secrets chiffrés au repos, jamais réaffichés.

  • Rejouable : conversation et messages sont identifiés par le fournisseur ; l'archivage suit une transition d'état, une résolution rejouée ne réarchive pas une carte sortie de l'archive.
  • Codes : 404 jeton inconnu, 401 signature fausse, 200 appliqué ou impossible par configuration (connexion désactivée, tableau supprimé, refus du kanban), 500 erreur transitoire, et le fournisseur réessaie.
  • Comme une clé, l'intégration agit au nom de son créateur : elle s'arrête s'il quitte l'organisation.

Sujets Intercom : conversation.user.created, conversation.admin.single.created, conversation.user.replied, conversation.admin.replied, conversation.admin.noted, conversation.admin.closed, conversation.admin.opened.

HubSpot. Ce qu'on lie est un fil de l'Inbox (e-mail, formulaire…), lu par l'API Conversations v3. Il faut une app privée (une clé de service ne reçoit pas de webhooks) avec les scopes conversations.read, et de préférence crm.objects.tickets.read et files.read. Différences avec Intercom :

  • Webhook : signature X-HubSpot-Signature-v3 (HMAC-SHA256 en base64 de méthode + URL publique du webhook + corps + horodatage, sous le client secret), refusée au-delà de 5 minutes. La notification ne porte que des identifiants : chaque fil concerné est relu par l'API (lecture seule), un lot pouvant en porter plusieurs. Sujets : conversation.creation, conversation.newMessage, conversation.propertyChange (propriété status).
  • Titre : le sujet du ticket né du fil s'il y en a un (scope crm.objects.tickets.read), sinon l'objet du premier e-mail sans ses « RE: ». Les notes internes sont les COMMENT du fil ; assignations et changements de statut sont ignorés.
  • Recherche : un fil n'a pas de titre, donc la recherche texte passe par les tickets et renvoie le fil d'origine de chacun. Une URL de l'Inbox (…/live-messages/<portail>/inbox/<fil>), un numéro de fil ou l'URL d'une fiche ticket (…/record/0-5/<ticket>) l'ouvrent directement.
  • Fichiers : servis par *.hubspotusercontent-*.net. Les images insérées dans un e-mail pointent vers le gestionnaire de fichiers, derrière la session HubSpot : elles sont échangées contre un lien signé par l'API Files (scope files.read), sans quoi elles restent non récupérées.
  • Région : na1 ou eu1, qui ne change que le lien Ouvrir (app.hubspot.com ou app-eu1.hubspot.com).

Limites ​

Il n'y a aujourd'hui aucune limitation de débit, ni par clé, ni par organisation, ni par adresse. Le nombre d'appels n'est pas limité, et l'API ne répond jamais 429. Nous le précisons parce qu'un intégrateur qui ne voit aucune limite mentionnée ne peut pas savoir si elle n'existe pas ou si la page est incomplète. Ne comptez pas sur cette absence de limite : une limite pourra être ajoutée. Un client qui traite déjà 429 comme « réessayer plus tard » n'aura alors rien à changer.

Taille des requêtes ​

LimiteAu-delà
Corps JSON256 ko413 payload_too_large
Fichier joint (POST .../attachments)55 Mo413 payload_too_large

Longueur des champs ​

Un champ trop long est refusé avec une erreur 400. Il n'est jamais tronqué, pour ne pas enregistrer une carte incomplète.

ChampLimite
title (carte)500 caractères
markdown (carte)100 000 caractères
comment100 000 caractères, comme markdown
groups30 étiquettes, 60 caractères chacune
assignees20 personnes
attachments50 par carte, 20 par commentaire
priorityentier de 1 à 10
color#RRGGBB
deadlineAAAA-MM-JJ
name (tableau)200 caractères
description (tableau)10 000 caractères
instructions (tableau)20 000 caractères
Colonnes par tableau100
title (colonne)200 caractères

Quotas de forfait ​

Ils dépendent du forfait de l'organisation, et non de la clé. Un dépassement répond 403 avec un code en PLAN_LIMIT_*. Un script peut tester ce code, ce qui n'est pas possible avec le message.

ForfaitTableauxCartes par tableau
Solo130
Starter5100
Proillimitéillimité

Un dépassement bloque seulement les ajouts. Une organisation passée à un forfait plus petit garde ce qu'elle a et peut continuer à le modifier.

Référence ​

La spécification est servie par l'API elle-même, en accès libre, à l'adresse /api/v1/openapi.json. Elle permet de générer un client typé.

Runeya