> ## Documentation Index
> Fetch the complete documentation index at: https://developers.scoutworks.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Attendance for a term

> A register for each section: one row for each young person, one column for each meeting, and their attendance as a percentage.

This recipe builds a term’s registers from Scoutworks. It saves one CSV file for each section. Open them in Excel, Numbers or Google Sheets. Use them to spot a young person who has stopped coming, or to check who met the attendance rule for an award.

It uses Python 3.9 or later and the `requests` package (`pip install requests`).

```python attendance.py theme={null}
import csv
import os
import re
import time
from collections import defaultdict

import requests

# The term. Dates are in UTC.
TERM_START = "2026-09-01T00:00:00Z"
TERM_END = "2026-12-20T00:00:00Z"

BASE = "https://api-demo.scoutworks.app/api/v1"
API_KEY = os.environ.get(
    "SCOUTWORKS_API_KEY",
    "sw_live_473adcc6c7236916021f0f24559db36576a53bab071d0f0c08a025adb510b6ab",
)
session = requests.Session()
session.headers["Authorization"] = f"Bearer {API_KEY}"


def get(url, params):
    """GET one page. Waits and tries again if the rate limit is reached."""
    while True:
        res = session.get(url, params=params, timeout=30)
        if res.status_code == 429:
            time.sleep(res.json().get("retryAfter", 30))
            continue
        if not res.ok:
            error = res.json()["error"]
            raise SystemExit(f"{res.status_code} {error['code']}: {error['message']}")
        return res.json()


def read_all(path, **filters):
    """Every item in a list, following nextCursor to the last page."""
    items, params = [], {"limit": 200, **filters}
    while True:
        page = get(BASE + path, params)
        items += page["data"]
        if page["nextCursor"] is None:
            return items
        params["cursor"] = page["nextCursor"]


def safe(value):
    """Stops a spreadsheet running a cell that starts with = + - or @."""
    text = str(value)
    return "'" + text if text.lstrip().startswith(("=", "+", "-", "@")) else text


meetings = [
    e for e in read_all("/events", **{"from": TERM_START, "to": TERM_END})
    if e["type"] == "programme_meeting"
]
print(f"{len(meetings)} meetings. Reading the registers…")

# section → member id → { "name": …, meeting id → "P" or "A" }
registers = defaultdict(dict)
# section → the meetings that section has a register for
section_meetings = defaultdict(list)

for meeting in meetings:
    seen = set()
    for a in read_all(f"/events/{meeting['id']}/attendees"):
        if a["attendance"] is None or a["section"] is None:
            continue  # Not taken yet.
        section = a["section"]["name"]
        row = registers[section].setdefault(
            a["member"]["id"],
            {"name": (a["member"]["lastName"], a["member"]["firstName"])},
        )
        row[meeting["id"]] = "P" if a["attendance"] == "present" else "A"
        seen.add(section)
    for section in seen:
        section_meetings[section].append(meeting)

used = set()
for section, rows in sorted(registers.items()):
    dates = section_meetings[section]
    stem = "attendance-" + re.sub(r"[^a-z0-9]+", "-", section.lower()).strip("-")
    filename, n = stem + ".csv", 2
    while filename in used:  # Two names like "Cubs 1" and "Cubs-1" must not share a file.
        filename, n = f"{stem}-{n}.csv", n + 1
    used.add(filename)
    with open(filename, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.writer(f)
        writer.writerow(["Last name", "First name"] + [m["startsAt"][:10] for m in dates] + ["Present", "Attendance"])
        for row in sorted(rows.values(), key=lambda r: r["name"]):
            marks = [row.get(m["id"], "") for m in dates]
            present, taken = marks.count("P"), marks.count("P") + marks.count("A")
            writer.writerow(
                [safe(row["name"][0]), safe(row["name"][1])]
                + marks
                + [f"{present} of {taken}", f"{round(100 * present / taken)}%"]
            )
    print(f"Saved {filename} with {len(dates)} meetings")
```

Run it:

```bash theme={null}
python attendance.py
```

Each file has `P` for present, `A` for absent and a blank for a meeting where the young person was not on the register. The last column is their attendance for the meetings where a register was taken.

## Choose the term

Change `TERM_START` and `TERM_END` at the top. `TERM_END` is the first moment **after** the term, so `2026-12-20T00:00:00Z` includes every meeting up to the end of 19 December.

## Switch to your group

Change `api-demo.scoutworks.app` to `api.scoutworks.app`, and set your key in the environment. `read -s` asks for the key without showing it, so it stays out of your shell history:

```bash theme={null}
read -rs -p "Scoutworks API key: " SCOUTWORKS_API_KEY && export SCOUTWORKS_API_KEY
python attendance.py
```

Your key needs the `events:read` scope only.

## Notes

* **How many requests.** The script makes one request for the events, then one for each meeting. A group with six sections has about 80 meetings in a term. That is below the limit of 120 a minute. If it goes over, the script waits and goes on. See [Rate limits](/guides/rate-limits).
* **Trips and camps.** The script counts meetings only. To count every event, remove the `if e["type"] == "programme_meeting"` line.
* **The `utf-8-sig` encoding** makes Excel show accented names correctly.
* The API only lists events that are published. A meeting still in draft has no register here.
