Receive signed JSON events at your own HTTPS endpoint when tasks, projects, or members change in your Momentum workspace. Available on the Studio plan.
| Type | When it fires |
|---|---|
task.created | A new task is added to the workspace. |
task.updated | Any field on an existing task changes — including modeler-side updates from the Revit addin (status flipping to Done, reminder dismissals, etc.). |
task.deleted | A task is deleted. |
project.created | A new project is added. |
project.updated | Any field on a project changes (name, allocated hours, checklist ticks, assigned modelers, etc.). |
project.deleted | A project is deleted. |
project.budget_threshold | A 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.joined | An invitee accepts your invite link and joins the workspace as a member. |
member.data_deletion_requested | A workspace user requests data deletion/export review through the privacy workflow. Payload includes the request id, requester email when available, and reason. |
seenUtc when a modeler views a task). Use the stable eventId to dedupe on your end if exact-once handling matters.
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:
type — one of the event types above.eventId — stable per write. Safe to use as a dedupe key.tenantId — your workspace ID. Always your own (events fan out per-tenant).createdUtc — ISO 8601, UTC, when we built the event.data — event-specific payload. Task and project objects mirror what you'd get back from the dashboard's API. Tenant IDs are stripped from the inner object since they're already on the envelope.| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Momentum-Webhook/1.0 |
X-Momentum-Event | The event type, e.g. task.created. |
X-Momentum-Delivery | Unique delivery ID (12 hex bytes). Same value across retries of the same delivery. |
X-Momentum-Signature | sha256=<hex> — HMAC-SHA256 of the raw request body, keyed with your endpoint's signing secret. |
X-Momentum-Timestamp | ISO 8601 UTC timestamp the request was signed at. |
Always verify the signature before trusting an incoming webhook. Use a constant-time comparison.
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();
});
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
express.raw(), Flask request.get_data(), FastAPI await request.body().
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:
failed permanently.)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.
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.
Email Info@getmomentum.studio with the URL of your endpoint and we'll help debug.