Agent skill
CompanyCam Webhooks Skill
Receive and verify CompanyCam webhooks (job-site photos). Use when setting up CompanyCam webhook handlers, debugging X-CompanyCam-Signature verification, or handling events like project.created, photo.created, comment.created, document.created, todo_list.completed, and task.completed.
Install this skill
npx skills add hookdeck/webhook-skills --skill companycam-webhooks
When to Use This Skill
- How do I receive CompanyCam webhooks?
- How do I verify the
X-CompanyCam-Signatureheader? - Why is my CompanyCam webhook signature verification failing?
- How do I handle
photo.created,project.created, ortask.completedevents? - Why are my CompanyCam webhooks unsigned?
Setup
Set a token when you create the CompanyCam webhook (POST /public_ with webhook.url, webhook.scopes, and webhook.token). The token is optional on create, but without one, deliveries are unsigned and fail verification. The token is the HMAC key; store it as COMPANYCAM_WEBHOOK_TOKEN. See references/setup.md.
Verification (core)
CompanyCam has no webhook verification SDK, so verify manually. The X-CompanyCam-Signature header is Base64(HMAC-SHA1(token, raw body)). The algorithm is SHA-1 and the digest is base64 (not hex). Use the raw body; never verify a re-serialized parse. Compare the base64 strings as equal-length UTF-8 buffers (no lenient base64 decode of the header).
const crypto = require('crypto');
function verifyCompanyCamSignature(rawBody, signature, token) {
if (!signature || !token) return false;
const expected = crypto.createHmac('sha1', token).update(rawBody).digest('base64');
const a = Buffer.from(signature, 'utf8');
const b = Buffer.from(expected, 'utf8');
if (a.length !== b.length) return false; // timingSafeEqual throws on length mismatch
return crypto.timingSafeEqual(a, b);
}
In Python, use hmac.compare_ (bytes, so a non-ASCII header returns 401 rather than raising a 500).
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
In Express, read the body with express.raw({ type: '*/*' }) so any content type verifies.
Common Event Types
The event name is in the payload's event_type field; the object is in payload.
event_type | Triggered When |
|---|---|
project.created | A project is created |
project.updated | A project is updated |
photo.created | A photo is created and processed |
photo.updated | A photo is edited (annotations) |
comment.created | A comment is created |
document.created | A document is created for a project |
todo_list.completed | A checklist is completed |
task.completed | A checklist task is completed |
Scopes also include wildcards (*, project.*, photo.*, ...), project.archived, project.deleted, video.created, and more. See references/overview.md.
CompanyCam expects exactly HTTP 200; any other status is retried with exponential backoff (max 10 attempts), and a webhook with more than 25 errors is disabled.
Environment Variables
COMPANYCAM_WEBHOOK_TOKEN=your_webhook_token # the `token` you passed when creating the webhook
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 companycam --path /webhooks/companycam
Reference Materials
- references/overview.md - Webhook concepts, scopes, payload
- references/setup.md - Create the webhook with a token
- references/verification.md - Signature details and gotchas