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-Signature header?
  • Why is my CompanyCam webhook signature verification failing?
  • How do I handle photo.created, project.created, or task.completed events?
  • Why are my CompanyCam webhooks unsigned?

Setup

Set a token when you create the CompanyCam webhook (POST /public_api/v1/webhooks 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_digest(signature.encode("utf-8"), expected.encode("utf-8")) (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_typeTriggered When
project.createdA project is created
project.updatedA project is updated
photo.createdA photo is created and processed
photo.updatedA photo is edited (annotations)
comment.createdA comment is created
document.createdA document is created for a project
todo_list.completedA checklist is completed
task.completedA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 9, 2026

View on GitHub →