Agent skill

Copper Webhooks Skill

Receive and authenticate Copper CRM webhooks. Use when setting up a Copper webhook endpoint, adding a secret header to a subscription's headers object, debugging 401s, or handling person, company, lead, opportunity, project, task and activity_log events like new, update and delete.

Install this skill

npx skills add hookdeck/webhook-skills --skill copper-webhooks


Copper (the CRM for Google Workspace) sends a JSON webhook when a record is created, updated or deleted. Webhooks are not signed. You create a subscription with POST /webhooks and can give it a headers object that Copper sends as HTTP headers on every notification. Put a secret in one of those headers and have your endpoint compare it with the value you stored.

When to Use This Skill

  • How do I receive Copper webhooks?
  • Does Copper sign webhook payloads? (No: use a secret header from the subscription's headers)
  • How do I authenticate Copper webhook requests?
  • How do I handle person / opportunity / lead events with new, update and delete?
  • Why does my Copper webhook endpoint return 401?

Verification (core)

const crypto = require('crypto');

// Equal-length Buffers only: timingSafeEqual throws on unequal lengths.
function safeEqual(a, b) {
  const x = Buffer.from(String(a), 'utf8');
  const y = Buffer.from(String(b), 'utf8');
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

function verifyCopperRequest(headers, expected = process.env.COPPER_WEBHOOK_SECRET) {
  if (!expected) return false;                                   // never accept when unconfigured
  const name = (process.env.COPPER_WEBHOOK_HEADER || 'X-Webhook-Secret').toLowerCase();
  const received = headers[name];                                // Node lowercases header names
  return Boolean(received) && safeEqual(received, expected);     // whole value, no prefix parsing
}
  • Subscription: POST https://api.copper.com/developer_api/v1/webhooks with "headers": {"X-Webhook-Secret": "<long random value>"}.
  • The header name is yours to choose (COPPER_WEBHOOK_HEADER, default X-Webhook-Secret), looked up case-insensitively. COPPER_WEBHOOK_SECRET is the exact, whole header value, including a Bearer prefix if you typed one. Nothing is stripped or parsed.
  • Missing header or mismatch: 401. Bad JSON after that: 400. Handled: 200.
  • A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS (Copper only accepts https:// targets) and rotate the credential if it leaks.
  • The subscription's optional secret object is not a header and not a body field called secret: Copper merges its key/value pairs into the top level of the JSON body (a secret of {"key": "copper_notifications"} arrives as "key": "copper_notifications"). It is a weaker extra check that Hookdeck does not perform; don't rely on it alone.
  • Read the body raw (express.raw({ type: '*/*' })) and authenticate before parsing it.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

The body is {ids, type, event, subscription_id, updated_attributes, timestamp} plus your secret key/value pairs at the top level. updated_attributes is only on update events and timestamp is an ISO 8601 string. Dispatch on type plus event. ids holds 1 to 30 record IDs per notification.

eventSent When
newA record is created
updateA field on an existing record changes (not new relationships, new activities or metadata)
deleteA record is removed
typeRecord
lead, person, company, opportunity, project, taskThe matching CRM record
activity_logAn activity (new, update and delete, per the webhooks overview)

Copper's webhooks overview lists activity_log for all three events, but the Create subscription reference lists only lead, person, company, opportunity, project and task as valid type values, so test an activity_log subscription before relying on it.

Each subscription covers one type and one event, so create one subscription per combination you need (up to 100 per account). Notifications fire at most once and are not retried.

Environment Variables

COPPER_WEBHOOK_HEADER=X-Webhook-Secret   # header name from the subscription's headers object
COPPER_WEBHOOK_SECRET=your-secret-value  # exact, whole header value

Local Development

Copper needs a public https:// URL. Tunnel with the Hookdeck CLI (no account required):

npx hookdeck-cli listen 3000 copper --path /webhooks/copper

Use the printed URL as the subscription's target.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 3, 2026

View on GitHub →