# 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)

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/copper-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/copper-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/copper-webhooks/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.

| `event` | Sent When |
| --- | --- |
| `new` | A record is created |
| `update` | A field on an existing record changes (not new relationships, new activities or metadata) |
| `delete` | A record is removed |

| `type` | Record |
| --- | --- |
| `lead`, `person`, `company`, `opportunity`, `project`, `task` | The matching CRM record |
| `activity_log` | An 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

```bash
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):

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

```

Use the printed URL as the subscription's `target`.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/copper-webhooks/references/overview.md) - What Copper webhooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/copper-webhooks/references/setup.md) - Create a subscription with a secret header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/copper-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/copper-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests