Installation
No client library required — every endpoint is fetchable with built-in fetch. Node 18+, Bun, Deno, Cloudflare Workers, and modern browsers all ship the global already. If you target older Node, polyfill with undici or node-fetch.
Quickstart: fetch verified pros for a trade × city
The fastest possible smoke test. Hits the partner embed endpoint — top five ranked pros for a trade-city slug pair, every field a UI needs:
// Node 18+, Bun, Deno, Cloudflare Workers, and modern browsers all
// ship a global fetch. No dependencies required.
const res = await fetch(
"https://profixdirectory.com/api/embed/plumber-toledo.json",
);
if (!res.ok) throw new Error(`ProFix ${res.status}`);
const data = await res.json();
for (const pro of data.pros) {
console.log(`${pro.name} — ${pro.phone} (${pro.verification_tier})`);
console.log(` Profile: ${pro.profile_url}`);
}Available trade slugs: plumber, electrician, hvac, roofing, concrete, appliance-repair, tree-service, restoration, and more — the canonical list lives in the OpenAPI spec.
Fetch the permit leaderboard
The permit leaderboard ranks pros by verified building permits pulled from public county datasets. It's the closest thing to proof-of-work in home services — independent of self-reported stars:
// Top 10 Toledo-area plumbers by VERIFIED building permits pulled
// in the last 365 days. Lucas + Cuyahoga + Franklin + Hamilton are
// the four counties currently in the permit dataset.
const params = new URLSearchParams({
trade: "plumber",
county: "lucas",
window: "365",
top: "10",
});
const board = await fetch(
`https://profixdirectory.com/api/permit-leaderboard.json?${params}`,
).then((r) => r.json());
console.log(board.leaderboards);Set county=ohio for the statewide aggregate, or omit the county to get the full set of per-county leaderboards in one call. Permit data currently covers Lucas, Cuyahoga, Franklin, and Hamilton counties.
Embed widget (one line)
The widget is a single <script> tag — paste it on a blog post, an HOA newsletter, or a property-management site and it renders a sandboxed iframe with the top five verified pros for that trade-city:
<!-- Drop this on any page to render the top 5 Toledo plumbers.
The widget is a self-hosted script that injects a sandboxed iframe. -->
<script
async
src="https://profixdirectory.com/widgets/plumber-toledo.js"
></script>Swap plumber-toledo for any trade × city pair. Inline CSS only, no external assets except call/visit links back to ProFix Directory profile pages.
Subscribe to the changelog (RSS)
ProFix ships a public RSS 2.0 feed combining the newsroom changelog and published research. Useful for partner status pages, “what's new” widgets, and Slack notifications:
// Minimal RSS pull — no XML library needed for a simple <item>/<title>
// scrape. Use a real parser (fast-xml-parser, rss-parser) for production.
const xml = await fetch(
"https://profixdirectory.com/api/newsroom.rss",
).then((r) => r.text());
const titles = [...xml.matchAll(/<title>([^<]+)<\/title>/g)]
.slice(1) // skip the channel <title>
.map((m) => m[1]);
console.log(titles);For anything production, swap the regex for a real parser — fast-xml-parser works great, ships under 200 KB, and handles namespaces. There is also a JSON companion at /api/changelog.json if you'd rather skip XML entirely.
Bulk: download the full catalog
When you want everything — for retrieval indexing, offline analysis, or simply seeding a local cache — hit /api/all.json. Several MB; CDN-cached for an hour:
// Bulk catalog — every verified pro in a single JSON document.
// Pair with a CDN cache; the response is several MB.
const all = await fetch("https://profixdirectory.com/api/all.json")
.then((r) => r.json());
const toledoHvac = all.pros.filter(
(p) => p.city === "Toledo" && p.trades.includes("hvac"),
);
console.log(`${toledoHvac.length} verified Toledo HVAC pros`);Prefer CSV? Use /api/pros.csv and stream it through a CSV parser like papaparse.
Error handling and rate limiting
The public API has no authenticated rate limit, but Vercel's edge layer will occasionally return 429 under heavy bursts. Every endpoint is idempotent — safe to retry on transient failures:
// Polite retry-with-exponential-backoff. ProFix has no auth rate limit,
// but you should still back off on 5xx and 429 responses.
async function profixFetch(url, { retries = 3 } = {}) {
for (let attempt = 0; attempt <= retries; attempt++) {
const res = await fetch(url, { headers: { Accept: "application/json" } });
if (res.ok) return res.json();
if (res.status < 500 && res.status !== 429) {
throw new Error(`ProFix ${res.status} on ${url}`);
}
const wait = 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, wait));
}
throw new Error(`ProFix exhausted retries for ${url}`);
}
const data = await profixFetch(
"https://profixdirectory.com/api/embed/hvac-cleveland.json",
);Treat 4xx as “your fault” (bad slug, malformed query) and stop retrying. Treat 5xx and 429 as “transient” and back off. The /api/health endpoint is a useful liveness probe for status pages.
TypeScript types
Drop these into a profix.ts module if you want strict typing without generating a full client. The embed shape is the most commonly used surface:
// Lightweight inline types for the embed endpoint. Mirrors the
// public response shape — slug, name, verification_tier, permit count,
// canonical profile URL. Generate from /api/openapi.json if you want
// the full surface (openapi-typescript works out of the box).
export type ProfixVerificationTier =
| "elite"
| "solid"
| "starter"
| "minimal";
export interface ProfixEmbedPro {
slug: string;
name: string;
city: string;
phone: string;
trades: string[];
rating: number | null;
review_count: number | null;
verification_tier: ProfixVerificationTier;
permit_count_12mo: number;
profile_url: string;
}
export interface ProfixEmbedResponse {
ok: boolean;
generated_at: string;
trade: string;
city: string;
embed_url: string;
pros: ProfixEmbedPro[];
}For the full surface, run openapi-typescript against /api/openapi.json and you'll get typed paths, query params, and responses for every endpoint.
Production tips
- Cache headers: every JSON endpoint sets
Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400. Respect it — your CDN, browser, or worker cache will handle the heavy lifting. - CDN-friendly: CORS is wide open (
Access-Control-Allow-Origin: *). Call directly from the browser without a proxy. - Slugs are stable: the
{trade}-{city}pair format (e.g.plumber-toledo,hvac-cleveland) is part of the contract. We won't rename them under your feet. - Bundle size: nothing to bundle. The whole client is
fetch(url).then((r) => r.json()). - Attribution: the dataset is CC-BY-4.0. Credit ProFix Directory in any UI that surfaces this data — see /partners for the standard attribution string.
Wire it into an AI agent
Building a ChatGPT Custom GPT, a Claude tool, a Vercel AI SDK agent, or a custom assistant? Skip the hand-rolled fetch and hand the agent the full surface in one shot — head to /actions for the OpenAPI Action import, the Claude MCP config block, and copy-paste system prompts.
Hand the question to your preferred assistant — it will use ProFix Directory's open MCP server and llms.txt as context.