Solvey.

Recipe

Tickets in a Slack channel

Slack's incoming webhooks want Slack's JSON, and Solvey sends its own, so a thirty-line relay sits between them: verify the signature, turn the event into one line, post it. Run it anywhere that runs Node.

1. Get the two URLs

  1. In Slack: Apps → Incoming Webhooks → Add to Slack, pick the channel, copy the https://hooks.slack.com/services/… URL.
  2. Decide where the relay will run and what its public URL is — a small VM, a container, a serverless function. It needs to be reachable by Solvey.

2. The relay

relay.mjs
// relay.mjs — verify Solvey's signature, post a line to Slack. Node 20+, no dependencies.
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.SOLVEY_WEBHOOK_SECRET;      // whsec_… from Admin center → Webhooks
const SLACK = process.env.SLACK_WEBHOOK_URL;            // https://hooks.slack.com/services/…
const SOLVEY = process.env.SOLVEY_URL;                  // https://your-solvey-host

function verify(header, body) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", SECRET).update(`${t}.${body}`).digest("hex");
  return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

function line(event) {
  const t = event.data.ticket;
  const link = `<${SOLVEY}/tickets/${t.id}|${t.key}>`;
  switch (event.type) {
    case "ticket.created":        return `🆕 ${link} ${t.subject} — ${t.priority}, from ${t.requester.name ?? t.requester.email}`;
    case "ticket.status_changed": return `↪️ ${link} is now *${t.statusLabel}*`;
    case "ticket.replied":        return `💬 ${link} ${event.data.comment.author?.name ?? "someone"} replied: ${event.data.comment.body.slice(0, 140)}`;
    default:                      return `${event.type} ${link}`;
  }
}

createServer((req, res) => {
  let body = "";
  req.on("data", (c) => (body += c));
  req.on("end", async () => {
    if (!verify(req.headers["solvey-signature"] ?? "", body)) { res.statusCode = 401; return res.end(); }
    const event = JSON.parse(body);
    await fetch(SLACK, { method: "POST", headers: { "content-type": "application/json" },
      body: JSON.stringify({ text: line(event) }) });
    res.statusCode = 200; res.end("ok");
  });
}).listen(process.env.PORT ?? 8787);

3. Subscribe

In Admin center → Webhooks, add the relay's URL and choose the events — ticket.created, ticket.status_changed and ticket.replied make a good channel. Copy the secret into SOLVEY_WEBHOOK_SECRET, set SLACK_WEBHOOK_URL and SOLVEY_URL, start the relay:

shell
SOLVEY_WEBHOOK_SECRET=whsec_… SLACK_WEBHOOK_URL=https://hooks.slack.com/services/… SOLVEY_URL=https://your-solvey-host node relay.mjs

Create a ticket. The delivery appears in the subscription's log as succeeded and the line lands in the channel. If the log says 401, the secret in the relay does not match the subscription; if it says a timeout, Solvey could not reach the relay's URL.

Why a relay, and not a direct URL

Because the relay verifies the signature, nothing can post to your channel by guessing the relay's address. And because it is yours, you decide what a line says. The same shape works for Teams, Discord, PagerDuty or your own system — only the line() function and the destination change. The details of what Solvey sends are on Webhooks.