Skip to main content

API reference

Build a Slack bot for new matches

End-to-end tutorial: subscribe to match.matched webhooks, post into Slack, and let the recruiter accept / decline from a Slack message.

Last updated

Build a Slack bot that posts new Jobby.dev matches into a team channel and lets the on-call recruiter accept or decline directly from a Slack message. End-to-end implementation; ~1 hour including setup.

What we're building

Every time a Jobby.dev match fires for your team, an interactive Slack message lands in your #hiring-now channel with the candidate card and two buttons: Accept / Decline. Clicking accept opens the live room URL in the recruiter's browser; declining records the decision and rotates to the next match.

1. Create a Slack app

At api.slack.com/apps, create an app with these OAuth scopes:

  • chat:write — post messages to channels.
  • incoming-webhook — single-channel posting from a webhook URL.

Install to your workspace and copy the bot token + the incoming-webhook URL. Save both as env vars on your server.

2. Stand up an HTTPS endpoint

Minimal Express endpoint:

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

const app = express();
const JOBBYDEV_SECRET = process.env.JOBBYDEV_WEBHOOK_SECRET;
const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;

app.post(
  "/jobbydev",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    if (!verifySignature(req.header("Jobbydev-Signature"), req.body, JOBBYDEV_SECRET)) {
      return res.status(400).send("invalid signature");
    }
    const event = JSON.parse(req.body.toString("utf8"));
    if (event.type !== "match.matched") return res.status(200).send("ignored");

    await fetch(SLACK_WEBHOOK_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ blocks: matchBlocks(event.data) }),
    });
    res.status(200).send("ok");
  },
);

See verifying signatures for the verifySignature implementation.

3. Subscribe to webhooks

curl https://jobby.dev/api/v1/webhooks \
  -H "Authorization: Bearer $JOBBYDEV_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "target_url": "https://my-app.example.com/jobbydev",
    "events": ["match.matched"],
    "description": "Slack bot for new matches"
  }'

Copy the returned secret (prefixed jbb_whsec_) into your server's JOBBYDEV_WEBHOOK_SECRET env var.

4. Render an interactive Slack message

function matchBlocks(data) {
  return [
    {
      type: "section",
      text: { type: "mrkdwn", text: `*New match for ${data.role_title}*\n<https://jobby.dev/account/matches/${data.match_id}|${data.seeker_headline}>` },
    },
    {
      type: "actions",
      elements: [
        { type: "button", text: { type: "plain_text", text: "Accept" }, style: "primary",
          url: `https://jobby.dev/account/matches/${data.match_id}/accept` },
        { type: "button", text: { type: "plain_text", text: "Decline" }, style: "danger",
          action_id: "decline", value: data.match_id },
      ],
    },
  ];
}

Note: the Acceptbutton is a URL link — clicking it opens Jobby.dev in the recruiter's browser, where they authenticate and accept (per the humans-only rule). The Decline button uses an action_id for in-Slack handling — see step 5.

5. Wire the decline action

Add a Slack interactivity endpoint at POST /slack/interactivity. When clicked, decode thevalue (the match_id), call POST /api/v1/matches/{matchId}/decline with a PAT carrying matches:write, and update the original Slack message to remove the buttons.

app.post("/slack/interactivity", express.urlencoded({ extended: true }), async (req, res) => {
  const payload = JSON.parse(req.body.payload);
  const action = payload.actions?.[0];
  if (action?.action_id !== "decline") return res.status(200).send();

  const matchId = action.value;
  await fetch(`https://jobby.dev/api/v1/matches/${matchId}/decline`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.JOBBYDEV_API_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ reason: "declined_via_slack" }),
  });
  res.status(200).send({ replace_original: true, text: "Declined." });
});

6. Test

Use POST /api/v1/webhooks/{id}/test to fire a synthetic match.matched event. The Slack message should land within seconds. Click decline; confirm the original message updates and Jobby.dev shows the match as declined.

Production hardening

  • Idempotency: store processed event IDs (the event.id field) in Redis with a 24h TTL. Skip duplicates from at-least-once delivery.
  • Verify Slack's own signature on the interactivity endpoint — Slack signs requests the same way Jobby.dev does.
  • Don't do real work inline — accept the webhook, queue, return 200 within 10 seconds.

Related reading