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, Opportunity or Lead create, 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 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/, 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" } }]
EntityOpHandled As
AbEntry (contact)Create, Update, DeleteSync contacts
OpportunityCreate, Update, DeleteTrack deals and stage changes
LeadCreate, Update, DeleteRoute 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 6, 2026

View on GitHub →