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.