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

  1. In Campfire, open Account Settings. On the Account tab, open Connected apps & API access.
  2. Pick the organization the token is for. A token works for one unit.
  3. Give it a Token name you will recognize later, such as "Roster sync".
  4. 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.
  5. Choose when it expires under Expires after (7, 30 or 90 days).
  6. Tap Create token. If you signed in a while ago, Campfire asks you to confirm your password first.
  7. 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 appScopeLets the tokenAlso needs, at request time
Rostermembers:read_basicRead members: names, adult or youth, role ids, patrols, den, departure date.Nothing more.
Contact detailsmembers:read_contactAdd 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.
Advancementadvancement:readRead a member's ranks, badges, awards, adventures and requirement dates.Your own family, or advancement:view_all.
Activitiesactivities:readRead a member's camping nights, service hours and miles.Same as Advancement.
Events and RSVPsevents:readRead events and who has RSVPed.Nothing more. Every member sees every event and RSVP in the app.
Family accountsfinances:readRead 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 dataimports:writePreview, 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 paymentspayments:writeRecord, 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. memberId always 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 as 2026-11-07T14:00:00.000Z.
  • Money is whole cents: 4500 is $45.00.
  • Lists take limit (1 to 50, default 25) and cursor. Each page has nextCursor; pass it back unchanged to get the next page. It is null on 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 from and to date 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-..."
}
StatusCodeMeans
400invalid_argument, invalid_cursorSomething in the request is wrong; the message says what. A cursor that expired or came from another list is invalid_cursor.
401unauthenticatedNo token, a malformed one, or one that expired or was revoked.
403access_deniedYour role doesn't allow this, or you are no longer an active adult member of the unit.
403insufficient_scopeThe token wasn't given the scope this needs.
403feature_disabledThe unit has this turned off, such as finances or health forms, or isn't on Campfire Pro.
404not_foundIt doesn't exist, or you can't see it.
405method_not_allowedWrong method for this path. The Allow header lists the right ones.
409plan_changed, import_running, not_undoableAn import can't go ahead as asked. See Import rows.
409reference_conflict, month_closedA payment can't be recorded or voided as asked. See Record payments.
410import_expiredThe preview is more than an hour old. Preview again.
413payload_too_largeThe file is over 10 MB.
429rate_limitedToo many requests. Wait the number of seconds in Retry-After.
503unavailableTry 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.
504noneThe 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

WhatLimit
Every request60 a minute per token, 120 a minute per account, 600 a minute per unit, 120 a minute per IP address
Import previews20 an hour per account
Import applies and undos, together10 an hour per unit, and one import running at a time per unit
Payments recorded120 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.

Did this help?

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

Contact Support