Gareth Wilson Gareth Wilson

Guide to Maximizer Webhooks: Features and Best Practices

Published


Maximizer webhooks push changes in Maximizer CRM to your own systems as they happen. A contact is added to the address book, a lead is updated, an opportunity moves to a new sales stage, an interaction is logged: each can reach your service within moments, so you can sync another database, alert a sales channel, or trigger follow-up without polling the Maximizer API.

This guide covers how Maximizer's targets and subscriptions fit together, the array payload and its fields, how to authenticate requests with a header on the target, and the 2-second response limit that decides whether a webhook is delivered at all.

What are Maximizer webhooks?

Maximizer webhooks are organisation-level: a subscription receives every change to its entity, not just one user's. You manage them through the Webhooks API, with a token that belongs to an Administrator.

There are two pieces. A target is your endpoint: a URL and, optionally, headers Maximizer sends with every request. A subscription connects a target to one entity and operation, such as Opportunity and Update, optionally with filters. When a matching change happens, Maximizer sends a POST to the target.

Maximizer doesn't sign webhooks. To secure a target, its documentation says you can put a secret in the URL's query string or configure the target to send specific headers. A header is the better choice: it stays out of logs, and it's what Hookdeck's Maximizer source checks.

Maximizer webhook features

FeatureDetails
ScopeOrganisation-level only
ManagementWebhooks API at https://api.maximizer.com/webhooks, Administrator token (Octopus regular or PAT, or OAuth)
TargetsPOST /v1/targets, at most 10 per Maximizer instance
SubscriptionsPOST /v1/subscriptions: one Entity and Op per target, with optional filters and recipients
MethodPOST
BodyJSON array of webhook objects
AuthenticationHeaders you set on the target. No signature
Response requirement200 OK within 2 seconds
RetriesTwo more attempts after a short delay, then the webhook is discarded
VersioningData.Version per entity, major.minor

Common events

Maximizer identifies a webhook by its Entity and Data.Op together. The documented combinations are:

EntityOp
AbEntry (address book entries)Create, Update, Delete
LeadCreate, Update, Delete
LeadDefaultCreate, Update, Delete
OpportunityCreate, Update, Delete
InteractionLogCreate, Update, Delete
InteractionLogDefaultCreate, Update, Delete
ExportRead

Maximizer's Postman collection is the reference for the current list, with sample payloads for every supported object and version.

Each request body is an array. Here is Maximizer's "Opportunity Stage Change" example, trimmed:

[
  {
    "Id": "d81f9d6e-8afc-4fa4-9052-2af207494e5d",
    "Timestamp": "2024-09-20T00:01:29.5149836Z",
    "ProcessedTimestamp": "2024-09-20T00:02:45.8974002Z",
    "WorkspaceId": "108F314C-6787-4EE6-BE49-C0E0C651F039",
    "SourceId": "1a0dde7c-743f-4b55-a2bf-e7738393a8e5",
    "Recipients": [],
    "Options": null,
    "Entity": "Opportunity",
    "Data": {
      "Op": "Update",
      "Original": {
        "Key": "T3Bwb3J0dW5pdHkJMjQwOTE5MjUxNjQ1MjA0MTQwMDAxTwkw",
        "SalesStageSetupName": "Qualified",
        "Objective": "Test Opportunity",
        "Revenue": 12000.00
      },
      "New": {
        "Key": "T3Bwb3J0dW5pdHkJMjQwOTE5MjUxNjQ1MjA0MTQwMDAxTwkw",
        "SalesStageSetupName": "Presentation"
      },
      "Version": "1.1"
    }
  }
]
  • Id is a unique webhook ID.
  • Timestamp is when the change happened at the source; ProcessedTimestamp is when Maximizer forwarded it.
  • WorkspaceId identifies the Maximizer database.
  • Recipients carries any recipients set on the subscription. Options is reserved, and SourceId is for Maximizer's internal use.
  • Data.Original holds the previous values and is null on create; Data.New holds the changed values and is null on delete.
  • Data.Version is the entity's version: the major number changes on breaking changes, the minor on additive ones.

Inspect Maximizer webhooks as they arrive. Register a Hookdeck Inspect URL as a target to see the array Maximizer sends for each subscription, with no account or setup required.

Setting up Maximizer webhooks

Generate a long random value first, for example with openssl rand -hex 32, and get an Administrator token for your Maximizer CRM instance.

Create a target with a secret header:

curl --request POST \
  --url https://api.maximizer.com/webhooks/v1/targets \
  --header "Authorization: Bearer $MAXIMIZER_ADMIN_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{
    "Name": "Opportunity sync",
    "Endpoint": {
      "Url": "https://example.com/webhooks/maximizer",
      "Headers": [
        { "Key": "Authorization", "Value": "Bearer YOUR_LONG_RANDOM_VALUE" }
      ]
    }
  }'

Then create a subscription for each Entity and Op you need, using the target's ID:

curl --request POST \
  --url https://api.maximizer.com/webhooks/v1/subscriptions \
  --header "Authorization: Bearer $MAXIMIZER_ADMIN_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{ "Entity": "Opportunity", "Op": "Update", "TargetId": "TARGET_ID" }'

Subscriptions can add filters, which must all match, such as { "Key": { "$EQ": "key-value" } } or { "$HASFIELD": "field-name" }. The combination of target, entity and operation must be unique.

To route through Hookdeck, create a source with the Maximizer type and use its URL as the target's Url. Enter the header key from the target in API Key Header and the exact value, including any Bearer prefix, in API Key. Hookdeck finds the header case-insensitively, compares the whole value, and accepts POST.

Securing Maximizer webhooks

Look up the header you set on the target and compare its whole value with the stored secret, in constant time.

const crypto = require('crypto');
const express = require('express');

const HEADER_NAME = (process.env.MAXIMIZER_WEBHOOK_HEADER || 'authorization').toLowerCase();
const EXPECTED = process.env.MAXIMIZER_WEBHOOK_SECRET; // e.g. "Bearer 3f9c..."

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/maximizer', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get(HEADER_NAME);
  if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
    return res.sendStatus(401);
  }

  let webhooks;
  try {
    webhooks = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (!Array.isArray(webhooks)) return res.sendStatus(400);

  // Queue each item (route on item.Entity + item.Data.Op) and answer within 2 seconds.
  res.sendStatus(200);
});

The same check in Python:

import hmac
import os

HEADER_NAME = os.environ.get("MAXIMIZER_WEBHOOK_HEADER", "Authorization")
EXPECTED = os.environ.get("MAXIMIZER_WEBHOOK_SECRET", "")


def verify_maximizer_request(headers) -> bool:
    if not EXPECTED:
        return False  # never accept when unconfigured
    received = headers.get(HEADER_NAME)  # most frameworks look headers up case-insensitively
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), EXPECTED.encode("utf-8"))

A static header authenticates the sender, not the body. It cannot detect a modified payload, and with no signature a captured request can be replayed and still pass. Use HTTPS, a long random value, and update the target's headers and your receiver together if it might have leaked.

Make Maximizer webhooks production-ready. Hookdeck Event Gateway checks your target header, answers Maximizer with a 200 well inside 2 seconds, and queues every CRM change durably for your service.

Maximizer webhook limitations and pain points

Two seconds and three attempts

The Problem: A handler that writes to a database or calls another API before responding loses webhooks under load.

Why It Happens: Targets must return 200 OK within 2 seconds. Otherwise Maximizer tries twice more after a short delay, and if all three attempts fail the webhook is discarded.

Workarounds:

  • Check the header, store the array and respond, then process it in the background.
  • Return exactly 200; Maximizer documents 200 OK as the success response.

How Hookdeck Can Help: Hookdeck answers Maximizer straight away and stores each request, then delivers it to your service with a delivery timeout and retries you choose, so a slow or briefly unavailable service no longer means lost changes.

Ten targets per instance

The Problem: Several integrations each want their own endpoint and run out of targets.

Why It Happens: Maximizer limits each CRM instance to 10 targets.

Workarounds:

  • Share one target across subscriptions, and route inside your service on Entity and Data.Op.

How Hookdeck Can Help: One Hookdeck source can feed many connections, each with its own filter and destination, so one target serves every consumer.

Version changes

The Problem: A new major version of an entity changes fields your handler depends on.

Why It Happens: Data.Version tracks the entity's version in the source system, and a new major version signals breaking changes.

Workarounds:

  • Read Data.Version and handle the major versions you've tested, as Maximizer recommends.
  • Log and acknowledge versions you don't recognise.

How Hookdeck Can Help: Every request is stored in Hookdeck with its body, so you can see when a new version first arrived and replay those events once your handler supports it.

A secret in the URL

The Problem: A query-string secret ends up in access logs and monitoring tools.

Why It Happens: Maximizer offers a query-string secret as one way to secure a target.

Workarounds:

  • Put the secret in the target's Headers instead.

How Hookdeck Can Help: Hookdeck's Maximizer source checks the header name and value you configure. Hookdeck answers Maximizer with a 200 either way, but a request without the right header is recorded as failed verification and never reaches your service.

Best practices

Treat the body as a list

Every request is an array. Process each item and deduplicate on its Id. See our guide to webhook idempotency.

Route on Entity and Data.Op together

Neither field identifies the event alone. Build a key such as Opportunity.Update in your own code.

Use Original and New

For updates, compare Data.Original with Data.New to see exactly what changed, such as a sales stage move, instead of re-reading the record.

Filter at the subscription

Subscription filters cut traffic before it reaches you. Fewer requests means fewer chances to miss the 2-second window.

Acknowledge fast and process asynchronously

Two seconds is too short for anything but storing the request. See why to process webhooks asynchronously.

Conclusion

Maximizer sends organisation-level CRM changes as a JSON array to a target you register through the Webhooks API, authenticated only by headers you put on the target. Use a secret header rather than a URL secret, compare it in constant time, route on Entity plus Data.Op, and return 200 within 2 seconds, because after three failed attempts Maximizer discards the webhook.

Hookdeck Event Gateway checks the header, answers within Maximizer's window, and keeps every change until your service has processed it.

Get started with Hookdeck for free and handle Maximizer webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.