Écrire un script
Un script Python ou JavaScript qui lit toutes les pages, ne relit que les changements, écrit sans doublon et reprend après un 429.
Python utilise requests, JavaScript fetch (Node.js récent). Chaque étape s'ajoute au même fichier.
1. Garde la clé hors du code
Donne ta clé au script par une variable d'environnement, jamais dans le fichier. Sur Mac et Linux, installe requests, puis passe la clé au lancement :
pip install requests
FITDISTANCE_KEY="fd_live_xxxxxxxx" python3 script.py
FITDISTANCE_KEY="fd_live_xxxxxxxx" node script.mjs2. Fais le premier appel
call ajoute ta clé et réessaie après un 429.
Python
import os
import time
import uuid
import requests
BASE = "https://api.fitdistance.io/api/partner/v1"
KEY = os.environ["FITDISTANCE_KEY"]
def call(method, path, headers=None, **kwargs):
for _ in range(5):
res = requests.request(
method,
BASE + path,
headers={"Authorization": f"Bearer {KEY}", **(headers or {})},
timeout=30,
**kwargs,
)
if res.status_code == 429:
time.sleep(int(res.headers.get("Retry-After", "1")))
continue
res.raise_for_status()
return res.json()
raise RuntimeError("Trop de 429")
print(call("GET", "/me")["credits"])JavaScript (fichier .mjs)
import { randomUUID } from "node:crypto";
const BASE = "https://api.fitdistance.io/api/partner/v1";
const KEY = process.env.FITDISTANCE_KEY;
async function call(method, path, { headers, body } = {}) {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
...headers,
},
body: body && JSON.stringify(body),
});
if (res.status === 429) {
const seconds = Number(res.headers.get("Retry-After") ?? 1);
await new Promise((done) => setTimeout(done, seconds * 1000));
continue;
}
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
return res.json();
}
throw new Error("Trop de 429");
}
console.log((await call("GET", "/me")).credits);3. Lis toutes les pages
Chaque liste renvoie data, has_more et next_cursor. Tant que has_more vaut true, rappelle la route avec cursor égal à next_cursor. Une page coûte un crédit, quel que soit son nombre d'élèves : demande limit=200, le maximum autorisé.
Python
students, cursor, has_more = [], None, True
while has_more:
page = call("GET", "/students", params={"limit": 200, "cursor": cursor})
students += page["data"]
has_more, cursor = page["has_more"], page["next_cursor"]JavaScript
const students = [];
let cursor;
let hasMore = true;
while (hasMore) {
const query = new URLSearchParams({ limit: "200" });
if (cursor) query.set("cursor", cursor);
const page = await call("GET", `/students?${query}`);
students.push(...page.data);
hasMore = page.has_more;
cursor = page.next_cursor;
}4. Ne relis que les changements
Garde le plus grand updated_at lu et passe-le ensuite dans since : tu reçois les objets dont updated_at est supérieur ou égal. Le test est inclusif. Le dernier objet lu revient donc, repère-le par son id. Le updated_at d'un élève bouge aussi à chaque connexion, donc une synchro quotidienne revoit les élèves connectés. Suis les pages comme à l'étape 3.
Python
since = max(s["updated_at"] for s in students)
changed = call("GET", "/students", params={"since": since, "limit": 200})["data"]JavaScript
const since = students.map((s) => s.updated_at).sort().at(-1);
const query = new URLSearchParams({ since, limit: "200" });
const changed = (await call("GET", `/students?${query}`)).data;5. Écris sans doublon
Une écriture passe par POST /operations/<nom>, avec un corps JSON et un en-tête Idempotency-Key. Cet exemple marque une conversation comme lue ; il demande Messages › Écrire (messaging:write), sinon tu reçois un 403 SCOPE_MISSING. Le threadUid est le champ uid d'une conversation de messaging.threads_list, qui demande Messages › Lire (messaging:read).
Python
call(
"POST",
"/operations/messaging.mark_read",
headers={"Idempotency-Key": str(uuid.uuid4())},
json={"threadUid": "uid-de-la-conversation"},
)JavaScript
await call("POST", "/operations/messaging.mark_read", {
headers: { "Idempotency-Key": randomUUID() },
body: { threadUid: "uid-de-la-conversation" },
});Idempotency-Key est une chaîne unique par écriture, comme un UUID. Génère-la une fois par écriture et réutilise-la à chaque nouvel essai. La même clé avec le même corps ne tourne qu'une fois, et le rejeu renvoie la première réponse avec Idempotency-Replayed: true, sans coût. Si le script redémarre, il envoie une nouvelle clé et l'écriture peut avoir lieu deux fois. Garde la clé de côté quand c'est important. Un autre corps avec la même clé donne un 409 IDEMPOTENCY_MISMATCH. La première écriture réussie d'une clé t'envoie un e-mail qui nomme l'opération et sa cible.
6. Reprends après un 429
Un 429 RATE_LIMITED veut dire que tu vas trop vite : 10 appels par seconde et 300 par minute pour une clé, dont 60 écritures et 20 suppressions par minute. Il ne coûte rien. Retry-After donne les secondes à attendre, et RateLimit-* dit où tu en es. call attend puis réessaie, cinq fois au plus.