Facebook Pixel
Fit'DistanceDocumentation
Open the app
On this page

Write a script

A Python or JavaScript script that reads every page, rereads only the changes, writes without duplicates and recovers from a 429.

Python uses requests, JavaScript fetch (recent Node.js). Each step adds to the same file.

1. Keep the key out of the code

Give your key to the script through an environment variable, never in the file. On Mac and Linux, install requests, then pass the key at launch:

pip install requests
FITDISTANCE_KEY="fd_live_xxxxxxxx" python3 script.py
FITDISTANCE_KEY="fd_live_xxxxxxxx" node script.mjs

2. Make the first call

call adds your key and retries after a 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("Too many 429s")


print(call("GET", "/me")["credits"])

JavaScript (.mjs file)

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("Too many 429s");
}

console.log((await call("GET", "/me")).credits);

3. Read every page

Each list returns data, has_more and next_cursor. As long as has_more is true, call the route again with cursor set to next_cursor. A page costs one credit, whatever its number of students, so ask for limit=200, the maximum allowed.

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. Reread only the changes

Keep the greatest updated_at you read and pass it as since next time: you get the objects whose updated_at is greater than or equal. The test is inclusive, so the last object you read comes back. Spot it by its id. A student's updated_at also moves at each login, so a daily sync receives the students who logged in again. Follow the pages as in step 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. Write without duplicates

A write goes through POST /operations/<name>, with a JSON body and an Idempotency-Key header. This example marks a conversation as read and needs Messages › Write (messaging:write), or you get a 403 SCOPE_MISSING. The threadUid is the uid field of a conversation from messaging.threads_list, which needs Messages › Read (messaging:read).

Python

call(
    "POST",
    "/operations/messaging.mark_read",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={"threadUid": "uid-of-the-conversation"},
)

JavaScript

await call("POST", "/operations/messaging.mark_read", {
  headers: { "Idempotency-Key": randomUUID() },
  body: { threadUid: "uid-of-the-conversation" },
});

Idempotency-Key is a string that is unique to each write, such as a UUID. Generate it once per write and reuse it on every new attempt. The same key with the same body runs once, and the replay returns the first response with Idempotency-Replayed: true, free. If the script restarts, it sends a new key and the write can happen twice. Store the key when that matters. Another body with the same key gives a 409 IDEMPOTENCY_MISMATCH. The first successful write of a key emails you the operation and its target.

6. Recover from a 429

A 429 RATE_LIMITED means you are going too fast: 10 calls per second and 300 per minute for a key, including 60 writes and 20 deletes per minute. It costs nothing. Retry-After gives the seconds to wait, and RateLimit-* tells you where you stand. call waits and tries again, five times at most.

Next pageKeys and creditsWhat costs a credit, what is free, where your credits come from and where to follow your usage.