Agent skill
Campfire Webhooks Skill
Receive and verify Campfire webhooks (campfire.ai, the AI accounting and ERP platform at app.meetcampfire.com; not 37signals' Campfire chat). Use when setting up a Campfire webhook handler, debugging Campfire-Webhook-Signature-v1 verification, or handling topics like Invoice.created, Invoice.paid, Invoice.payment, Bill.created, Bill.paid, CreditMemo.created or JournalEntry.updated. The signature is a hex HMAC-SHA256 over {timestamp}:{raw body}.
Install this skill
npx skills add hookdeck/webhook-skills --skill campfire-webhooks
Campfire is an AI accounting and ERP platform. Its webhooks report created, updated and deleted invoices, bills, credit and debit memos, journal entries and contracts, plus invoice and bill payments.
Not 37signals' Campfire (the team-chat product). This skill is for campfire.ai.
When to Use This Skill
- How do I receive Campfire webhooks?
- How do I verify the
Campfire-Webhook-Signature-v1signature? - How do I handle
Invoice.paidorBill.createdevents? - Why does my Campfire webhook signature verification fail?
- What is the difference between
Invoice.paymentandInvoice.paid?
Verification (core)
Campfire signs the bytes {Campfire-Webhook-Request-Timestamp}:{raw request body} (a colon, not a dot) with HMAC-SHA256, keyed with the webhook's 50-character signing secret as UTF-8 text. The bare lowercase hex digest (no prefix, despite the -v1 in the name) is in Campfire-Webhook-Signature-v1. Missing header, malformed value or mismatch: respond 401.
const crypto = require('crypto');
function verifyCampfireSignature(rawBody, headers, secret) {
const timestamp = headers['campfire-webhook-request-timestamp'];
const signature = headers['campfire-webhook-signature-v1'];
if (!secret || !timestamp || !signature) return false;
if (!/^[0-9a-fA-F]{64}$/.test(signature)) return false; // validate before decoding
const message = Buffer.concat([Buffer.from(timestamp, 'latin1'), Buffer.from(':'), rawBody]);
const expected = crypto.createHmac('sha256', Buffer.from(secret, 'utf8')).update(message).digest();
return crypto.timingSafeEqual(expected, Buffer.from(signature.toLowerCase(), 'hex'));
}
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Gotchas
- Raw body, as bytes. Express:
express.raw({ type: '*/*' }); Next.js:await request.arrayBuffer(); FastAPI:await request.body(). NeverJSON.parse/JSON.stringifybefore verifying, and never decode and re-encode the body. - The timestamp is signed exactly as received. Do not trim or reformat the header value.
- The secret is used as-is. It is the
tokenreturned when the webhook is created (or Show on the webhook card). Do not hex- or base64-decode it. It is not your API key, and Campfire generates it: to rotate, delete the webhook and create a new one. - Compare safely. Require exactly 64 hex characters (
Buffer.from(x, 'hex')silently drops bad characters), lowercase, thencrypto.timingSafeEqualon equal-length buffers (Python:hmac.compare_digeston bytes). - Timestamp window is opt-in. Campfire recommends a 5-minute tolerance and re-signs each retry, but Hookdeck retries and replays resend the original headers and Hookdeck applies no tolerance, so the examples do not reject old timestamps by default. Set
CAMPFIRE_(for exampleWEBHOOK_ TOLERANCE_ SECONDS 300; unset or0= off) to enable it. The timestamp is Unix seconds. - Dispatch on
topic. The body is{ webhook_.uuid, topic, data, changes } *.deletedcarries only{"id": ...}indataandnullinchanges.
Topics
Exact, case-sensitive strings (28 documented):
| Object | Topics |
|---|---|
| Invoices | Invoice.created, Invoice.updated, Invoice.deleted, Invoice.payment, Invoice.paid |
| Bills | Bill.created, Bill.updated, Bill.deleted, Bill.payment, Bill.paid |
| Credit memos / lines | CreditMemo.{created,updated,deleted}, CreditMemoLine.{created,updated,deleted} |
| Debit memos / lines | DebitMemo.{created,updated,deleted}, DebitMemoLine.{created,updated,deleted} |
| Journal entries | JournalEntry.created, JournalEntry.updated, JournalEntry.deleted |
| Contracts | Contract.created, Contract.updated, Contract.deleted |
.payment fires on every increase in amount paid (partial payments included). .paid fires when the document becomes fully paid; then both fire as separate deliveries and no .updated is sent for that change.
Delivery
POST, respond 2xx to acknowledge. 4xx / 5xx fail and are retried up to 5 times about a minute apart; a 404 is not retried. Each retry is re-signed. Not ordered, duplicates possible: be idempotent and treat data as the latest state of the object.
Environment Variables
CAMPFIRE_WEBHOOK_SECRET=your-50-character-signing-secret
# Optional replay window in seconds; unset or 0 = off
CAMPFIRE_WEBHOOK_TOLERANCE_SECONDS=300
Local Development
npx hookdeck-cli listen 3000 campfire --path /webhooks/campfire
Use port 8000 for the FastAPI example. No account required: the CLI creates a guest account on first run and gives you a public URL plus a web UI for inspecting requests. Paste the URL into Settings > Developer > Webhooks in Campfire.
Reference Materials
- references/overview.md - Topics, payload, headers, delivery and retries
- references/setup.md - Creating webhooks in the app and API, signing secret, managing topics
- references/verification.md - Signature details, timestamp tolerance, debugging