# Maximizer Webhooks

Maximizer CRM sends organization-level webhooks to a **target** you register with the Webhooks API
(`POST /v1/targets`), for each **subscription** (`Entity` + `Op`) you create. Webhooks are **not
signed**. You authenticate them with a secret header that you add to the target's
`Endpoint.Headers`; Maximizer sends it on every request, and your endpoint compares it with the
value you stored. The body is a JSON **array** of webhook objects.

## When to Use This Skill

* How do I receive Maximizer CRM webhooks?
* Does Maximizer sign webhook payloads? (No: use a secret header on the target)
* How do I verify a Maximizer webhook with `Endpoint.Headers`?
* How do I handle `AbEntry`, `Opportunity` or `Lead` create, update and delete webhooks?
* Why does my Maximizer webhook endpoint return 401, or why did Maximizer stop retrying?

## Verification (core)

```javascript
const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyMaximizerRequest(headers,
    secret = process.env.MAXIMIZER_WEBHOOK_SECRET,
    headerName = process.env.MAXIMIZER_WEBHOOK_HEADER || 'Authorization') {
  if (!secret) return false;                   // never accept when unconfigured
  const received = headers[headerName.toLowerCase()];   // Node lower-cases names
  return typeof received === 'string' && safeEqual(received, secret);  // WHOLE value
}

```

* The header name is **yours**: whatever `Key` you put in `Endpoint.Headers`. Set it in
  `MAXIMIZER_WEBHOOK_HEADER` (default `Authorization`); lookup is case-insensitive.
* `MAXIMIZER_WEBHOOK_SECRET` is the **exact, whole header value**, including any `Bearer ` prefix
  you typed. Don't strip or parse the prefix; compare the whole string.
* Python: `hmac.compare_digest` on **bytes** (encode both sides). Passing `str` raises `TypeError`
  on non-ASCII input.
* Missing header or mismatch: `401`. Body that isn't a JSON array: `400`. Otherwise `200`.
* Read the body raw (`express.raw({ type: '*/*' })`) and authenticate before parsing it.

> **A static shared credential authenticates the sender, not the body.** There is no signature and
> no timestamp, so a captured request can be replayed and the payload can't be proven unmodified.
> Use HTTPS only, keep the secret out of logs, and rotate it (update the target's headers and
> `MAXIMIZER_WEBHOOK_SECRET`) if it leaks. Treat handlers as idempotent: each webhook has an `Id`.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/maximizer-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/maximizer-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/maximizer-webhooks/examples/fastapi/).

## Payload and Events

Each request is a JSON array. `Entity` is at the top level of each object; the operation is in
`Data.Op`. `Data.Original` is null on create, `Data.New` is null on delete.

```json
[{ "Id": "d81f9d6e-...", "Entity": "Opportunity",
   "Data": { "Op": "Update", "Original": { "Key": "..." }, "New": { "Key": "..." }, "Version": "1.1" } }]

```

| Entity | Op | Handled As |
| --- | --- | --- |
| `AbEntry` (contact) | `Create`, `Update`, `Delete` | Sync contacts |
| `Opportunity` | `Create`, `Update`, `Delete` | Track deals and stage changes |
| `Lead` | `Create`, `Update`, `Delete` | Route leads |

The Postman collection's payload examples also cover `LeadDefault` and `InteractionLog` /
`InteractionLogDefault` (`Create`, `Update`, `Delete`) and `Export` (`Read`, for AbEntry and
Opportunity exports); they arrive the same way. No other entities are documented. Subscriptions
are unique per target + `Entity` + `Op`, so create one per combination you want.

## Respond Within 2 Seconds

Targets must return `200 OK` within **2 seconds**. Otherwise Maximizer makes two more attempts and
then discards the webhook. Acknowledge first, then do slow work in the background or a queue.

## Environment Variables

```bash
MAXIMIZER_WEBHOOK_HEADER=Authorization                  # the Key you set in Endpoint.Headers
MAXIMIZER_WEBHOOK_SECRET=Bearer your-long-random-secret # the exact, whole Value

```

## Local Development

Maximizer can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

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

```

Use the printed URL as the target's `Endpoint.Url`.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/maximizer-webhooks/references/overview.md) - What Maximizer webhooks are, entities, payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/maximizer-webhooks/references/setup.md) - Create the target and subscriptions with the Webhooks API
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/maximizer-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/maximizer-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests