Webhooks (outbound events)
Register an https URL and the events you want. When one happens, mxAURA POSTs a signed JSON payload to it — immediately, then with retries. Your systems react instead of polling.
Events
| Event | Fires when | Payload data |
|---|---|---|
dispatch.sent |
A campaign message was selected and its properties confirmed written to the contact (HubSpot sends next). This is the same guarantee the HubSpot action contract makes. | campaign_id, campaign_name, pool_id, pool_name, pool_type, message_id, message_name, message_position, contact_id, portal_id |
dispatch.blocked |
A workflow reached a campaign that cannot send | campaign_id, campaign_name, reason_code (no_sender, no_branding, reauth_required, campaign_disabled), reason, portal_id, contact_id |
article.published |
An article went live in a hub | article_id, hub_id, placement_id, published_at |
hub.subscribed |
A new subscription (embed key) was created for a hub | hub_id, hub_name, subscription_id, subscriber_account_id, subscriber_name |
webhook.test |
You pressed Send test | message |
* subscribes to all.
Payload
{
"id": "6f1c0e2a-…", // event id, unique
"event": "dispatch.sent",
"created_at": "2026-09-04T03:25:13.490Z",
"account_id": "bc486e46-…",
"data": { "campaign_id": "…", "message_name": "Welcome 1", "contact_id": "124303957916", "…": "…" }
}
Headers on every delivery:
Content-Type: application/json
User-Agent: mxAURA-Webhooks/1
X-MXA-Event: dispatch.sent
X-MXA-Delivery: <delivery id — unique per attempt series>
X-MXA-Signature: t=1725420313,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>
Verify the signature
Always verify. Anyone can POST JSON to a URL; only mxAURA knows your secret.
- Read
X-MXA-Signature; split intotandv1. - Compute
HMAC-SHA256(secret, t + "." + rawBody)over the raw request body (before any JSON parsing). - Compare to
v1with a constant-time comparison. - Reject if
tis older than five minutes (replay protection).
Node (Express)
import crypto from 'node:crypto';
app.post('/hooks/mxaura', express.raw({ type: 'application/json' }), (req, res) => {
const sig = Object.fromEntries((req.get('X-MXA-Signature') || '').split(',').map(p => p.split('=')));
const expected = crypto.createHmac('sha256', process.env.MXA_WEBHOOK_SECRET).update(`${sig.t}.${req.body}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(sig.t)) < 300;
if (!fresh || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1 || ''))) return res.status(401).end();
const event = JSON.parse(req.body);
// handle event.event / event.data …
res.status(200).end();
});
Python (Flask)
import hmac, hashlib, time, json
from flask import Flask, request, abort
app = Flask(__name__)
@app.post('/hooks/mxaura')
def hook():
parts = dict(p.split('=', 1) for p in request.headers.get('X-MXA-Signature', '').split(','))
expected = hmac.new(SECRET.encode(), f"{parts['t']}.".encode() + request.get_data(), hashlib.sha256).hexdigest()
if abs(time.time() - int(parts['t'])) > 300 or not hmac.compare_digest(expected, parts.get('v1', '')):
abort(401)
event = json.loads(request.get_data())
return '', 200
Cloudflare Worker
export default { async fetch(req, env) {
const raw = await req.text();
const sig = Object.fromEntries((req.headers.get('X-MXA-Signature') || '').split(',').map(p => p.split('=')));
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(env.MXA_WEBHOOK_SECRET), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const mac = [...new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${sig.t}.${raw}`)))].map(b => b.toString(16).padStart(2, '0')).join('');
if (mac !== sig.v1 || Math.abs(Date.now() / 1000 - Number(sig.t)) > 300) return new Response('bad signature', { status: 401 });
const event = JSON.parse(raw);
return new Response('ok');
}};
Delivery, retries, pausing
- Respond 2xx within 10 seconds. Anything else is a failure. Do the work after you respond (queue it) if it takes longer.
- Failures retry at 1 min, 5 min, 30 min, 2 h, 12 h — five retries, six
attempts. Then the delivery is marked
failed. - An endpoint that fails 20 times in a row is paused: deliveries stop
until you resume it (app or
PUT /webhooks/{id} { "status": "active" }). The app and deck show paused endpoints and the last error. - Deliveries are logged for 30 days:
GET /webhooks/{id}/deliveries.
Ordering and duplicates
Deliveries are not guaranteed in order (retries reorder them). Treat id
as the deduplication key: if you have seen it, ignore it. An event is never
delivered twice as a distinct id, but a retry of the same delivery carries
the same id and X-MXA-Delivery.
Endpoint requirements
httpsonly.- A public host: literal private/loopback/link-local addresses,
localhost,*.local,*.internal, and mxAURA's own hostnames are refused. - At most 10 endpoints per account.
Managing endpoints
In the app: Account → API → Webhooks (add, edit events, send test,
delivery log, pause/resume, rotate secret, remove). Over the API: the
webhooks family in the resource reference.
Make, Zapier, n8n
Each of these gives you a "catch webhook" URL. Paste it as the endpoint,
choose the events, send a test, and map data.* into the rest of the
scenario. The signature can be verified in a code step if the platform
supports one; if not, at minimum keep the URL secret and pause the endpoint
if you see traffic you did not expect.