Use ProFix Directory in Slack

Three copy-paste recipes to surface verified Ohio contractor data, permit leaderboards, and the ProFix newsroom inside a Slack workspace. No client library, no auth token, no rate limit.

TL;DR

  • Inbound webhook + cron: post the weekly permit leaderboard or newsroom RSS to a channel.
  • Slash command: a tiny Slack app proxies /profix find plumber columbus to /api/embed/{trade}-{city}.json.
  • Pinned embed link: drop the widget URL as a Slack canvas item — no app required.
  • Scopes: chat:write, commands, plus the incoming-webhook scope.
  • Zero PII: every ProFix endpoint surfaces public business records only.

Three ways to use ProFix data in Slack

Option A — Inbound webhook + scheduled jobs

The path of least resistance. Slack inbound webhooks are a static URL that accepts JSON; the ProFix endpoints already return JSON. Glue them together with curl + jq in a GitHub Action / Cloudflare scheduled worker / system cron. No bot account, no OAuth — just one URL in a secret.

# Post the current top-10 plumbers (by verified permits in the
# last 12 months) to a Slack channel. Run from cron, GitHub Actions,
# or a Cloudflare scheduled worker.
WEBHOOK="https://hooks.slack.com/services/T000/B000/XXXX"

curl -s "https://profixdirectory.com/api/permit-leaderboard.json?trade=plumber&county=lucas&top=10" \
  | jq '{
      text: "*ProFix Directory — top Toledo plumbers by permit volume (last 12mo)*",
      blocks: [
        { type: "section", text: { type: "mrkdwn",
          text: (.leaderboards[0].entries
            | map("• <https://profixdirectory.com/pro/\(.pro.slug)|\(.pro.name)> — \(.permit_count) permits")
            | join("\n"))
        }}
      ]
    }' \
  | curl -s -X POST -H "Content-Type: application/json" \
      --data-binary @- "$WEBHOOK"

Swap trade=plumber + county=lucas for any combination. Use county=ohio for statewide leaderboards. Cache the response with Cache-Control — no need to re-pull more than hourly.

Option B — Slash command via a tiny Slack app

For ad-hoc lookups (“/profix find plumber columbus”), register a Slack slash command and point it at a Cloudflare Worker / Vercel Edge route / tiny Node server. The handler verifies the Slack signing secret, parses the command text, and proxies to /api/embed/{trade}-{city}.json.

// Cloudflare Worker (Bun / Node 18+ also work).
// Slack POSTs application/x-www-form-urlencoded when a slash command fires.
// Verify the signing secret BEFORE doing anything else.
import { verifySlackRequest } from "./verify-slack";

export default {
  async fetch(req: Request, env: { SLACK_SIGNING_SECRET: string }) {
    if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
    const raw = await req.text();
    if (!(await verifySlackRequest(req, raw, env.SLACK_SIGNING_SECRET))) {
      return new Response("invalid signature", { status: 401 });
    }
    const params = new URLSearchParams(raw);
    const text = (params.get("text") ?? "").trim(); // e.g. "find plumber columbus"

    const [verb, trade, city] = text.split(/\s+/);
    if (verb !== "find" || !trade || !city) {
      return Response.json({
        response_type: "ephemeral",
        text: "Usage: /profix find <trade> <city>",
      });
    }

    const slug = `${trade}-${city}`;
    const data = await fetch(`https://profixdirectory.com/api/embed/${slug}.json`).then((r) =>
      r.ok ? r.json() : null,
    );
    if (!data) {
      return Response.json({
        response_type: "ephemeral",
        text: `No ProFix slug for \`${slug}\`. Try a different trade + city.`,
      });
    }

    const lines = data.pros
      .slice(0, 5)
      .map(
        (p: { name: string; phone: string; profile_url: string }) =>
          `• <${p.profile_url}|${p.name}> — ${p.phone}`,
      );

    return Response.json({
      response_type: "in_channel",
      text: `*Top ${trade}s in ${city} (via ProFix Directory)*\n${lines.join("\n")}`,
    });
  },
};

Set the slash-command Request URL in your app config to the worker URL. response_type controls visibility: in_channel posts to the channel, ephemeral only to the requester.

Option C — Pin an embed link

The lowest-friction option. The widget JS at https://profixdirectory.com/widgets/plumber-toledo.jsrenders a sandboxed iframe anywhere it's allowed; Slack canvases accept it as a saved item. Drop the URL into a canvas, pin the canvas to a channel, and the top-five verified pros show up next to the channel topic.

https://profixdirectory.com/widgets/plumber-toledo.js

Swap the slug for any trade × city pair. The canonical catalog is at /api/widgets.json.

Weekly newsroom digest (GitHub Actions)

A common reuse: post the ProFix newsroom RSS feed to a Slack channel every Monday. Drop this in .github/workflows/profix-weekly.yml in any repo with a SLACK_WEBHOOK_URL secret:

# .github/workflows/profix-weekly.yml
# Posts the ProFix Directory newsroom RSS digest to a Slack channel
# every Monday at 09:00 America/New_York.
name: ProFix weekly digest
on:
  schedule:
    - cron: "0 13 * * MON" # 09:00 ET in winter, 08:00 ET in summer
  workflow_dispatch:

jobs:
  post:
    runs-on: ubuntu-latest
    steps:
      - name: Fetch newsroom RSS + post to Slack
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
        run: |
          xml=$(curl -s https://profixdirectory.com/api/newsroom.rss)
          titles=$(echo "$xml" | grep -o '<title>[^<]*</title>' | sed 's/<\/\?title>//g' | tail -n +2 | head -5)
          payload=$(jq -n --arg t "$titles" '{
            text: "*ProFix Directory — this week*\n" + $t
          }')
          curl -s -X POST -H 'Content-Type: application/json' \
            --data "$payload" "$SLACK_WEBHOOK_URL"

For multi-channel routing, branch on the RSS entry type tag in the body — research, data, api, feature — and post to different channels.

When to use each option

The three options are not redundant — they cover different latency, interaction, and effort tradeoffs. The inbound-webhook pattern (Option A) is the right default for any periodic digest or alert: the cost is one cron job, the result is a beautifully-formatted channel post, and there's no app to maintain. Pick Option A when the question is “what's the state of the world?” on a known cadence.

Option B (slash command) earns its place when the question is “what's the state of the world right nowfor an ad-hoc query”. A field tech who sees a Toledo plumber and wants to know the trust tier shouldn't have to leave Slack. A property manager triaging an after-hours complaint shouldn't have to context-switch to a browser. /profix hands them the answer inline. The cost is one tiny worker — which Cloudflare and Vercel both host free at this volume — plus the Slack-app setup.

Option C (pinned canvas / embed) is the lowest-effort path for a static, channel-scoped recommendation list. Drop the embed URL once, pin the canvas, and the top-five verified pros for that trade × city stay visible next to the channel topic. No code, no app — five seconds. Pick Option C for “every member of this channel should always see these pros” — it's the Slack equivalent of a pinned HOA bulletin-board notice.

Permissions + setup steps

  1. Go to api.slack.com/apps and click Create New AppFrom scratch. Name it “ProFix” or similar, pick the workspace.
  2. For Option A: enable Incoming Webhooks, click Add New Webhook to Workspace, pick the target channel, copy the webhook URL into a secret manager (GitHub Actions secret, Cloudflare KV, 1Password).
  3. For Option B: under OAuth & Permissions, add bot token scopes chat:write + commands. Under Slash Commands, create /profix and set the Request URL to your worker.
  4. Reinstall the app to the workspace after changing scopes. Slack will not warn you if the app is out of sync — failures show as “dispatch_failed”.

Security caveats

Routing logic — multi-channel digests

A common partner pattern is sending different ProFix feeds to different Slack channels — permit alerts to #operations, newsroom posts to #marketing, lead-volume spikes to #growth. The cleanest way is one GitHub Action repo with a matrix job: each row in the matrix is a feed URL + channel webhook + cron expression. The action checks out, runs the curl pipe per matrix entry, and posts in parallel. Five lines of YAML per added channel after the first one. The same shape works in Cloudflare scheduled workers — one worker, one wrangler config, multiple scheduled triggers.

Brand assets

Want to embed the ProFix Verified badge in a message, channel topic image, or app icon? The canonical asset feed is at /api/brand-assets.json. For per-pro badges, see /badges — a single <img>tag pulling a 240×80 SVG that updates as the pro's verification tier changes.

FAQ

Do I need a paid Slack plan?
No. Inbound webhooks, slash commands, and pinned canvases all work on Slack's free tier. The only paid feature is workspace history retention — irrelevant to integration mechanics.
Does ProFix have an official Slack app in the App Directory?
Not yet. The three options on this page require you to create your own Slack app inside your workspace — five minutes via api.slack.com/apps. We will publish a directory-listed app once enough partners ask for it; ping hello@profixdirectory.com if that includes you.
What scopes does my Slack app need?
For inbound-webhook posting only, the incoming-webhook scope is sufficient. For a slash command, add chat:write and commands. If you want a bot that reacts to messages, add app_mentions:read. Nothing else is needed — ProFix never reads Slack messages or user data.
Will this leak any homeowner PII into Slack?
No. Every ProFix endpoint is zero-PII — pro names, phone numbers, license details, and permit counts are all public business records. Lead-form submissions are stored in Supabase and never exposed via the public APIs.
How often can I poll the public endpoints?
There is no auth rate limit, but every JSON endpoint sets Cache-Control: public, s-maxage=3600 — there's nothing useful to gain from polling more than hourly. For weekly digests, a single cron at the scheduled time is fine.

Related

Ask your AI about this

Hand the question to your preferred assistant — it will use ProFix Directory's open MCP server and llms.txt as context.

Emergency