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"
}
}
| Field | What it holds |
|---|---|
id | The Campfire member id. Use it everywhere a memberId is asked for, including imports and payments. |
firstName, lastName, nickName | Names as saved in Campfire. |
isAdult | Always present. Tells a parent from a Scout with the same name. |
roles | Role ids, not names: scoutmaster, treasurer, or your unit's own role ids. Members usually also have default_member. |
patrolIds | Ids of the member's patrols. |
cubDen | The den, for Scouts in a pack. |
leftDate | When 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. |
contact | Only 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 datesdateStarted,dateCompletedandawardedDateas saved,updatedAt, andrequirements: each signed-off requirement's id anddateCompleted. Requirements never signed off are not listed. - Each activity has
type(such ascampingorservice),amount(nights, hours or miles),date,eventIdwhen an event awarded it, andisManual.
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 andpaymentDueAt. locationis the saved location's name and address, ornull.audienceGroupIdsis empty when the event is for everyone.- Not returned: files, checklists, meeting plans, driver details, and who created the event.
descriptionis 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" }
amountCentsis always positive.effectCentsis what the entry does to the balance: plus for a charge, minus for a payment, credit or adjustment.- Only
completedentries count toward the balance. A bank payment ispendinguntil it settles. dateis 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.