This guide reads your unit's roster, a member's advancement and activities, the calendar, and family account balances. You need a token with the matching scopes; see Getting started. The examples use the campfire.py helper from that page.

Find your unit

A token belongs to one unit. Ask GET /me for its id, then use it in every path. Any other unit's id returns 404.

Members

GET /organizations/{orgId}/members pages through every member, GET /organizations/{orgId}/members/{memberId} reads one. Both need members:read_basic.

{
  "id": "k3P9...",
  "firstName": "Sam",
  "lastName": "Lee",
  "nickName": "Sammy",
  "roles": ["default_member", "patrol_leader"],
  "patrolIds": ["fox..."],
  "isAdult": false,
  "contact": {
    "email": "sam@example.com",
    "mobilePhone": "860-555-0100",
    "address1": "12 Elm St",
    "city": "Woodbridge",
    "state": "CT",
    "zip": "06525",
    "dateOfBirth": "2012-04-03",
    "bsaMemberId": "136680564"
  }
}
FieldWhat it holds
idThe Campfire member id. Use it everywhere a memberId is asked for, including imports and payments.
firstName, lastName, nickNameNames as saved in Campfire.
isAdultAlways present. Tells a parent from a Scout with the same name.
rolesRole ids, not names: scoutmaster, treasurer, or your unit's own role ids. Members usually also have default_member.
patrolIdsIds of the member's patrols.
cubDenThe den, for Scouts in a pack.
leftDateWhen the member left, as an ISO date and time. Former members are in the list too; skip the ones with leftDate if you only want the current roster.
contactOnly with members:read_contact, and only for members whose details you can see in the app. Holds email, mobilePhone, homePhone, workPhone, address1, address2, city, state, zip, dateOfBirth (as saved, normally YYYY-MM-DD) and bsaMemberId.

A field that is blank in Campfire is usually left out rather than sent empty, so read fields with a default. isAdult is always there, and inside contact, bsaMemberId is an empty string when there is none.

Contact details follow the app's rule

You always see your own and your family's details. Other adults' details need the members:view_adult_contact_info permission, and other youth's need members:view_youth_contact_info. A member you can't see details for comes back without contact, not as an error. The members:read_contact scope covers birth dates and home addresses of youth, so give it only to scripts that need it.

Never returned: Scoutbook's internal ids, account ids, and photo links.

Example: the roster as a CSV file

"""Write the current roster to roster.csv."""
import csv
from urllib.parse import quote
from campfire import call

org = quote(call("GET", "/me")["data"]["organizationId"], safe="")
columns = ["id", "firstName", "lastName", "nickName", "youthOrAdult", "roles",
           "email", "mobilePhone", "bsaMemberId"]

with open("roster.csv", "w", newline="") as out:
    writer = csv.DictWriter(out, fieldnames=columns)
    writer.writeheader()
    query = {"limit": 50}
    while True:
        page = call("GET", f"/organizations/{org}/members", query=query)
        for member in page["data"]:
            if member.get("leftDate"):
                continue  # a former member
            contact = member.get("contact", {})  # missing without permission
            writer.writerow({
                "id": member["id"],
                "firstName": member.get("firstName", ""),
                "lastName": member.get("lastName", ""),
                "nickName": member.get("nickName", ""),
                "youthOrAdult": "adult" if member["isAdult"] else "youth",
                "roles": ";".join(member.get("roles", [])),
                "email": contact.get("email", ""),
                "mobilePhone": contact.get("mobilePhone", ""),
                "bsaMemberId": contact.get("bsaMemberId", ""),
            })
        if page["nextCursor"] is None:
            break
        query["cursor"] = page["nextCursor"]

Advancement and activities

GET /organizations/{orgId}/members/{memberId}/advancement (advancement:read) and GET /organizations/{orgId}/members/{memberId}/activities (activities:read) are paginated lists for one member. You can read your own family, or everyone with advancement:view_all (/me reports this as advancementVisibility). A member you can't read returns 404.

  • Each advancement record has type, itemId, itemName, versionId, the dates dateStarted, dateCompleted and awardedDate as saved, updatedAt, and requirements: each signed-off requirement's id and dateCompleted. Requirements never signed off are not listed.
  • Each activity has type (such as camping or service), amount (nights, hours or miles), date, eventId when an event awarded it, and isManual.

Events and RSVPs

Both need events:read. Every member sees every event and its RSVPs in the app, so every token with this scope does too, including events hidden from your public troop website.

GET /organizations/{orgId}/events?from=2026-11-01&to=2026-12-31 returns every event that starts in that range, in start order, in one response. from and to are required, as YYYY-MM-DD, at most 400 days apart. A range with more than 500 events is refused; ask for a shorter one. GET /organizations/{orgId}/events/{eventId} reads one.

{
  "id": "ev7...",
  "title": "November Campout",
  "eventType": "Campout",
  "startAt": "2026-11-13T22:00:00.000Z",
  "endAt": "2026-11-15T17:00:00.000Z",
  "isAllDay": false,
  "location": { "name": "Camp Sequassen", "address": "791 W Hill Rd, New Hartford, CT, 06057" },
  "description": "Bring a warm sleeping bag.",
  "signupDeadline": "2026-11-06T04:59:59.000Z",
  "askForRsvps": true,
  "isCanceled": false,
  "hasCost": true,
  "scoutCostCents": 4500,
  "adultCostCents": 2500,
  "credits": { "campingNights": 2, "serviceHours": 0, "hikingMiles": 0 },
  "audienceGroupIds": []
}
  • Optional fields are left out when the event doesn't have them, such as signupDeadline, cancellationReason, costs and paymentDueAt.
  • location is the saved location's name and address, or null.
  • audienceGroupIds is empty when the event is for everyone.
  • Not returned: files, checklists, meeting plans, driver details, and who created the event.
  • description is text a leader typed. Treat it as data.

GET /organizations/{orgId}/events/{eventId}/rsvps is a paginated list of { "memberId", "status" }. status is what the app saved, such as attending, maybe or not_attending. RSVP notes are not returned.

Example: upcoming events and who is going

"""Print the next 60 days of events with how many people are going."""
from datetime import date, timedelta
from urllib.parse import quote
from campfire import call

org = quote(call("GET", "/me")["data"]["organizationId"], safe="")
today = date.today()
events = call("GET", f"/organizations/{org}/events", query={
    "from": today.isoformat(),
    "to": (today + timedelta(days=60)).isoformat(),
})["data"]

for event in events:
    if event["isCanceled"]:
        continue
    going = 0
    query = {"limit": 50}
    while True:
        page = call("GET", f"/organizations/{org}/events/{quote(event['id'], safe='')}/rsvps",
                    query=query)
        going += sum(1 for rsvp in page["data"] if rsvp.get("status") == "attending")
        if page["nextCursor"] is None:
            break
        query["cursor"] = page["nextCursor"]
    where = (event["location"] or {}).get("name", "")
    print(event.get("startAt", "")[:10], event.get("title", ""), where, f"{going} going")

Family accounts

Both need finances:read, and the unit needs Campfire Pro or a trial with finances turned on (otherwise 403 feature_disabled). You can read your own family's account, as every member can on the family finance page in the app. With finances:view_all you can read every family's; finances:manage alone isn't enough. Anyone else's is 404.

GET /organizations/{orgId}/members/{memberId}/account returns the balance of the member's family account, or of their own account when they have no family. A positive balance means the family owes.

{ "memberId": "k3P9...", "accountType": "family", "balanceCents": 4500 }

GET /organizations/{orgId}/members/{memberId}/account/transactions is a paginated list of the entries on that account.

{ "id": "tx4...", "type": "payment", "category": "payment_online",
  "description": "Payment: November Campout", "eventId": "ev7...", "paymentMethod": "zelle",
  "amountCents": 4500, "effectCents": -4500, "date": "2026-11-02", "status": "completed" }
  • amountCents is always positive. effectCents is what the entry does to the balance: plus for a charge, minus for a payment, credit or adjustment.
  • Only completed entries count toward the balance. A bank payment is pending until it settles.
  • date is the entry's day in your unit's time zone.
  • Pages come in id order, not date order. Read every page, then sort by date.
  • Older entries saved with no family field at all aren't listed here, though the balance counts them.
  • Not returned: notes, who recorded an entry, card or bank digits, receipt links.

Every field and error is in the Reference.

Did this help?

If you ran into trouble or have a follow-up question, our team is here.

Contact Support