All integrations

Integrations · Receive events

Outbound webhooks

Every notification fires an HTTPS POST to your receiver in real time. HMAC-SHA256 signed; verify and act.

Configure

  1. Per-employee: /me/agent → Webhook section → Add Webhook. One signing secret per row.
  2. Tenant-wide config (/admin/integrations) is not yet shipped. Until it is, admins / hr_admins subscribe to global signals like team_support.approved via their own per-employee webhook at /me/agent.
  3. Save → page now shows the signing secret. Copy to receiver; rotate from same row if it leaks.

Request shape

POST with these headers + JSON body. 8-second timeout per attempt; 10 consecutive 4xx/5xx auto-disables.

shell
# Use any HTTPS receiver (ngrok / cloudflared / your prod host).
# Configure it once at /me/agent (tenant-wide /admin/integrations is on the roadmap).
# Inbound POST has these headers:
#   X-FluxDesk-Signature:   v1=<hex-sha256-hmac-of-body>
#   X-FluxDesk-Event-Type:  task_assigned | review | blocker | ...
#   X-FluxDesk-Delivery-Id: <uuid>
#
# Verify HMAC, then act on the body.
curl -X POST https://your-receiver.example.com/fluxdesk-events \
  -H 'content-type: application/json' \
  -H 'X-FluxDesk-Signature: v1=<hex>' \
  -H 'X-FluxDesk-Event-Type: task_assigned' \
  -d '{"version":1,"type":"task_assigned","title":"…"}'

Verify HMAC (Node / Express)

Recompute HMAC-SHA256(secret, raw_body) and compare in constant time with the value after v1= in X-FluxDesk-Signature. Get the RAW body, not parsed JSON, before verifying.

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

const SECRET = process.env.FLUXDESK_WEBHOOK_SECRET!;
const app = express();

// IMPORTANT: raw body, not parsed JSON, for HMAC.
app.post(
  "/fluxdesk-events",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.header("x-fluxdesk-signature") ?? "";
    const expected =
      "v1=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
    const ok =
      sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
    if (!ok) return res.status(401).end("invalid signature");

    const event = JSON.parse(req.body.toString("utf8"));
    console.log("got event:", event.type, event.title);
    res.status(204).end();
  },
);

app.listen(8787);

Event types

Subscribe to all and branch on type:

  • task_assigned

    A request was dispatched to the user.

  • task_status

    A request the user is involved in changed status.

  • blocker

    A blocker was raised, acknowledged, or resolved.

  • review

    A request entered the review queue or was returned.

  • meeting

    Meeting reminder (10 min before, etc.).

  • weekly_digest

    Monday founder digest.

  • team_support.approved

    Team-support request approved (admin / fan-out hook).

  • system

    Cron, GitHub auto-links, membership changes, etc.