API and webhooks
FaithFinance is a pass-through, not your accounting system. Each giving reaches your books through a versioned API and signed webhooks.
Keys
A church admin creates an app in the console (per integrator), gives it only the scopes it needs, and issues a key. The key is shown once and stored only as a hash. It can be revoked at any moment, and an app can hold several so you can rotate without downtime.
curl -H "Authorization: Api-Key wb_…" https://faithfinance.adventhymnals.org/api/plugins/offerscan/v1/givings?status=verifiedUse the Api-Key scheme, never Bearer. A key belongs to one church, so you never send a church id.
The model
- Envelope: a reusable slip with a signed, unguessable code. It identifies the envelope, never the giver.
- Giving: one use of an envelope, with the giver, the lines by category, the total, and its status:
pending(declared, no envelope yet),declared,verifiedorcancelled. - Verification: the treasurer’s count,
matchedordiscrepancy, in a batch (a Sabbath’s collection).
Amounts are integers in minor units: KES 1,500.00 is 150000. POSTs accept Idempotency-Key. Lists page with cursor.
Webhooks
Register an https endpoint and choose events: giving.declared, giving.changed, giving.attached, giving.cancelled, giving.verified, giving.discrepancy, batch.closed. We POST each one with:
FaithFinance-Event: giving.verified
FaithFinance-Delivery: dlv_…
FaithFinance-Signature: t=1761310931,v1=5f0c…{
"id": "evt_9c1f…",
"type": "giving.verified",
"created": "2026-10-24T13:02:11Z",
"church": { "slug": "nairobi-central-sda", "name": "Nairobi Central SDA", "currency": "KES" },
"data": { "giving": {
"id": "6b0e…", "code": "Q4MZ8T2P", "status": "verified",
"envelope": { "code": "K7P2QX9M3TZA", "serial": 1042 },
"giver_name": "Jane W.", "giver_ref": "M-0042",
"total": 180000,
"lines": [ { "category": "tithe", "amount": 150000 }, { "category": "church_budget", "amount": 20000 },
{ "category": "camp_meeting", "amount": 10000 } ],
"verification": { "declared_total": 180000, "counted_total": 180000, "result": "matched", "difference": 0 }
} }
}Answer 2xx within 10 seconds. Anything else is retried after 1m, 5m, 30m, 2h, 6h, 12h and 24h. Every attempt is in the console’s delivery log, and any delivery can be replayed. Deliveries can repeat, so key on the event id. Prefer polling? GET /v1/events?cursor=… returns the same events in order.
Checking the signature
import hmac, hashlib, time
def verify(secret: str, raw_body: bytes, header: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
if abs(time.time() - t) > 300:
return False # too old: a replay
mac = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, parts["v1"])