Guide to Maximizer Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Scope | Organisation-level only |
| Management | Webhooks API at https://api.maximizer.com/webhooks, Administrator token (Octopus regular or PAT, or OAuth) |
| Targets | POST /v1/targets, at most 10 per Maximizer instance |
| Subscriptions | POST /v1/subscriptions: one Entity and Op per target, with optional filters and recipients |
| Method | POST |
| Body | JSON array of webhook objects |
| Authentication | Headers you set on the target. No signature |
| Response requirement | 200 OK within 2 seconds |
| Retries | Two more attempts after a short delay, then the webhook is discarded |
| Versioning | Data.Version per entity, major.minor |
Common events
Maximizer identifies a webhook by its Entity and Data.Op together. The documented combinations are:
Entity | Op |
|---|---|
AbEntry (address book entries) | Create, Update, Delete |
Lead | Create, Update, Delete |
LeadDefault | Create, Update, Delete |
Opportunity | Create, Update, Delete |
InteractionLog | Create, Update, Delete |
InteractionLogDefault | Create, Update, Delete |
Export | Read |
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"
}
}
]
Idis a unique webhook ID.Timestampis when the change happened at the source;ProcessedTimestampis when Maximizer forwarded it.WorkspaceIdidentifies the Maximizer database.Recipientscarries any recipients set on the subscription.Optionsis reserved, andSourceIdis for Maximizer's internal use.Data.Originalholds the previous values and is null on create;Data.Newholds the changed values and is null on delete.Data.Versionis 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
200well 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 documents200 OKas 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
EntityandData.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.Versionand 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
Headersinstead.
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.