← Back to home

Integration · Slack

DirtFleet in Slack.

Three patterns from simplest to richest: a one-URL Incoming Webhook, a tiny Block-Kit shim, or a dedicated Slack app with slash commands. Pick the one that matches how much infrastructure you already have.

Pick a pattern

Effort vs. flexibility.

The simplest pattern is 10 minutes and no code. The richest is a real Slack app with slash commands. All three are valid; pick the one matching what you already have running.

  1. Pattern 1 — Slack Incoming Webhook (simplest, 10 min)

    Slack's per-channel Incoming Webhook is a single URL. Register that URL as a DirtFleet webhook subscription and you're done. Slack auto-renders the JSON if you shape it with their text/blocks contract.

    When to pick this

    Right when you want a single #flags or #ops channel to get every event. Zero infrastructure on your side.

    Show the code
    # 1. In Slack: Apps → "Incoming Webhooks" → new webhook for #flags
    #    You get a URL like https://hooks.slack.com/services/T00.../B00.../...
    #
    # 2. In DirtFleet: /settings → Webhooks → New
    #    URL: <the Slack hook URL>
    #    Events: flag.created
    #    Save (store the DirtFleet secret if you'll do step 3).
    #
    # 3. The problem: Slack's Incoming Webhook expects { "text": "..." },
    #    not DirtFleet's envelope shape. You have two options:
    #
    #    (a) Use Zapier as a transformer (see /integrations/zapier).
    #    (b) Run a tiny shim — see Pattern 2 below.
  2. Pattern 2 — Slack Block Kit via a tiny shim (most flexible)

    Stand up a 30-line Node/Express receiver that converts DirtFleet's envelope into Slack Block Kit. Verify the signature on the way in. Best for shops that already have a deployed app to add a route to.

    When to pick this

    When the shop already has any deployed server and wants rich Slack messages (action buttons, links, color bars) instead of plain text.

    Show the code
    import express from "express";
    import { createHmac, timingSafeEqual } from "node:crypto";
    
    const SECRET = process.env.DIRTFLEET_WEBHOOK_SECRET!;
    const SLACK_HOOK = process.env.SLACK_WEBHOOK_URL!;
    
    const app = express();
    app.post(
      "/dirtfleet-to-slack",
      express.raw({ type: "application/json", limit: "256kb" }),
      async (req, res) => {
        // Verify signature against the raw body
        const sig = req.header("x-fleetgo-signature") ?? "";
        const ts = Number(req.header("x-fleetgo-timestamp") ?? 0);
        if (Math.abs(Math.floor(Date.now() / 1000) - ts) > 300)
          return res.status(401).end();
        const raw = (req.body as Buffer).toString("utf8");
        const expected =
          "v1=" + createHmac("sha256", SECRET).update(`${ts}.${raw}`).digest("hex");
        if (
          expected.length !== sig.length ||
          !timingSafeEqual(Buffer.from(expected), Buffer.from(sig))
        )
          return res.status(401).end();
    
        const env = JSON.parse(raw);
        const slackBody = renderSlack(env);
        await fetch(SLACK_HOOK, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(slackBody),
        });
        res.status(200).end();
      },
    );
    
    function renderSlack(env: { event: string; data: Record<string, unknown> }) {
      if (env.event === "flag.created") {
        const d = env.data as { severity: string; assetNickname?: string; reason?: string };
        return {
          text: `${d.severity} flag on ${d.assetNickname ?? "an asset"}`,
          blocks: [
            { type: "section", text: { type: "mrkdwn",
              text: `*${d.severity}* flag on *${d.assetNickname}*\n>${d.reason ?? "—"}` } },
          ],
        };
      }
      return { text: `DirtFleet: ${env.event}` };
    }
    app.listen(3000);
  3. Pattern 3 — Dedicated Slack App with slash commands

    For shops that want bidirectional — receive flag events AND let mechanics resolve flags from Slack — install a real Slack App that hits the DirtFleet REST API both directions.

    When to pick this

    When the shop wants /flag resolve <id> as a Slack slash command, asset lookups via /asset CAT320, and similar two-way workflows.

    Show the code
    # Bidirectional, simplified
    #
    # A. Outbound (DirtFleet → Slack): same shim as Pattern 2.
    #
    # B. Inbound (Slack → DirtFleet): handle slash command POSTs from Slack
    #    and call the DirtFleet REST API.
    #
    # Express handler for /flag-resolve <flag-id> <resolution note>:
    app.post("/slack/flag-resolve", express.urlencoded({ extended: true }), async (req, res) => {
      const text = (req.body.text ?? "").trim();
      const [flagId, ...noteWords] = text.split(/\s+/);
      if (!flagId) return res.send("Usage: /flag-resolve <flag-id> <note>");
      const note = noteWords.join(" ");
    
      const r = await fetch(
        `https://dirtfleet.app/api/v1/flags/${flagId}`,
        {
          method: "PATCH",
          headers: {
            Authorization: `Bearer ${process.env.DIRTFLEET_API_KEY}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ status: "RESOLVED", resolutionNote: note }),
        },
      );
    
      if (!r.ok) {
        const body = (await r.json().catch(() => ({}))) as { formError?: string };
        return res.send(`Failed: ${body.formError ?? r.status}`);
      }
      res.send(`Resolved flag ${flagId}`);
    });

Don't want to build this yourself?

Use Zapier.

The Zapier path skips the shim entirely. /integrations/zapier has the field mappings for the "Red flag → Slack channel" pattern (Pattern 1 above) with no code required. Trade-off: Zapier adds a per-event cost and 30-90s latency for the relay; direct integration is free and near-instant.

Reference

The API surface you're hitting.

  • Webhook payloads: /docs/api/webhooks (signing, replay window, retries) — AsyncAPI spec at /asyncapi.yaml.
  • REST endpoints for slash-command handlers: /docs/api — OpenAPI at /openapi.yaml.
  • Local testing: POST /api/v1/webhooks/{id}/test fires a synthetic delivery with the live signature — pair with ngrok or cloudflared to develop locally.