Core concepts
Webhooks.
Campfire will POST a JSON payload to a URL you control whenever something interesting happens. Deliveries are signed, retried with backoff, and idempotent — each carries a delivery ID that is safe to dedupe against.
Anatomy of a delivery
Every webhook delivery is a POST with Content-Type: application/json and the following headers:
| Header | Meaning |
|---|---|
X-Campfire-Event | The event type, e.g. message.created. |
X-Campfire-Delivery | Unique delivery UUID. Safe dedupe key. |
X-Campfire-Timestamp | ISO 8601 timestamp of the delivery attempt. |
X-Campfire-Signature | Hex-encoded HMAC-SHA256 of the raw body, using your signing secret. |
User-Agent | Always Campfire-Webhooks/1.0. |
A 2xx response is treated as success. Any other status — or a timeout greater than the configured budget — triggers a retry with exponential backoff. We stop retrying after the configured maximum attempts, which is visible on the delivery record in the dashboard.
Payload shape
Every payload has the same envelope. The data field carries the event-specific body.
{
"event": "message.created",
"id": "evt_8a1d8e2c-8f2a-4a8b-9c0e-6a1d9e2c3f4b",
"created_at": "2026-04-17T14:02:31.000Z",
"organization_id": "5515fbe0-745b-4b01-9ed4-828767c84524",
"data": {
"message": {
"id": "msg_01HYT4...",
"conversation_id": "cnv_01HYT3...",
"sender_type": "visitor",
"sender_id": "vis_01HYT2...",
"content": "Hi — I can’t reset my password.",
"created_at": "2026-04-17T14:02:30.842Z"
}
}
}Verify the signature
The signature is a hex-encoded HMAC-SHA256 of the raw request body using your signing secret. Always compute the HMAC against the untouched bytes — if your framework parses the body as JSON before you see it, re-stringifying will produce a different signature and verification will fail.
Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// Capture the raw body so we can verify the signature against it.
app.post(
'/webhooks/campfire',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.get('X-Campfire-Signature') ?? '';
const expected = crypto
.createHmac('sha256', process.env.CAMPFIRE_SIGNING_SECRET)
.update(req.body) // Buffer of the raw body
.digest('hex');
const sigBuf = Buffer.from(signature, 'utf8');
const expBuf = Buffer.from(expected, 'utf8');
const ok =
sigBuf.length === expBuf.length &&
crypto.timingSafeEqual(sigBuf, expBuf);
if (!ok) return res.status(401).send('invalid signature');
const event = JSON.parse(req.body.toString('utf8'));
// ... handle event ...
res.status(200).send('ok');
},
);Python (Flask)
import hashlib
import hmac
import os
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["CAMPFIRE_SIGNING_SECRET"].encode()
@app.post("/webhooks/campfire")
def handle_campfire():
signature = request.headers.get("X-Campfire-Signature", "")
# Use the raw bytes. Do not re-serialize the parsed JSON.
expected = hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
event = request.get_json()
# ... handle event ...
return "ok", 200Compare with a constant-time check (timingSafeEqual / hmac.compare_digest). Never use a plain string equality — it leaks information through timing and makes your endpoint exploitable.
Event catalogue
Subscribe to only the events you need. You can add or remove events on an existing subscription without a new signing secret.
Conversation
- conversation.createdA new conversation was opened.
- conversation.updatedAny conversation field changed (status, priority, assignee, tags).
- conversation.resolvedThe conversation was resolved by an agent or AI.
- conversation.reopenedA resolved conversation was reopened.
- conversation.assignedThe conversation was assigned to an agent or team.
- conversation.unassignedAssignment was removed.
- conversation.priority_changedPriority moved (e.g. urgent → high).
- conversation.tag_addedA tag was attached to the conversation.
- conversation.tag_removedA tag was detached.
Message
- message.createdA new message — from a visitor, an agent, or AI — was persisted.
- message.updatedA message was edited.
- message.deletedA message was deleted.
Visitor
- visitor.identifiedA visitor became a known user via widget identification.
- visitor.blockedA visitor was blocked from messaging.
- visitor.unblockedA previously blocked visitor was allowed back.
Ticket
- ticket.createdA ticket was created from a conversation.
- ticket.updatedTicket fields changed.
- ticket.resolvedTicket resolved.
- ticket.closedTicket closed (resolved + no further action).
Agent
- agent.onlineAn agent came online.
- agent.offlineAn agent went offline.
- agent.assignedAn agent was assigned conversations via routing.
AI
- ai.response_generatedAn AI draft or automated reply was produced.
- ai.handoff_triggeredAI handed off to a human agent.
Mention
- mention.createdA teammate was @-mentioned on a conversation note.
Next