Agent skill
Maximizer Webhooks Skill
Receive and authenticate Maximizer CRM webhooks. Use when setting up a Maximizer webhook target, checking the secret header you configured in Endpoint.Headers, debugging 401s, or handling Entity/Op changes like AbEntry Create, Opportunity Update and Lead Delete.
Install this skill
npx skills add hookdeck/webhook-skills --skill 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,OpportunityorLeadcreate, update and delete webhooks? - Why does my Maximizer webhook endpoint return 401, or why did Maximizer stop retrying?
Verification (core)
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
Keyyou put inEndpoint.Headers. Set it inMAXIMIZER_WEBHOOK_HEADER(defaultAuthorization); lookup is case-insensitive. MAXIMIZER_WEBHOOK_SECRETis the exact, whole header value, including anyBearerprefix you typed. Don't strip or parse the prefix; compare the whole string.- Python:
hmac.compare_digeston bytes (encode both sides). PassingstrraisesTypeErroron non-ASCII input. - Missing header or mismatch:
401. Body that isn't a JSON array:400. Otherwise200. - 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 anId.
For complete handlers with tests, see examples/express/, examples/nextjs/, 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.
[{ "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
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):
npx hookdeck-cli listen 3000 maximizer --path /webhooks/maximizer
Use the printed URL as the target's Endpoint.Url.
Resources
- references/overview.md - What Maximizer webhooks are, entities, payload shape
- references/setup.md - Create the target and subscriptions with the Webhooks API
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests