Gareth Wilson Gareth Wilson

Guide to Mautic Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationMautic admin: the cog menu, then Webhooks > New
Delivery modesProcess Events Immediately (real time) or Queue Events Only - Process Via CLI Command (batched by cron), set in Configuration > Webhook Settings
Batch orderChronological or reverse chronological, for queued mode
Signature headerWebhook-Signature
SchemeHMAC-SHA256, standard base64
Signed contentThe raw JSON body only
SecretOne per webhook, randomly generated by default and editable in the webhook form
Other headersContent-Type: application/json, X-Origin-Base-URL (your instance's site URL) and User-Agent: Webhook, per Mautic's source
Replay windowNone. No timestamp is signed
Event IDNone
RetriesQueued mode only: failed items are retried by later cron runs after webhook_retry_delay (default 3,600 seconds)
Auto-unpublishA 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
TestingSend Test Payload in the webhook form

Common events

Mautic documents 13 event types:

AreaEvent typeFires when
Contactsmautic.lead_post_save_newMautic identifies a contact
mautic.lead_post_save_updateA contact is updated
mautic.lead_points_changeA contact's points change
mautic.lead_post_deleteA contact is deleted
mautic.lead_channel_subscription_changedA contact's channel subscription status changes
mautic.lead_company_changeA contact is added to or removed from a company
Companiesmautic.company_post_saveA company is created or updated
mautic.company_post_deleteA company is deleted
Email and SMSmautic.email_on_sendMautic sends an email to a contact
mautic.email_on_openA contact presumably opens an email
mautic.sms_on_sendMautic sends a text message to a contact
Forms and pagesmautic.form_on_submitA contact submits a Mautic form
mautic.page_on_hitA 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

  1. 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.
  2. Open Webhooks from the cog menu and click New.
  3. Enter a name and the Webhook POST URL, and select the events that should trigger the webhook.
  4. Keep the generated secret, or replace it with your own, and store it server-side, for example as MAUTIC_WEBHOOK_SECRET.
  5. 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_allowed_private_addresses 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_encode(hash_hmac('sha256', $jsonPayload, $secret, true)).

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-Signature header 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 200 only 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 410 from 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.


Gareth Wilson

Gareth Wilson

Product Marketing

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