API Motion
Client complet pour l'API REST de Motion (usemotion.com) — tous les 27 endpoints documentés pour les
tâches, projets, tâches récurrentes, commentaires, champs personnalisés, utilisateurs, espaces de travail, statuts et
plannings, plus une escape hatch raw pour tout ce qui n'est pas encore documenté.
Quand utiliser ce skill
Utilisez-le chaque fois que le travail implique Motion : lire ou créer des tâches, lister des projets, publier des commentaires sur des tâches, gérer des tâches récurrentes, ou lire les statuts, utilisateurs et plannings d'un espace de travail.
Identifiants
Un identifiant, MOTION_API_KEY. La plateforme l'injecte comme variable d'environnement lorsque
un script s'exécute via RunWithCredentials — ne transmettez jamais la clé en ligne de commande, et ne
l'imprimez ou ne l'affichez jamais.
Obtenez une clé dans Motion sous Paramètres → API. Motion ne l'affiche qu'une seule fois, copiez-la donc immédiatement. Si les appels commencent à retourner 401, la clé a été révoquée ou régénérée et doit être remplacée.
Remarque : avec la remise d'identifiant en mode proxy, la variable d'environnement contient un espace
réservé non secret et la valeur réelle est injectée dans les requêtes à api.usemotion.com. Un
identifiant nouvellement enregistré peut prendre quelques minutes à se propager ; un 401 immédiat qui
réussit ensuite est une propagation, pas une clé invalide.
Conventions suivies par l'API
Voici les faits qui font la différence entre un appel qui fonctionne et un qui ne fonctionne pas.
| Élément | Valeur |
|---|---|
| URL de base | https://api.usemotion.com |
| En-tête d'authentification | X-API-Key: <clé> |
| Vérification de santé | GET /v1/users/me — retourne le propriétaire de la clé |
| Préfixes de chemin | /v1/ pour tout sauf les champs personnalisés, qui sont en /beta/ |
| Limites de débit | 12 requêtes/min (individuel), jusqu'à 120 (équipes), supérieur pour entreprise |
| Descriptions | Markdown GitHub Flavored |
| Valeurs de priorité | ASAP, HIGH, MEDIUM, LOW (tâches récurrentes : HIGH/MEDIUM uniquement) |
| Types d'échéance | HARD, SOFT, NONE (tâches récurrentes : HARD/SOFT uniquement) |
Pagination. Les endpoints de liste prennent un cursor et retournent une enveloppe :
{ "meta": { "nextCursor": "…", "pageSize": 25 }, "tasks": [ … ] }
La clé de l'enveloppe est meta, et le champ de la page suivante est nextCursor. Passez --all sur n'importe
quelle commande de liste et le script suit le curseur pour vous, retournant un tableau plat.
--all s'arrête à 100 pages puis échoue, donc une requête incontrôlée ne peut pas bloquer une exécution —
réduisez la requête si vous voyez cela. Et si une page n'est ni un objet JSON ni un tableau (tronquée, ou
une page d'erreur de passerelle), la navigation refuse plutôt que de retourner ce qu'elle a : une liste
partielle qui semble complète est pire qu'une erreur.
Deux endpoints de liste retournent un tableau nu sans pagination — GET /v1/statuses et
GET /beta/workspaces/{id}/custom-fields. Tout le reste est envelopé.
Erreurs. Les réponses non 2xx portent {"message", "error", "statusCode"}. Les scripts
impriment cela en JSON sur stderr et sortent avec un code non nul. 429 est retenté avec backoff en
respectant Retry-After ; 400, 401, 403 et 404 sont remontés immédiatement et jamais
retentés. Les échecs de validation portent un tableau errors[] par champ ; le client ajoute ces
messages pour que la raison réelle soit visible.
Un User-Agent est obligatoire. Le proxy de Motion bloque la signature par défaut Python-urllib/x.y
avec un Cloudflare 403 (browser_signature_banned) avant que la requête n'atteigne l'API. Le
client envoie toujours motion-api-skill/1.0 ; si vous écrivez votre propre requête vers cette API,
définissez un User-Agent explicitement ou vous obtiendrez un 403 déroutant au lieu d'une réponse
réelle.
Les paramètres de requête booléens doivent être en minuscules. Motion analyse les chaînes de
requête comme du texte, donc un filtre booléen doit être le littéral true, pas le True de Python.
Se tromper n'est pas une opération silencieuse : l'API répond avec un 400 confus qui parle de la valeur
opposée (voir la section des différences en direct ci-dessous). Le client envoie toujours des littéraux en
minuscules.
Référence des commandes
Chaque commande imprime du JSON sur stdout. python3 motion.py <groupe> --help liste les indicateurs.
Tâches
| Commande | Endpoint |
|---|---|
tasks list | GET /v1/tasks |
tasks get <task_id> | GET /v1/tasks/{id} |
tasks create | POST /v1/tasks |
tasks update <task_id> | PATCH /v1/tasks/{id} |
tasks move <task_id> --workspace-id … | PATCH /v1/tasks/{id}/move |
tasks delete <task_id> --yes | DELETE /v1/tasks/{id} |
tasks unassign <task_id> --yes | DELETE /v1/tasks/{id}/assignee |
Filtres de tasks list : --assignee-id, --project-id, --workspace-id, --name
(sous-chaîne insensible à la casse), --label (singulier, un nom d'étiquette), --status (répétable),
--include-all-statuses. --status et --include-all-statuses s'excluent mutuellement —
les envoyer tous deux est rejeté par l'API, et le CLI refuse tôt avec ce message plutôt que de dépenser
une requête pour un 400 garanti.
--include-all-statuses est le moyen de voir une tâche qui a dépassé le statut par défaut de son espace
de travail (par exemple, tout ce qui se trouve dans Backlog ou In Review) ; sans cela, GET /v1/tasks ne retourne
que le statut par défaut et la liste semble vide. Le combiner avec --all retourne chaque tâche
assignée à quelqu'un, dans tous les statuts.
tasks create / tasks update prennent les mêmes champs : --name, --workspace-id,
--due-date (ISO 8601 ; requis pour les tâches planifiées), --duration, --status,
--project-id, --description, --priority, --label (répétable), --assignee-id.
--duration accepte un nombre entier de minutes, ou les chaînes NONE / REMINDER
(insensible à la casse en entrée, normalisé en majuscules).
L'auto-planification est un état ternaire, et la distinction est importante :
- omettre les deux indicateurs → le champ est laissé de côté de la requête entièrement (inchangé)
--auto-scheduled '{"deadlineType":"HARD"}'→ l'active ; les champs imbriqués sontstartDate,deadlineType(HARD/SOFT/NONE) etschedule(par défautWork Hours)--auto-scheduled-off→ envoie unnullexplicite, la désactivant
Le statut de la tâche doit avoir l'auto-planification activée, sinon l'API refuse.
Une réponse de tâche ressemble à ceci — réduite aux champs dont le workflow de calendrier ci-dessous dépend :
{
"id": "tsk_123",
"name": "Rédiger le plan T4",
"description": "Esquisser les trois scénarios.",
"status": { "name": "En cours", "isResolvedStatus": false },
"priority": "HIGH",
"labels": ["t4"],
"assignees": [{ "id": "usr_1", "name": "Sam Patel" }],
"project": { "id": "prj_1", "name": "Planification" },
"dueDate": "2026-11-01T17:00:00.000Z",
"duration": 45,
"scheduledStart": "2026-10-28T09:00:00.000Z",
"startOn": "2026-10-28",
"completedTime": null
}
Interprétez-le comme : status est un objet, pas une chaîne — le filtre que vous voulez est
status.isResolvedStatus, qui devient true une fois qu'une tâche est terminée ou annulée.
assignees et labels sont des tableaux ; project est un objet ou null. scheduledStart
et dueDate sont en UTC ; startOn est une date nue.
Il n'y a pas d'endpoint de calendrier. La position d'une tâche sur le calendrier est son scheduledStart
(UTC ISO 8601), avec duration en minutes et startOn comme date simple. Pour répondre à "quelle est la
prochaine", récupérez les tâches et triez par scheduledStart — ne cherchez pas un endpoint de planning qui
retournerait des événements, car schedules list ne retourne que les fenêtres d'heures de travail
(voir ci-dessous), pas les éléments réservés. Filtrez les tâches status.isResolvedStatus pour
exclure tout ce qui est déjà terminé ou annulé.
Projets
| Commande | Endpoint |
|---|---|
projects list [--workspace-id …] | GET /v1/projects |
projects get <project_id> | GET /v1/projects/{id} |
projects create --name … --workspace-id … | POST /v1/projects |
projects create prend également --due-date, --description (HTML accepté), --label
(répétable), --priority (par défaut MEDIUM côté serveur), et la paire de modèles
--project-definition-id + --stages. Les étapes doivent correspondre exactement à l'ordre et au nombre de
la définition, sinon l'API retourne 400. Pour les longs tableaux d'étapes, utilisez --stages @stages.json.
Le CLI n'applique pas la paire ; l'API rejette une liste d'étapes sans un identifiant de définition.
Tâches récurrentes
| Commande | Endpoint |
|---|---|
recurring-tasks list --workspace-id … | GET /v1/recurring-tasks |
recurring-tasks create | POST /v1/recurring-tasks |
recurring-tasks delete <id> --yes | DELETE /v1/recurring-tasks/{id} |
recurring-tasks create nécessite --frequency, --name, --workspace-id et
--assignee-id. Optionnel : --deadline-type (HARD/SOFT uniquement — NONE est rejeté ici),
--duration (minutes en entier, ou REMINDER ; NONE n'est pas accepté sur cet
endpoint), --starting-on, --ideal-time (HH:mm), --schedule (par défaut Work Hours),
--description, --priority (HIGH/MEDIUM uniquement).
--starting-on est envoyé à l'API exactement comme donné — pas de découpage.
--frequency est une chaîne dans le DSL propre à Motion, pas un objet. Cela surprend les gens.
Le vocabulaire est documenté à https://docs.usemotion.com/cookbooks/frequency/ :
- Codes jour :
MO,TU,WE,TH,FR,SA,SU - Quotidien :
daily_every_day,daily_every_week_day,daily_specific_days_[MO, TU, FR] - Hebdomadaire :
weekly_any_day,weekly_any_week_day,weekly_specific_days_[MO, FR] - Bi-hebdomadaire :
biweekly_first_week_any_day,biweekly_first_week_specific_days_[MO, TU],biweekly_second_week_any_week_day - Mensuel par jour de semaine :
monthly_first_MO,monthly_second_TU,monthly_last_FR - Mensuel par date :
monthly_1,monthly_15,monthly_31(un jour hors limites revient au dernier jour du mois) - Mensuel par semaine :
monthly_any_day_first_week,monthly_any_week_day_last_week,monthly_last_day_of_month - Trimestriel :
quarterly_first_day,quarterly_last_week_day,quarterly_any_day_first_week
Un tableau de jours doit toujours être associé à un type de fréquence — [MO, FR] seul n'est pas valide.
Un appel complet, puisque la chaîne de fréquence est la partie difficile :
python3 motion.py recurring-tasks create \
--name "Revue hebdomadaire" --workspace-id ws_1 --assignee-id usr_1 \
--frequency "weekly_specific_days_[MO,FR]" \
--deadline-type SOFT --duration 30 --ideal-time 16:00 \
--schedule "Work Hours" --priority MEDIUM
Notez les ensembles plus restreints ici : les tâches récurrentes ne prennent que la priorité HIGH/MEDIUM et seulement
les types d'échéance HARD/SOFT, alors que le reste de l'API autorise également ASAP/LOW et NONE.
Le CLI applique les deux.
Commentaires
| Commande | Endpoint |
|---|---|
comments list --task-id … | GET /v1/comments |
comments create --task-id … [--content …] | POST /v1/comments |
--content est du Markdown GitHub Flavored à l'envoi et revient en HTML. Il est
optionnel — sans --content, le corps de la requête ne porte que l'identifiant de la tâche. Nous n'avons pas
vérifié si Motion accepte un commentaire vide ; passez --content quand vous voulez dire quelque chose.
Champs personnalisés — notez le préfixe /beta/
| Commande | Endpoint |
|---|---|
custom-fields list --workspace-id … | GET /beta/workspaces/{workspaceId}/custom-fields |
custom-fields create --workspace-id … --type … --name … | POST /beta/workspaces/{workspaceId}/custom-fields |
custom-fields delete <id> --workspace-id … --yes | DELETE /beta/workspaces/{workspaceId}/custom-fields/{id} |
custom-field-values add-task <task_id> … | POST /beta/custom-field-values/task/{taskId} |
custom-field-values add-project <project_id> … | POST /beta/custom-field-values/project/{projectId} |
custom-field-values delete-from-task <task_id> <value_id> --yes | DELETE /beta/custom-field-values/task/{taskId}/custom-fields/{valueId} |
custom-field-values delete-from-project <project_id> <value_id> --yes | DELETE /beta/custom-field-values/project/{projectId}/custom-fields/{valueId} |
Les 12 types de champs personnalisés, exactement comme orthographiés : text, url, date, person,
multiPerson, phone, select, multiSelect, number, email, checkbox, relatedTo.
Définir une valeur nécessite --custom-field-instance-id (le champ sur l'espace de travail), --type,
et --value. --value est du JSON, donc citez-le correctement : --value '"bonjour"' pour une
chaîne, --value '42' pour un nombre, --value '["A","B"]' pour une multi-sélection, --value 'true' pour
une case à cocher.
python3 motion.py custom-field-values add-task tsk_123 \
--custom-field-instance-id cfi_1 --type select --value '"Alpha"'
custom-fields create prend --metadata en JSON — format (plain/formatted/percent,
nombres uniquement), toggle pour les cases à cocher, et options pour les champs de sélection :
--metadata '{"options":[{"id":"a","value":"Alpha","color":"#ff0000"}]}'
Utilisateurs, espaces de travail, statuts, plannings
| Commande | Endpoint |
|---|---|
me | GET /v1/users/me |
users list [--workspace-id …] [--team-id …] | GET /v1/users |
workspaces list [--ids …] | GET /v1/workspaces |
statuses list [--workspace-id …] | GET /v1/statuses |
schedules list | GET /v1/schedules |
workspaces list --ids est répétable, et --all suit le curseur.
users list doit être cadré. Passez --workspace-id ou --team-id ; le script refuse un
appel non cadré tôt avec ce message plutôt que de dépenser une requête sur le 400 que l'API
retourne. Pour lister tout le monde, bouclez sur vos espaces de travail.
schedules list retourne les fenêtres d'heures de travail, pas les événements. Chaque planning a son propre
timezone et un tableau par jour de blocs {start, end} — et un jour peut contenir plusieurs
blocs (par exemple, un planning scolaire avec un bloc le matin et un le soir). Ne lisez pas cela comme
"l'utilisateur a un bloc de travail par jour", et ne le traitez pas comme le calendrier ; pour ce qui est
effectivement réservé, lisez les valeurs scheduledStart des tâches.
Escape hatch brute
python3 motion.py raw GET /v1/tasks --query status=Todo --query status=Doing
python3 motion.py raw GET /v1/tasks --query assigneeId=u1 --query includeAllStatuses=true --all
python3 motion.py raw POST /v1/comments --body '{"taskId":"t1","content":"salut"}'
python3 motion.py raw DELETE /v1/tasks/t1 --yes
Utilisez cela pour tout ce que les commandes nommées ne couvrent pas. --query KEY=VALUE est répétable et répéter le
même KEY les envoie tous (l'API les traite comme un tableau implicite).
--body accepte soit un texte JSON inline, soit un chemin de fichier préfixé par @ (--body @payload.json).
Toujours spécifier --endpoint. --all est disponible ici aussi : il récupère toutes les pages et rassemble les
résultats en un seul tableau, exactement comme les commandes de liste dédiées.
Chaque requête brute porte le comportement de base : X-API-Key, User-Agent, backoff sur 429, et
--yes obligatoire pour les suppressions.
Différences en direct
Deux pièges qui ont causé des bugs réels pendant le développement de ce client et qui pourraient surprendre les futurs utilisateurs.
1. Les booléens en minuscules (encore). Motion analyse les paramètres de requête. Les
True et False de Python sont des noms en anglais ; la chaîne littérale true ne l'est pas.
Envoyer includeAllStatuses=True dans une requête crée un paramètre inattendu que Motion
n'analyse pas comme un booléen : la réponse est un 400 qui dit que allStatuses=false a
été reçu. Le client envoie toujours des littéraux en minuscules, donc cela ne se produit que si vous
écrivez votre propre appel avec requests.
2. L'utilisateur virtuel dans les assignations. Motion crée un utilisateur motion pour
le bot ou le système et l'utilise comme valeur par défaut pour certaines tâches sans personne
assignée. Les listes d'utilisateurs incluent "motion" avec un email comme motion@…. L'assigner
à une tâche (où c'est autorisé) ne produit pas d'erreur mais n'est pas vraiment utile — filtrez-le
si vous cherchez un humain réel.