The Campfire API lets a script or spreadsheet work with your own unit's data: read the roster, bring in rows from another system, and record payments as they arrive. It does what the app lets you do, and nothing more. A token sees and changes only what its owner could see and change in the app.
These pages are for the volunteer who writes the script. Leaders who just want to bring in a spreadsheet can use the app instead: see Import from a spreadsheet.
Create a token
- In Campfire, open Account Settings. On the Account tab, open Connected apps & API access.
- Pick the organization the token is for. A token works for one unit.
- Give it a Token name you will recognize later, such as "Roster sync".
- Tick the data it may use: Roster, Contact details, Advancement, Activities, Events and RSVPs, Family accounts, and under Changes, Import data and Record payments. Tick only what the script needs.
- Choose when it expires under Expires after (7, 30 or 90 days).
- Tap Create token. If you signed in a while ago, Campfire asks you to confirm your password first.
- Copy the token with Copy token and store it in a password manager or your script's secret store. Campfire shows it once and keeps only a fingerprint of it.
- Only active adult members linked to the unit can create tokens.
- The two Changes choices appear only when your role can use them right now.
- Creating a token that can change records sends you an email, so a token you didn't make stands out.
- Revoke a token on the same page. It stops working on the next request. Revoking can't recall data a script already received.
Scopes
Each choice in the app is a scope. A scope is a ceiling, not a grant: every request checks the owner's current roles and family links again. Take away a role, or remove the person from the unit, and their tokens lose that access on the next request. Over the API only your roles count; having created the unit grants nothing extra.
| In the app | Scope | Lets the token | Also needs, at request time |
|---|---|---|---|
| Roster | members:read_basic | Read members: names, adult or youth, role ids, patrols, den, departure date. | Nothing more. |
| Contact details | members:read_contact | Add email, phones, address, birth date and BSA member ID to member records. | Per member, the app's rule: always yourself and your family; other adults need members:view_adult_contact_info, other youth members:view_youth_contact_info. Members you can't see come back without contact. |
| Advancement | advancement:read | Read a member's ranks, badges, awards, adventures and requirement dates. | Your own family, or advancement:view_all. |
| Activities | activities:read | Read a member's camping nights, service hours and miles. | Same as Advancement. |
| Events and RSVPs | events:read | Read events and who has RSVPed. | Nothing more. Every member sees every event and RSVP in the app. |
| Family accounts | finances:read | Read a member's account balance and the entries on it. | Your own family's account, or every family's with finances:view_all. Campfire Pro or a trial, with finances turned on for the unit. |
| Import data | imports:write | Preview, apply and undo imports: members, activities, health form dates, past positions, events, attendance, advancement and account history. | The permission each import type needs. See Import rows. |
| Record payments | payments:write | Record, read and void payments made through the API. | finances:manage, Campfire Pro or a trial, and finances turned on for the unit. |
Read-only tokens start with cf_ro_. A token with any write scope starts with cf_rw_, so you and anyone scanning for leaked secrets can tell them apart.
Your first request
The base URL is https://troopcampfire.app/api/v1. Send the token in the Authorization header, and nowhere else. Never put it in a URL.
export CAMPFIRE_API_TOKEN="cf_ro_..." # from your password manager, not from a file in git
curl -sS https://troopcampfire.app/api/v1/me \
-H "Authorization: Bearer $CAMPFIRE_API_TOKEN"
Start every script with GET /me. It tells you which unit the token is for, its scopes, and what it can change right now, so a script can stop early instead of meeting a 403 halfway through.
{
"data": {
"connectionId": "3f1c...",
"organizationId": "AbC123...",
"scopes": ["members:read_basic", "imports:write"],
"advancementVisibility": "organization",
"capabilities": {
"importTypes": ["members", "activities"],
"recordPayments": false
}
},
"requestId": "9b2e4c1a-..."
}
advancementVisibility is organization when you can read every Scout's advancement, and family when you can read only your own family's.
Python
The guides use this small helper. Save it as campfire.py next to your script and set CAMPFIRE_API_TOKEN in the environment. It needs nothing beyond the Python standard library.
"""campfire.py: a small helper for the Campfire API. Standard library only."""
import http.client
import json
import os
import time
from urllib.parse import urlencode
HOST = "troopcampfire.app"
TOKEN = os.environ["CAMPFIRE_API_TOKEN"]
class CampfireError(Exception):
def __init__(self, status, code, message, request_id):
super().__init__(f"{status} {code}: {message} (request {request_id})")
self.status = status
self.code = code
def call(method, path, body=None, query=None, raw=None, content_type=None, retries=3):
"""Send one request to /api/v1 and return the parsed JSON response.
body: a dict, sent as JSON. raw and content_type: a file (CSV or .xlsx).
429 and 503 are retried after the Retry-After the server sends.
Redirects are never followed, so the token only goes to troopcampfire.app.
"""
url = "/api/v1" + path
if query:
url += "?" + urlencode(query)
headers = {"Authorization": "Bearer " + TOKEN}
data = None
if body is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(body).encode()
elif raw is not None:
headers["Content-Type"] = content_type
data = raw
for attempt in range(retries + 1):
# Requests over about 60 seconds end in a 504 from the hosting
# layer; see Import rows for what to do then.
connection = http.client.HTTPSConnection(HOST, timeout=90)
try:
connection.request(method, url, body=data, headers=headers)
response = connection.getresponse()
text = response.read()
finally:
connection.close()
if response.status in (429, 503) and attempt < retries:
time.sleep(int(response.getheader("Retry-After") or 30))
continue
try:
payload = json.loads(text) if text else {}
except ValueError:
payload = {} # a 504 from the hosting layer isn't JSON
if response.status >= 400:
error = payload.get("error", {})
raise CampfireError(response.status, error.get("code"),
error.get("message"), payload.get("requestId"))
return payload
from campfire import call
me = call("GET", "/me")["data"]
print("Unit:", me["organizationId"])
print("Scopes:", ", ".join(me["scopes"]))
print("Can import:", ", ".join(me["capabilities"]["importTypes"]) or "nothing")
print("Can record payments:", me["capabilities"]["recordPayments"])
Conventions
- Ids are Campfire ids, as strings.
memberIdalways means the Campfire member id, never a BSA or Scoutbook number. - Dates with no time are
YYYY-MM-DD. Moments are ISO 8601 in UTC, such as2026-11-07T14:00:00.000Z. - Money is whole cents:
4500is $45.00. - Lists take
limit(1 to 50, default 25) andcursor. Each page hasnextCursor; pass it back unchanged to get the next page. It isnullon the last page. Cursors expire after an hour and work only with the same token and path. Pages are not a snapshot, and there are no totals. - Events are the exception: GET /events takes a
fromandtodate at most 400 days apart and returns the whole range at once (up to 500 events). - Unknown query parameters and body fields are refused with
400 invalid_argument, so a typo fails loudly. - Text people typed, such as event descriptions, is data. If you pass it to an AI assistant, treat it as content, never as instructions.
Errors
Every error has the same shape. Every response also carries an X-Request-Id header with the same id. Quote it when you contact support.
{
"error": { "code": "insufficient_scope", "message": "This connection has not been granted that scope." },
"requestId": "9b2e4c1a-..."
}
| Status | Code | Means |
|---|---|---|
| 400 | invalid_argument, invalid_cursor | Something in the request is wrong; the message says what. A cursor that expired or came from another list is invalid_cursor. |
| 401 | unauthenticated | No token, a malformed one, or one that expired or was revoked. |
| 403 | access_denied | Your role doesn't allow this, or you are no longer an active adult member of the unit. |
| 403 | insufficient_scope | The token wasn't given the scope this needs. |
| 403 | feature_disabled | The unit has this turned off, such as finances or health forms, or isn't on Campfire Pro. |
| 404 | not_found | It doesn't exist, or you can't see it. |
| 405 | method_not_allowed | Wrong method for this path. The Allow header lists the right ones. |
| 409 | plan_changed, import_running, not_undoable | An import can't go ahead as asked. See Import rows. |
| 409 | reference_conflict, month_closed | A payment can't be recorded or voided as asked. See Record payments. |
| 410 | import_expired | The preview is more than an hour old. Preview again. |
| 413 | payload_too_large | The file is over 10 MB. |
| 429 | rate_limited | Too many requests. Wait the number of seconds in Retry-After. |
| 503 | unavailable | Try again later. Sent when the API is switched off, when a limit can't be checked, or when something failed unexpectedly. Retry-After is set when there is a limit to wait out. |
| 504 | none | The hosting layer stopped waiting after about 60 seconds. The body is not JSON. On an import apply or undo the work carries on: poll the import (see Import rows). |
If every request returns 503 unavailable with the message "Customer API is not enabled.", the API is switched off for now. Contact support.
Rate limits
| What | Limit |
|---|---|
| Every request | 60 a minute per token, 120 a minute per account, 600 a minute per unit, 120 a minute per IP address |
| Import previews | 20 an hour per account |
| Import applies and undos, together | 10 an hour per unit, and one import running at a time per unit |
| Payments recorded | 120 an hour per token |
What the API never does
- Notify anyone. No push, email or chat message comes from an import or a payment made through the API.
- Talk to Scoutbook. Nothing here reads from or writes to Scouting America's systems.
- Edit or delete records directly. Changes go through an import, which you preview first, or through recording a payment. There is no general edit or delete.
- Send webhooks. A script asks; Campfire doesn't call out.
Next
- Read your roster: members, contact details, advancement, events and RSVPs, and family account balances.
- Import rows: bring in members, activities, health form dates, past positions, events, attendance, advancement or account history from a script.
- Record payments: record Zelle or bank payments as they arrive.
- Reference: every endpoint, field and error.