> ## Documentation Index
> Fetch the complete documentation index at: https://docs.campfire.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications when data changes in Campfire

Webhooks let Campfire notify your systems in real time when data changes — for example when an invoice is paid, a bill is created, or a journal entry is updated. Instead of polling the API, you register an HTTPS endpoint, subscribe it to the topics you care about, and Campfire sends a signed `POST` request to that endpoint every time a matching event occurs.

<Info>Managing webhooks — in the app or via the API — requires an **admin** user. Non-admin API keys receive a `403` from the webhook endpoints.</Info>

## Setting up a webhook

You can create a webhook in the Campfire app or via the API.

### In the app

<Steps>
  <Step title="Go to the Webhooks page">
    Go to [Settings > Developer > Webhooks](https://app.meetcampfire.com/v2/settings/webhooks) in your Campfire instance.
  </Step>

  <Step title="Create the webhook">
    Click **New Webhook**, enter the HTTPS URL of your endpoint, check **Active**, and select the topics you want to subscribe to. Topics are grouped by object (Invoices, Bills, Journal Entries, etc.) — you can select a whole group or individual topics.
  </Step>

  <Step title="Copy your signing secret">
    After saving, the webhook card shows an **HMAC-SHA256 Secret**. Click **Show** to reveal it and store it in your application's secret store. You'll use it to [verify signatures](#verifying-the-signature) on incoming deliveries.
  </Step>
</Steps>

### Via the API

Create a webhook with a `POST` to the webhook endpoint. See our [authentication guide](/quickstart) for how to get an API key.

```bash theme={null}
curl -X POST https://api.meetcampfire.com/integrations/api/v1/webhook \
  -H "Authorization: Token $CAMPFIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/campfire/webhook",
    "active": true,
    "topics": [
      {"name": "Invoice.paid"},
      {"name": "Bill.created"}
    ]
  }'
```

The response includes your signing secret (`token`) and the webhook's `uuid`:

```json theme={null}
{
  "id": 12,
  "uuid": "3c88df19-1c1f-4b1c-b4a7-768793055a60",
  "url": "https://example.com/campfire/webhook",
  "token": "your-50-character-signing-secret",
  "topics": [
    {"id": 5, "name": "Invoice.paid"},
    {"id": 18, "name": "Bill.created"}
  ],
  "active": true,
  "created_at": "2026-08-04T12:00:00+0000",
  "last_modified_at": "2026-08-04T12:00:00+0000"
}
```

<Note>The signing secret is generated by Campfire and cannot be set or changed. To rotate it, delete the webhook and create a new one.</Note>

## Topics

A webhook only receives events for the topics it is subscribed to. Topic names are exact strings — subscribe using the values below.

| 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      | `CreditMemo.created`, `CreditMemo.updated`, `CreditMemo.deleted`                           |
| Credit Memo Lines | `CreditMemoLine.created`, `CreditMemoLine.updated`, `CreditMemoLine.deleted`               |
| Debit Memos       | `DebitMemo.created`, `DebitMemo.updated`, `DebitMemo.deleted`                              |
| Debit Memo Lines  | `DebitMemoLine.created`, `DebitMemoLine.updated`, `DebitMemoLine.deleted`                  |
| Journal Entries   | `JournalEntry.created`, `JournalEntry.updated`, `JournalEntry.deleted`                     |
| Contracts         | `Contract.created`, `Contract.updated`, `Contract.deleted`                                 |

**Payment topics** have special semantics for invoices and bills:

* `Invoice.payment` / `Bill.payment` fires whenever the amount paid increases — including partial payments.
* `Invoice.paid` / `Bill.paid` fires when the document transitions to fully paid. When that happens, **both** the `.paid` and `.payment` topics fire (as two separate deliveries), and no `.updated` event is sent for that change.

## Payload

Every delivery is a `POST` with a JSON body containing four fields:

| Field          | Type           | Description                                                                                                                                                                                              |
| -------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `webhook_uuid` | string         | UUID of the webhook subscription that received this event. Matches the `Campfire-Webhook-UUID` header.                                                                                                   |
| `topic`        | string         | The topic that fired, e.g. `"Invoice.paid"`.                                                                                                                                                             |
| `data`         | object         | The full serialized object, in the same shape as the corresponding `GET` endpoint in the [API reference](/api-reference). For `*.deleted` topics, `data` contains only the object's id: `{"id": 12345}`. |
| `changes`      | object or null | Field-level diff of what changed, keyed by field name with `old` and `new` values. `null` for `*.deleted` topics. On `*.created` topics, every field is listed with `"old": null`.                       |

Example delivery for `Invoice.paid`:

```json theme={null}
{
  "webhook_uuid": "3c88df19-1c1f-4b1c-b4a7-768793055a60",
  "topic": "Invoice.paid",
  "data": {
    "id": 12345,
    "invoice_number": "INV-1042",
    "payment_status": "paid",
    "amount_paid": 1500.0
    // ...full invoice object, same shape as the Invoices API
  },
  "changes": {
    "payment_status": {"old": "open", "new": "paid"},
    "amount_paid": {"old": 500.0, "new": 1500.0}
  }
}
```

### Request headers

| Header                               | Description                                                                       |
| ------------------------------------ | --------------------------------------------------------------------------------- |
| `Content-Type`                       | Always `application/json`.                                                        |
| `Campfire-Webhook-Request-Timestamp` | Unix timestamp (seconds) of when the request was signed, e.g. `1770000000`.       |
| `Campfire-Webhook-Signature-v1`      | Hex-encoded HMAC-SHA256 signature of the request. See below.                      |
| `Campfire-Webhook-UUID`              | UUID of the webhook subscription. Constant across all deliveries to that webhook. |

## Verifying the signature

Every delivery is signed with your webhook's secret so you can verify it genuinely came from Campfire. The signed message is the delivery timestamp and the raw request body, joined by a single `:` character:

```
{timestamp}:{raw request body}
```

where `timestamp` is the value of the `Campfire-Webhook-Request-Timestamp` header. Campfire computes an HMAC-SHA256 digest of this message using your signing secret as the key, hex-encodes it, and sends it in the `Campfire-Webhook-Signature-v1` header.

<Warning>Compute the signature over the **raw request body exactly as received** — read it before your framework parses it, and never re-serialize the parsed JSON. Re-serializing can change whitespace and key order, which changes the signature.</Warning>

To verify a delivery:

1. Read the `Campfire-Webhook-Request-Timestamp` and `Campfire-Webhook-Signature-v1` headers and the raw request body.
2. Reject the delivery if the timestamp is too old — we recommend a tolerance of 5 minutes. This protects against replay attacks. (Retried deliveries are re-signed with a fresh timestamp, so a tolerance check will not reject legitimate retries.)
3. Compute `HMAC-SHA256` over `"{timestamp}:{raw body}"` using your signing secret, hex-encode it, and compare it to the signature header using a constant-time comparison.

<CodeGroup>
  ```python Python theme={null}
  import hashlib
  import hmac
  import time


  def verify_campfire_signature(raw_body: bytes, headers: dict, secret: str, tolerance_seconds: int = 300) -> bool:
      timestamp = headers["Campfire-Webhook-Request-Timestamp"]
      signature = headers["Campfire-Webhook-Signature-v1"]

      if abs(time.time() - int(timestamp)) > tolerance_seconds:
          return False

      signed_payload = f"{timestamp}:".encode() + raw_body
      expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, signature)
  ```

  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  function verifyCampfireSignature(rawBody, headers, secret, toleranceSeconds = 300) {
    const timestamp = headers["campfire-webhook-request-timestamp"];
    const signature = headers["campfire-webhook-signature-v1"];

    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) {
      return false;
    }

    const expected = crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}:${rawBody}`)
      .digest("hex");

    return (
      expected.length === signature.length &&
      crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
    );
  }
  ```
</CodeGroup>

<Note>In Express, use `express.raw({ type: "application/json" })` (or capture `req.rawBody`) on your webhook route so the body is available unparsed. In Django/Flask/FastAPI, use `request.body` / `request.get_data()` / `await request.body()` respectively.</Note>

You can confirm your implementation against real deliveries: the [delivery log](#delivery-and-retries) for each event includes the exact request headers and body Campfire sent, including the computed signature.

## Delivery and retries

* Deliveries are `POST` requests. Respond with a **2xx** status code to acknowledge receipt. Any 4xx or 5xx response is treated as a failure.
* Failed deliveries are retried up to **5 times**, roughly one minute apart. A `404` response is not retried.
* Each retry is re-signed with a fresh timestamp and signature.
* Respond quickly — acknowledge the delivery first and process the event asynchronously if your handling is slow.
* Deliveries are not guaranteed to arrive in order, and retries mean your endpoint may occasionally receive the same event more than once. Make your handler idempotent — for example, treat the payload's `data` as the latest state of the object rather than applying `changes` incrementally.

### Delivery logs

Recent delivery attempts appear under **Recent Events** on the Webhooks settings page, where you can inspect each request and response. The same data is available via the API (most recent 25 events):

```bash theme={null}
curl https://api.meetcampfire.com/integrations/api/v1/webhook/12/events \
  -H "Authorization: Token $CAMPFIRE_API_KEY"
```

Each event includes the delivery `status` (`PENDING`, `SUCCESS`, or `FAILURE`), the `topic`, and an `http` object with the full request and response — URL, headers, body, and response status code.

## Managing webhooks via the API

| Method          | Endpoint                                   | Description            |
| --------------- | ------------------------------------------ | ---------------------- |
| `GET`           | `/integrations/api/v1/webhook`             | List your webhooks     |
| `POST`          | `/integrations/api/v1/webhook`             | Create a webhook       |
| `GET`           | `/integrations/api/v1/webhook/{id}`        | Retrieve a webhook     |
| `PUT` / `PATCH` | `/integrations/api/v1/webhook/{id}`        | Update a webhook       |
| `DELETE`        | `/integrations/api/v1/webhook/{id}`        | Delete a webhook       |
| `GET`           | `/integrations/api/v1/webhook/{id}/events` | List recent deliveries |

Notes:

* `{id}` is the integer `id` field from the webhook object, not its `uuid`.
* `url` and `active` are required on create and update.
* `topics` is a list of `{"name": "<topic>"}` objects. Topic names must match the values in the [topics table](#topics) exactly.
* Updating `topics` **replaces** the full set — include every topic the webhook should be subscribed to, not just new ones.
* `token` (the signing secret) is read-only and returned on every read. To rotate it, delete the webhook and create a new one.
* To pause deliveries without deleting the webhook, set `"active": false`.

<Note>
  **Need help?** Contact your Campfire support team and we're happy to help you build your best Campfire workflow!
</Note>
