Menu

Webhooks

MB Suite tells you with a signed POST when something happens in your organization: how to subscribe, verify the signature and handle retries.

A webhook is a subscription: you choose the events and a public https:// URL, and every time one happens MB Suite sends a POST with the signed event. They are created and managed in Settings → API & MCP → Webhooks (owners and admins). Through the API and the MCP you can read the subscriptions, the event catalog and the deliveries.

Create a subscription

  1. 1Create webhook: your server's URL, the events, the format (Full or IDs only) and, in a workspace, whether it also listens to its Accounts.
  2. 2In Advanced, up to 5 static headers if your receiver requires its own token (Make, Monday). Their values are stored encrypted and never shown again.
  3. 3Copy the signing secret (whsec_…): it is shown only once. It can be rotated; for 24 hours events are signed with both the new and the previous one.
  4. 4Send test event sends a signed webhook.test, even if the subscription is turned off.

Each organization has a limit of active subscriptions according to its plan, and never more than 20. The destination must be a public server: private, internal or MB Suite addresses are rejected, and they are checked again before every send.

What your server receives

A POST with Content-Type: application/json, a body of up to 256 KB and these headers:

HeaderValue
MB-Signaturet=<unix>,v1=<hex> (see below).
MB-Event-IdThe event id (evt_…).
MB-Event-TypeThe type, for example crm.deal.stage_changed.
MB-Webhook-IdThe subscription.
MB-Delivery-IdThe delivery (a manual resend creates another one).
MB-Api-VersionThe version the body was built with.
Idempotency-KeyThe event id: the same on every retry.
User-AgentMB-Suite-Webhooks/1.0 (+https://developers.mb-suite.com/webhooks)
{
  "id": "evt_4f9c1a7e2b6d0c3a8e5f1b9d2c",
  "type": "crm.deal.stage_changed",
  "created_at": "2025-11-01T12:26:40.000Z",
  "api_version": "2026-11-01",
  "org_id": "ORG_ID",
  "data": { "…": "…" }
}
  • Full (snapshot, recommended): data carries the whole records, in the subscription's version.
  • IDs only (thin): only ids and simple values; your system fetches the rest through the API.
  • An event that does not fit in 256 KB is sent as IDs only, with "truncated": true in data.

The full list of events, with an example of each, is in Events. Each operation in the reference states which events it can emit; what an event does not cover yet is stated in its description.

Verify the signature

MB-Signature is t=<unix>,v1=<hex>, with v1 = HMAC-SHA256(secret, "<t>.<body>") over the exact body, without re-serializing it. During a rotation two v1= arrive: either one is valid. Reject a t older than 5 minutes (replay) and compare in constant time.

import crypto from "node:crypto";
import express from "express";

function verifyMbSignature(header, rawBody, secret, toleranceSec = 300) {
  if (!header) return false;
  const parts = header.split(",").map((p) => p.trim().split("="));
  const t = Number(parts.find(([k]) => k === "t")?.[1]);
  const signatures = parts.filter(([k]) => k === "v1").map(([, v]) => v);
  if (!t || signatures.length === 0) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false; // replay
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return signatures.some((sig) => sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
}

const app = express();
// The raw body: express.raw, NOT express.json (re-serializing changes the signature).
app.post("/mb-suite/webhook", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyMbSignature(req.get("MB-Signature"), req.body, process.env.MB_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  // event = { id, type, created_at, api_version, org_id, data }
  // Idempotency: `event.id` (also sent as `Idempotency-Key`) repeats when we retry.
  switch (event.type) {
    case "crm.lead.created":
      // event.data.person, event.data.prospect | event.data.deal, event.data.source
      break;
    case "crm.deal.stage_changed":
      // event.data.deal, event.data.stage, event.data.previous.stage
      break;
  }
  res.sendStatus(200); // any 2xx = delivered; anything else or more than 10 s = retry
});

Responding and retries

  • Answer 2xx in less than 10 seconds and leave heavy work to a queue. Any other answer, a redirect (3xx) or silence count as a failure.
  • Retries: right away, after 5 minutes, after 30, after 2 hours, after 5, after 10 and then every 12 hours: 11 attempts over about 72 hours.
  • A retry carries the same event id: use it to avoid processing twice.
  • After 24 hours of failures in a row, owners and admins get an email; at 72 hours the subscription turns itself off. When you reactivate it you can resend what failed in the previous 72 hours, and every delivery can be resent by hand from View deliveries.
  • Events and deliveries are kept for 30 days.

Recipes: Make, Odoo and Monday

Make

  1. 1In Make, create a scenario that starts with Webhooks → Custom webhook and copy its URL (https://hook.us1.make.com/…).
  2. 2In MB Suite, Create webhook with that URL and the events you want (for example crm.lead.created and crm.deal.stage_changed).
  3. 3In Make, Re-determine data structure; in MB Suite, Send test event (or a real change) so Make learns the JSON's shape.
  4. 4Make does not verify HMAC: add a static header (X-Make-Token: <a long value>) and compare it in a Filter of the scenario.

Odoo

Odoo 17 or later has automation rules with the On webhook trigger: each rule gives its own URL (https://<your-odoo>/web/hook/<uuid>) and an Execute Python Code action reads payload (the whole event). Since that action does not see the raw body, protect it with a static header and store payload["id"] so a retry does not create the lead twice. On older versions, put Make in the middle with Odoo → Create a Record.

Monday

Monday does not receive webhooks from other systems: the simplest way is Make in the middle (Custom webhook → Monday → Create an Item), mapping data.person.full_name, data.deal.title, data.stage.name or data.source.name. To avoid duplicate items on a retry, filter by the event id or look the item up by data.deal.id before creating it.