Some units watch a mailbox: when the bank or Zelle emails that a family paid, a script records the payment in Campfire. This endpoint is for that kind of automation. It records exactly what Record Payment does in the app, with no preview step, and it is safe to retry.

You need a token with payments:write (Record payments in the app), the finances:manage permission, Campfire Pro or a trial, and finances turned on for the unit. The examples use the campfire.py helper from Getting started.

Record a payment

POST /organizations/{orgId}/payments

POST /api/v1/organizations/{orgId}/payments
Content-Type: application/json

{
  "memberId": "k3P9...",
  "amountCents": 4500,
  "method": "zelle",
  "paidOn": "2026-11-02",
  "eventId": "ev7...",
  "description": "Zelle from J. Smith",
  "reference": "ZL-3F9A2C71"
}
FieldRules
memberId (required)Any member of the unit, as a Campfire id. The payment goes on their family's account, or on their own account when they have no family.
amountCents (required)Whole cents, 1 to 1,000,000.
method (required)cash, check, zelle, venmo, cash_app, paypal or ach. Card payments come only through Campfire's own online payments.
paidOn (required)Exactly YYYY-MM-DD. Up to a day ahead is allowed, for time zones. At most a year back.
eventIdTies the payment to an event, so the event's readiness list shows the family as paid. Leave it out, or send null, for no event.
descriptionUp to 200 characters. Defaults to "Payment", or "Payment: " and the event's title.
reference (required)Up to 128 characters: the bank's or app's confirmation number. One payment per reference in the unit.

The answer is 201 with the payment:

{
  "data": {
    "id": "api_6f0c2d9a1b7e4c3f8a2d1e0b",
    "memberId": "k3P9...",
    "accountType": "family",
    "amountCents": 4500,
    "method": "zelle",
    "paidOn": "2026-11-02",
    "eventId": "ev7...",
    "description": "Zelle from J. Smith",
    "reference": "ZL-3F9A2C71",
    "createdAt": "2026-11-02T15:04:05.000Z"
  },
  "requestId": "..."
}
  • id starts with api_.
  • accountType is family or individual.
  • createdAt is when Campfire recorded it; paidOn is the day it was paid.
  • The family's balance goes down in the same step. The payment is saved as completed, marked as recorded through the API, with the token's owner as the person who recorded it.
  • Partial payments are just smaller amounts. The event's readiness list already shows what is still owed.
  • Nobody is notified.

Retries are safe

reference fixes the payment's id. That makes retrying safe, even after a timeout where you don't know whether the first try landed:

You sendCampfire answers
A new reference201, recorded.
The same reference and the same fields again200 with the payment already recorded. Nothing is added. This answer comes before any other check, so it holds even if the month has since been closed, the member has left or the date is now over a year back.
The same reference with a different member, amount, method, date, event or description409 reference_conflict. A person should look.

A script that reads the same emails twice can never record a payment twice, as long as it sends the confirmation number as reference.

Closed months

When the treasurer has closed the books for a month, a new payment dated in it is refused with 409 month_closed, and so is voiding one. Ask the treasurer to reopen the month in the app first. Campfire reads the end of a closed month as the treasurer's local date, so the 1st of the next month stays open. A retry of a payment that was recorded before the close still gets its 200.

Read and void

  • GET /organizations/{orgId}/payments/{paymentId} returns the payment, as above.
  • DELETE /organizations/{orgId}/payments/{paymentId} voids it: the payment is removed and the amount is credited back to the family the payment is on now (if the member has moved to another family since, that family). The answer is {"voided": true}. After a void, the same reference can be recorded again.
  • Both work only on payments recorded through the API. Payments recorded in the app or paid online return 404; change those in the app.
  • Both need payments:write and the same permission and plan as recording.
curl -sS -X DELETE \
  -H "Authorization: Bearer $CAMPFIRE_API_TOKEN" \
  https://troopcampfire.app/api/v1/organizations/$ORG_ID/payments/api_6f0c2d9a1b7e4c3f8a2d1e0b
# {"data":{"voided":true},"requestId":"..."}

Errors to expect

ResponseMeans
400 invalid_argumentA field is missing or out of range, or no member or event in the unit has that id. The message says which.
403 insufficient_scopeThe token doesn't have payments:write.
403 access_deniedThe token's owner no longer has finances:manage.
403 feature_disabledThe unit isn't on Campfire Pro, or finances are turned off.
409 reference_conflictThat reference was already used for a different payment.
409 month_closedThe date is in a closed month.
429 rate_limitedMore than 120 new payments in an hour from this token (retries of a recorded payment don't count). Wait Retry-After seconds.

Example: Zelle emails

Matching a Zelle sender to a family is the script's job. Zelle emails usually carry the sender's name, so keep a small table from sender to a member of that family. A token with Contact details can help you build it from the roster's emails; see Read your roster. Anything the script can't match should go to a person, not a guess.

"""Record Zelle payments parsed from the bank's notification emails.

Reading the mailbox and parsing each email is up to you; this part takes
(sender, amount in cents, date, confirmation number) and records it.
"""
from urllib.parse import quote
from campfire import call, CampfireError

org = quote(call("GET", "/me")["data"]["organizationId"], safe="")

# Who each Zelle sender is: any member of the paying family will do, since
# the payment lands on the family account. Build this once (from the roster,
# or by hand) and add to it when a new family pays.
SENDERS = {
    "JANE SMITH": "k3P9...",
}


def record(sender, amount_cents, paid_on, confirmation):
    member_id = SENDERS.get(sender.strip().upper())
    if member_id is None:
        return "unknown sender, needs a person"
    try:
        result = call("POST", f"/organizations/{org}/payments", body={
            "memberId": member_id,
            "amountCents": amount_cents,
            "method": "zelle",
            "paidOn": paid_on,
            "description": f"Zelle from {sender}",
            "reference": confirmation,
        })
    except CampfireError as error:
        if error.code in ("reference_conflict", "month_closed", "invalid_argument"):
            return f"{error.code}, needs a person: {error}"
        raise  # 401/403: the token or its access needs fixing
    # 201 means recorded now; 200 means it was already recorded. Both are done.
    return "recorded " + result["data"]["id"]


print(record("Jane Smith", 4500, "2026-11-02", "ZL-3F9A2C71"))

Did this help?

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

Contact Support