Guide to Mautic Webhooks: Features and Best Practices
Mautic webhooks send your other systems what happens to your contacts in Mautic: a new contact is identified, a form is submitted, an email is sent or opened, a landing page is visited, or a contact's points change. Mautic is the open-source marketing automation platform, and webhooks are how its contact activity reaches a CRM, a data warehouse or an internal tool without polling its API.
This guide covers how Mautic webhooks are configured and signed, how the payload is structured in real-time and queued modes, the events you can subscribe to, how Mautic handles failures, and the best practices for running a Mautic receiver in production.
What are Mautic webhooks?
A Mautic webhook has a name, a POST URL, a set of events and a secret key. When one of those events happens, Mautic sends a JSON POST to the URL with a Webhook-Signature header: a base64 HMAC-SHA256 of the raw body, keyed with the secret.
Mautic has two delivery modes, set once for the whole instance. In real-time mode, Mautic sends each event as it happens, one event per request. In queued mode, Mautic stores events in its database and a cron job sends them in batches, so one request can carry many events of several types.
The header name trips people up. Webhook-Signature is also the header name the Standard Webhooks specification uses, but Mautic's value is a bare base64 HMAC of the body. There is no webhook-id or webhook-timestamp, nothing else is signed, and a Standard Webhooks library will reject every Mautic delivery.
Mautic is open source and usually self-hosted, so every installation is its own sender. Deliveries come from wherever your Mautic instance runs.
Mautic webhook features
| Feature | Details |
|---|---|
| Configuration | Mautic admin: the cog menu, then Webhooks > New |
| Delivery modes | Process Events Immediately (real time) or Queue Events Only - Process Via CLI Command (batched by cron), set in Configuration > Webhook Settings |
| Batch order | Chronological or reverse chronological, for queued mode |
| Signature header | Webhook-Signature |
| Scheme | HMAC-SHA256, standard base64 |
| Signed content | The raw JSON body only |
| Secret | One per webhook, randomly generated by default and editable in the webhook form |
| Other headers | Content-Type: application/json, X-Origin-Base-URL (your instance's site URL) and User-Agent: Webhook, per Mautic's source |
| Replay window | None. No timestamp is signed |
| Event ID | None |
| Retries | Queued mode only: failed items are retried by later cron runs after webhook_retry_delay (default 3,600 seconds) |
| Auto-unpublish | A 410 response unpublishes the webhook immediately. A webhook with no successes in its recent log (default 100 entries) is unpublished unless auto-unpublish is disabled |
| Testing | Send Test Payload in the webhook form |
Common events
Mautic documents 13 event types:
| Area | Event type | Fires when |
|---|---|---|
| Contacts | mautic.lead_ | Mautic identifies a contact |
mautic.lead_ | A contact is updated | |
mautic.lead_ | A contact's points change | |
mautic.lead_post_delete | A contact is deleted | |
mautic.lead_ | A contact's channel subscription status changes | |
mautic.lead_ | A contact is added to or removed from a company | |
| Companies | mautic.company_post_save | A company is created or updated |
mautic.company_ | A company is deleted | |
| Email and SMS | mautic.email_on_send | Mautic sends an email to a contact |
mautic.email_on_open | A contact presumably opens an email | |
mautic.sms_on_send | Mautic sends a text message to a contact | |
| Forms and pages | mautic.form_on_submit | A contact submits a Mautic form |
mautic.page_on_hit | A contact visits a landing page or a tracked third-party page |
The body has no fixed envelope. Each top-level key is an event type, and its value is an array of events of that type:
{
"mautic.form_on_submit": [
{
"submission": {
"id": 7,
"form": { "id": 2, "name": "Contact us", "alias": "contactus" },
"lead": { "id": 42 },
"results": { "email": "jane@example.com" },
"referer": "https://example.com/contact",
"dateSubmitted": "2026-10-22T09:30:00+00:00"
},
"timestamp": "2026-10-22T09:30:00+00:00"
}
]
}
In real-time mode you get one key with one item. In queued mode one request can hold several keys, each with several items. Every item carries its own timestamp, the time Mautic queued the event, in ISO 8601 format. The rest of the item depends on the event: contact for most contact, email and SMS events, points.old_points and points.new_points for points changes, submission for forms, company for company events, and just an id for deletions.
Mautic's documentation gives property tables for each event but no example deliveries, and the page itself says it "requires a major update". Treat the property tables as a guide, and capture a real payload from your own Mautic version with Send Test Payload before you build on it.
Setting up Mautic webhooks
- If you want batched delivery, open Configuration from the cog menu, go to the Webhook Settings tab, and set Queue Mode to Queue Events Only - Process Via CLI Command. Choose the order of queued events. Leave it on Process Events Immediately for real-time delivery.
- Open Webhooks from the cog menu and click New.
- Enter a name and the Webhook POST URL, and select the events that should trigger the webhook.
- Keep the generated secret, or replace it with your own, and store it server-side, for example as
MAUTIC_WEBHOOK_SECRET. - Click Apply, then Send Test Payload to send an example request.
In queued mode, nothing is sent until the cron job runs. Schedule it on the Mautic server:
php bin/console mautic:webhooks:process
Mautic recommends one webhook per app or script, so that each secret is exposed in as few places as possible.
Mautic 7 refuses to post to private network addresses unless they're allowed in the webhook_ setting, so a localhost URL won't work for local testing there. Use a public tunnel or a Hookdeck source URL instead.
If you use Hookdeck, create a source with the Mautic source type, set the webhook's secret on it, and use the Hookdeck source URL as the Webhook POST URL.
Securing Mautic webhooks
Compute an HMAC-SHA256 of the raw request body, with the secret as a plain string, encode it as standard base64, and compare it in constant time with Webhook-Signature. Mautic's own sender does exactly this: base64_.
const crypto = require('crypto');
const express = require('express');
function verifyMauticSignature(rawBody, signatureHeader, secret) {
if (!signatureHeader || !secret) return false;
const expected = Buffer.from(crypto.createHmac('sha256', secret).update(rawBody).digest('base64'), 'utf8');
const received = Buffer.from(signatureHeader.trim(), 'utf8');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
const app = express();
app.post('/webhooks/mautic', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyMauticSignature(req.body, req.get('webhook-signature'), process.env.MAUTIC_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const payload = JSON.parse(req.body.toString('utf8'));
for (const [eventType, events] of Object.entries(payload)) {
for (const event of events) {
// Handle each event of each type. Persist first, then process async.
}
}
res.sendStatus(200);
});
The same check in Python:
import base64
import hashlib
import hmac
import os
def verify_mautic_signature(raw_body: bytes, signature_header, secret: str) -> bool:
if not signature_header or not secret:
return False
expected = base64.b64encode(hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).digest())
return hmac.compare_digest(signature_header.strip().encode("utf-8"), expected)
# In FastAPI: raw_body = await request.body()
# verify_mautic_signature(raw_body, request.headers.get("webhook-signature"),
# os.environ["MAUTIC_WEBHOOK_SECRET"])
To check your verifier, use the secret mautic-test-secret and this exact body:
{"mautic.lead_post_delete":[{"id":42,"timestamp":"2026-10-22T09:30:00+00:00"}]}
The Webhook-Signature header should be:
q9x+uMz7H8YMlxcXzEPO0J1jngWkYIyJcCI0dAVAGCc=
Make Mautic webhooks production-ready. Hookdeck Event Gateway verifies the
Webhook-Signatureheader on every Mautic request, accepts deliveries immediately, and retries them to your service, so a failed real-time delivery is no longer a lost event.
Mautic webhook limitations and pain points
Real-time deliveries are not retried
The Problem: If your endpoint is down when a contact submits a form, that event never arrives.
Why It Happens: In real-time mode, Mautic sends the event once while handling the action that triggered it. Its source only stores and retries events in queued mode, so a failed real-time delivery is logged and dropped.
Workarounds:
- Switch to queued mode if you can accept the delay of a cron interval. Failed queued items are retried by later cron runs once
webhook_retry_delay(an hour by default) has passed. - Reconcile from the Mautic API after an outage.
How Hookdeck Can Help: Put Hookdeck in front of your service. Hookdeck accepts every real-time delivery as soon as Mautic sends it and retries delivery to your service until it succeeds, so your own downtime no longer loses events.
A Standard Webhooks verifier rejects everything
The Problem: You use a Standard Webhooks library because the header is called Webhook-Signature, and every request fails.
Why It Happens: Mautic signs only the body, with the secret as a plain string, and sends a bare base64 digest. Standard Webhooks expects webhook-id and webhook-timestamp headers, a v1, prefix and a base64-encoded secret.
Workarounds:
- Verify with a plain HMAC-SHA256 of the raw body, as in the code above.
- Don't decode the secret, and don't add any prefix.
How Hookdeck Can Help: Hookdeck's Mautic source type uses Mautic's scheme rather than Standard Webhooks, so verification works without custom code.
One request, many events
The Problem: Your handler processes the first event and drops the rest, or breaks when two event types arrive together.
Why It Happens: In queued mode, Mautic aggregates events: every top-level key is an event type, and each holds an array. Even in real-time mode the single event is wrapped in an array.
Workarounds:
- Always iterate over every key and every item.
- Return
200only after all items are persisted, so a failure doesn't drop part of a batch.
How Hookdeck Can Help: Hookdeck stores each batched request as one event, so if your handler fails partway through a batch, you can fix the bug and replay the whole request from Hookdeck.
Endpoints get unpublished
The Problem: Webhooks stop firing and you only notice later.
Why It Happens: Mautic unpublishes a webhook immediately when your endpoint returns 410, and also when its recent log (100 entries by default) contains no successful responses, unless auto-unpublish is disabled.
Workarounds:
- Never return
410from your webhook route unless you mean "stop sending". - Monitor whether your webhooks are still published, and fix endpoints before their failure log fills up.
How Hookdeck Can Help: Hookdeck answers Mautic with a 2xx as soon as it has stored the request, so failures in your own service don't count against the webhook, and Hookdeck's issues and alerts tell you about delivery problems on your side.
No event ID and no replay protection
The Problem: You can't reliably tell a retried batch from a new one, and a captured request can be replayed.
Why It Happens: Items carry no event ID, and the signature covers only the body. Queued retries resend the same items.
Workarounds:
- Build an idempotency key from the event type, the main object's ID (contact, submission, company) and the item's
timestamp. - Keep the endpoint on HTTPS.
How Hookdeck Can Help: Hookdeck's deduplication can drop repeated requests before they reach your service, based on the fields you choose.
Best practices
Verify against the raw body
The signature covers the exact JSON bytes Mautic sent. Read the raw body (express.raw(), await request.body()) and parse only after verification.
Iterate over every event type and item
Loop over Object.entries(payload) and then over each array. Don't assume one key, and don't assume one item per key.
Treat mautic.email_on_open with care
Mautic itself says a contact "presumably" opens an email: the event depends on a tracking pixel, so mail clients that block images miss opens and privacy proxies can trigger them. Don't drive critical logic from it.
Pick the delivery mode deliberately
Real-time mode is immediate but has no retries. Queued mode retries but adds the cron interval to every event. If you need both speed and reliability, use real-time mode behind Hookdeck.
Deduplicate deliveries
Use a composite key such as event type, object ID and timestamp, and store it before processing. See our guide to webhook idempotency.
Acknowledge fast and process asynchronously
In Mautic's source, real-time mode sends the request inline while Mautic handles the action that caused it, so a slow endpoint slows Mautic down too. Persist the events, return 200, and process afterwards. See why to process webhooks asynchronously.
Conclusion
Mautic signs every webhook with a base64 HMAC-SHA256 of the raw body in Webhook-Signature, keyed with the plain secret, whatever the header name suggests. Verify against the raw bytes, loop over every event type and item, build your own idempotency keys, and choose between real-time delivery without retries and queued delivery with cron latency.
Hookdeck Event Gateway sits in front of your receiver, verifies every Mautic signature, and retries delivery to your service, so real-time Mautic webhooks get the reliability that Mautic only gives queued ones.
Get started with Hookdeck for free and handle Mautic webhooks reliably in minutes.