# Campfire Webhooks

[Campfire](https://campfire.ai) 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-v1` signature?
* How do I handle `Invoice.paid` or `Bill.created` events?
* Why does my Campfire webhook signature verification fail?
* What is the difference between `Invoice.payment` and `Invoice.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`.

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/campfire-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/campfire-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/campfire-webhooks/examples/fastapi/).

### Gotchas

* **Raw body, as bytes.** Express: `express.raw({ type: '*/*' })`; Next.js: `await request.arrayBuffer()`; FastAPI: `await request.body()`. Never `JSON.parse` / `JSON.stringify` before 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 `token` returned 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, then `crypto.timingSafeEqual` on equal-length buffers (Python: `hmac.compare_digest` on 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_WEBHOOK_TOLERANCE_SECONDS` (for example `300`; unset or `0` = off) to enable it. The timestamp is Unix **seconds**.
* **Dispatch on `topic`.** The body is `{ webhook_uuid, topic, data, changes }`. `*.deleted` carries only `{"id": ...}` in `data` and `null` in `changes`.

## 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

```bash
CAMPFIRE_WEBHOOK_SECRET=your-50-character-signing-secret
# Optional replay window in seconds; unset or 0 = off
CAMPFIRE_WEBHOOK_TOLERANCE_SECONDS=300

```

## Local Development

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/campfire-webhooks/references/overview.md) - Topics, payload, headers, delivery and retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/campfire-webhooks/references/setup.md) - Creating webhooks in the app and API, signing secret, managing topics
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/campfire-webhooks/references/verification.md) - Signature details, timestamp tolerance, debugging