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.idfield) 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.