Webhooks

Receive signed JSON events at your own HTTPS endpoint when tasks, projects, or members change in your Momentum workspace. Available on the Studio plan.

Quick start

  1. Open your dashboard as the workspace admin (Studio plan required).
  2. Find the Integrations card and click + Add endpoint.
  3. Enter your HTTPS URL, pick the events you care about, save.
  4. Copy the signing secret shown on screen — this is the only time it's displayed in full.
  5. Click Send test event in the Deliveries drawer to verify your endpoint receives + verifies the signature.

Event types

TypeWhen it fires
task.createdA new task is added to the workspace.
task.updatedAny field on an existing task changes — including modeler-side updates from the Revit addin (status flipping to Done, reminder dismissals, etc.).
task.deletedA task is deleted.
project.createdA new project is added.
project.updatedAny field on a project changes (name, allocated hours, checklist ticks, assigned modelers, etc.).
project.deletedA project is deleted.
project.budget_thresholdA project crosses a configured percentage of its allocated hours (e.g., 80% used). Each threshold fires exactly once per project — re-arm by removing/re-adding the threshold or bumping allocated hours. Payload includes threshold, usedPercent, usedHours, allocatedHours.
member.joinedAn invitee accepts your invite link and joins the workspace as a member.
member.data_deletion_requestedA workspace user requests data deletion/export review through the privacy workflow. Payload includes the request id, requester email when available, and reason.
Update events fire for every write. Including writes that don't change anything semantically (e.g. the addin stamping seenUtc when a modeler views a task). Use the stable eventId to dedupe on your end if exact-once handling matters.

Payload shape

Every event arrives as a JSON POST with this envelope:

{
  "type":       "task.updated",
  "eventId":    "task:abc123:2026-04-30T14:22:01.234Z",
  "tenantId":   "t-abcdef0123",
  "createdUtc": "2026-04-30T14:22:01.456Z",
  "data": {
    "task":   { /* current task fields */ },
    "before": { /* previous task fields, on update events */ }
  }
}

Field rules:

Headers

HeaderValue
Content-Typeapplication/json
User-AgentMomentum-Webhook/1.0
X-Momentum-EventThe event type, e.g. task.created.
X-Momentum-DeliveryUnique delivery ID (12 hex bytes). Same value across retries of the same delivery.
X-Momentum-Signaturesha256=<hex> — HMAC-SHA256 of the raw request body, keyed with your endpoint's signing secret.
X-Momentum-TimestampISO 8601 UTC timestamp the request was signed at.

Verifying the signature

Always verify the signature before trusting an incoming webhook. Use a constant-time comparison.

Node.js

const crypto = require('crypto');

function verifyMomentumSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader) return false;
  const expected = 'sha256=' + crypto.createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  const a = Buffer.from(signatureHeader);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// In an Express handler — make sure to use express.raw() so rawBody is intact:
app.post('/momentum-webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const ok = verifyMomentumSignature(
      req.body,
      req.header('X-Momentum-Signature'),
      process.env.MOMENTUM_WEBHOOK_SECRET);
    if (!ok) return res.status(401).send('bad signature');
    const event = JSON.parse(req.body.toString('utf8'));
    // ... handle event ...
    res.status(200).end();
  });

Python

import hashlib, hmac

def verify_momentum_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
    if not signature_header:
        return False
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature_header, expected)

# Flask example:
@app.post("/momentum-webhook")
def handle():
    raw = request.get_data()
    if not verify_momentum_signature(
        raw,
        request.headers.get("X-Momentum-Signature", ""),
        os.environ["MOMENTUM_WEBHOOK_SECRET"]):
        return "bad signature", 401
    event = json.loads(raw)
    # ... handle event ...
    return "", 200
Sign the raw bytes, not a re-serialized JSON object. If your framework parses JSON before you see the body, the round-trip will reorder keys / change whitespace and break verification. Express needs express.raw(), Flask request.get_data(), FastAPI await request.body().

Retries

Your endpoint should respond with a 2xx status as fast as possible — ideally under a second. We give 10 seconds before timing out.

Anything that's not a 2xx counts as a failed delivery. Failed deliveries retry on this schedule:

Each attempt reuses the same X-Momentum-Delivery header value. Use it for idempotency on your end — the same delivery may arrive more than once if your endpoint replied slowly the first time.

If you've fixed your endpoint and want to replay a failed delivery sooner, click Retry now in the Deliveries drawer on the dashboard.

Rotating your signing secret

Open the endpoint in the Integrations card and click Rotate secret. The new secret is shown once — copy it into your endpoint's environment first, then save. There's no overlap window: the very next delivery uses the new key.

Disabling vs deleting

Limits

Need a hand?

Email Info@getmomentum.studio with the URL of your endpoint and we'll help debug.