Partner & site integrations
Developers
Embed Handyman.com widgets and forms on your own site. Use our public JSON endpoints for live content—no API key required. Forms can link out or load in an iframe.
Early access
This page is a living integration guide. Widget payloads and form endpoints will expand over time. Questions? Contact us or apply via the partner program.
Overview
- Widgets —
GET /api/public/*returns JSON withAccess-Control-Allow-Origin: *. Call from any domain withfetch. - Forms — Post a project or contact us by linking to Handyman.com or embedding our hosted pages in an iframe (recommended for account creation + Turnstile).
- Base URL —
https://www.handyman.com
Widgets (live JSON)
Fetch these endpoints from your site and render the response. Responses are cached briefly at the edge; poll every few minutes for a “live” feel.
| Endpoint | Use for |
|---|---|
| GET /api/public/services?limit=100&q=plumb | Full project/service catalog (Popular Services grid) |
| GET /api/public/projects?limit=6 | Latest open homeowner projects. Each item includes url, slug, excerpt, photoUrl. Add ?since= or ?after_id= for incremental poll. |
| GET /api/public/questions?limit=8 | Community Q&A threads. Each item includes url, slug, excerpt. Same ?since= / ?after_id= poll params (question_id cursor). |
| GET /api/public/contractors?limit=6 | Featured paid contractors |
| GET /api/public/posts?limit=3 | Latest blog articles |
Example — latest projects list
Vanilla JS; swap the container id and styling to match your site.
<!-- Container on your page -->
<div id="handyman-projects"></div>
<script>
fetch("https://www.handyman.com/api/public/projects?limit=6")
.then(function (res) { return res.json(); })
.then(function (data) {
var el = document.getElementById("handyman-projects");
if (!el || !data.projects) return;
el.innerHTML = data.projects.map(function (p) {
var loc = [p.city, p.budget].filter(Boolean).join(" · ");
var img = p.photoUrl
? '<img src="' + p.photoUrl + '" alt="" style="width:64px;height:64px;object-fit:cover;border-radius:8px;margin-right:8px" />'
: "";
return '<article style="display:flex;margin-bottom:12px">' + img +
'<div><a href="' + (p.url || "https://www.handyman.com/projects/" + p.project_id) + '">' +
'<strong>' + (p.title || p.projectType || "Project") + '</strong></a>' +
(p.excerpt ? '<p style="font-size:12px;color:#444;margin:4px 0 0">' + p.excerpt + '</p>' : '') +
(loc ? '<div style="font-size:12px;color:#666">' + loc + '</div>' : '') +
'</div></article>';
}).join("");
})
.catch(function () {
document.getElementById("handyman-projects").textContent = "Projects unavailable.";
});
</script>Example — live refresh (incremental poll)
Poll every 60s; prepend new items using the highest project_id or date_added from the prior response.
var lastProjectId = 0;
function pollProjects() {
var url = "https://www.handyman.com/api/public/projects?limit=6";
if (lastProjectId > 0) url += "&after_id=" + lastProjectId;
fetch(url)
.then(function (r) { return r.json(); })
.then(function (data) {
(data.projects || []).forEach(function (p) {
if (p.project_id > lastProjectId) lastProjectId = p.project_id;
// prepend p to your widget DOM
});
});
}
setInterval(pollProjects, 60000);Example — community questions
fetch("https://www.handyman.com/api/public/questions?limit=5&after_id=100")
.then(function (r) { return r.json(); })
.then(function (data) {
console.log(data.questions);
// Each item: question_id, slug, url, excerpt, question_text, category, answer_count, votes, date_posted
});Example — featured contractors
fetch("https://www.handyman.com/api/public/contractors?limit=4")
.then(function (r) { return r.json(); })
.then(function (data) {
console.log(data.contractors);
// Each item: ContractorId, Name, City, State, slug, url, photoUrl, profileHref, logo_url, paidUpgrade
});Example — RSS logo fallback by slug
Optional if logo_url is null on a card — merge by slug from feed enclosures.
Promise.all([
fetch("https://www.handyman.com/api/public/contractors?limit=6").then(function (r) { return r.json(); }),
fetch("https://www.handyman.com/api/public/contractors/rss-logos?limit=50").then(function (r) { return r.json(); }),
]).then(function (results) {
var contractors = results[0].contractors || [];
var bySlug = {};
(results[1].logos || []).forEach(function (row) {
bySlug[row.slug] = row.logo_url;
});
return contractors.map(function (c) {
return {
name: c.Name,
logo: c.photoUrl || c.logo_url || (c.slug && bySlug[c.slug]) || null,
href: c.url || c.profileHref,
};
});
});Embed theme (iframe styling)
Routes under /embed/* render without site nav and expose CSS variables for partner branding. Pass query params on the iframe src, or update live via postMessage.
Theme query params
Aliases: primary / color, borderRadius / rounded, fontFamily, dense.
https://www.handyman.com/embed/theme-preview?primaryColor=%232563eb&radius=12px&font=Inter%2C%20sans-serif&compact=1
// CSS vars on the embed shell:
// --embed-primary, --embed-radius, --embed-font, --embed-density, --embed-padpostMessage theme (parent page)
var iframe = document.getElementById("handyman-embed");
iframe.contentWindow.postMessage({
type: "handyman:embed-theme",
theme: {
primaryColor: "#2563eb",
radius: "12px",
font: "Inter, sans-serif",
compact: true
}
}, "*");Themed iframe — widget shell (future routes)
<iframe
src="https://www.handyman.com/embed/widgets/projects?limit=6&primaryColor=%23991b1b&radius=10px"
title="Latest projects"
width="100%"
height="420"
style="border:0;max-width:480px"
loading="lazy"
></iframe>Forms & embeds
Homeowner project posting includes free account creation (email or Google). The simplest integration is an iframe or button that opens our hosted flow—no backend on your side.
Post a project — iframe embed (recommended)
Pre-fill project details and affiliate ref with query params. User completes account + Google sign-in on Handyman.com.
<iframe
src="https://www.handyman.com/projects/post?project_type=air-conditioning&zipcode=33432&description=Need%20AC%20install&budget=1000-5000&timeline=within-1-month&city=Miami&state=FL&ref=4521&refType=contractor"
title="Post a home project on Handyman.com"
width="100%"
height="720"
style="border:0;border-radius:12px;max-width:640px"
loading="lazy"
></iframe>Post a project — link or button
<a href="https://www.handyman.com/projects/post?projectTypeId=12&zipcode=33432&ref=4521&refType=contractor"
target="_blank"
rel="noopener noreferrer">
Post your project free →
</a>Ask a question — link out
Community Q&A lives on Handyman.com; link users to post or browse.
<a href="https://www.handyman.com/questions/post">Ask the community</a>
<a href="https://www.handyman.com/questions">Browse discussions</a>Contact form — hosted page
Use our contact page in an iframe, or link to it. Programmatic POST to /api/contact may require same-origin unless CORS is enabled for your domain (ask us).
<iframe
src="https://www.handyman.com/contact"
title="Contact Handyman.com"
width="100%"
height="520"
style="border:0;border-radius:12px;max-width:480px"
loading="lazy"
></iframe>Partner API (VNOC / portfolio domains)
White-label handyman templates on partner domains. v1 + v2 endpoints below are live on production after deploy.
White-label handyman templates on partner domains. v1 + v2 endpoints below are live on production after deploy.
Where to set API keys
The same secret can be used for both keys, or use separate values. Every side must use the exact same string for each pair below.
| Project | Env var(s) | Used for |
|---|---|---|
| Handyman (Vercel — handyman2026) | WORKER_API_KEY PARTNER_API_KEY | Validates register-domain and partner/leads. Redeploy after changing. |
| Cloudflare Worker (service-vertical) | WORKER_API_KEY | Encrypted secret on the worker. Sent as apiKey in register-domain on first domain visit. Set via wrangler secret put WORKER_API_KEY or CF dashboard → Worker → Settings → Variables. |
| VNOC manage-app | HANDYMAN_API_KEY HANDYMAN_LEAD_URL | HANDYMAN_API_KEY = Handyman PARTNER_API_KEY. HANDYMAN_LEAD_URL = https://www.handyman.com/api/partner/leads |
v1 — minimum integration
| Endpoint | Notes |
|---|---|
| GET /api/public/services | Returns { services: [{ id, name, slug, icon_url }] }. Optional ?limit=100, ?q=plumb, ?top=1. |
| GET /projects/post?… | Hosted form with query pre-fill + ref / refType=contractor attribution. |
| POST /api/affiliate/register-domain | Server-side. Body { domain, apiKey } → ref_id. |
v2 — SEO + server lead forward
| Endpoint | Notes |
|---|---|
| POST /api/partner/leads | Server-side only. Bearer PARTNER_API_KEY (or WORKER_API_KEY). Returns redirect_url to pre-filled project post. |
| GET /api/public/cities?limit=500 | Returns { cities: [{ name, state, slug }] }. City pages: /contractor/find/city/miami-fl |
| GET /api/public/contractors?city=&state=&service= | Filter featured paid contractors. Each item includes logo_url and profileHref. ?featured=1 (default), ?service=air-conditioning. |
| GET /api/public/contractors/rss-logos | Slug → logo map (same URLs as /contractors/feed.xml enclosures). Use to enrich cards when logo_url is null. |
| GET /api/public/services/{slug} | Single service meta + URLs. Public page: /services/air-conditioning |
Services catalog
fetch("https://www.handyman.com/api/public/services?limit=100")
.then(function (r) { return r.json(); })
.then(function (data) {
console.log(data.services);
// [{ id: 12, name: "Air Conditioning", slug: "air-conditioning", icon_url: null, is_top: true }]
});Project post — accepted query params
All params are optional. Send users to /projects/post on Handyman.com (Google OAuth lives there).
https://www.handyman.com/projects/post
?project_type=air-conditioning // or projectTypeId=12 or type_id=12
&description=Need%20new%20AC%20unit
&budget=1000-5000 // or "$1,000 - $5,000"
&timeline=within-1-month // or "Within 1 month"
&city=Miami
&state=FL // or state_id=10
&zipcode=33432
&ref=4521 // ref_id from register-domain
&refType=contractorRegister domain affiliate (server-side)
curl -s -X POST "https://www.handyman.com/api/affiliate/register-domain" \
-H "Content-Type: application/json" \
-d '{"domain":"yourdomain.com","apiKey":"YOUR_WORKER_API_KEY"}'
// → { "ref_id": 4521, "slug": "yourdomain-com", "domain": "yourdomain.com" }v2 — forward lead (server-side)
VNOC saves the lead locally, then POSTs to Handyman. Response includes redirect_url for the user to finish on handyman.com.
curl -s -X POST "https://www.handyman.com/api/partner/leads" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_PARTNER_API_KEY" \
-d '{
"domain": "yourdomain.com",
"email": "[email protected]",
"name": "Jane Doe",
"phone": "555-0100",
"message": "Need AC install",
"project_type": "air-conditioning",
"city": "Miami",
"state": "FL",
"zip": "33432",
"ref": "4521"
}'
// → { "ok": true, "redirect_url": "https://www.handyman.com/projects/post?..." }v2 — cities + filtered contractors
fetch("https://www.handyman.com/api/public/cities?limit=500")
.then(function (r) { return r.json(); })
.then(function (data) { console.log(data.cities); });
fetch("https://www.handyman.com/api/public/contractors?city=Miami&state=FL&service=air-conditioning&limit=6")
.then(function (r) { return r.json(); })
.then(function (data) { console.log(data.contractors); });v2 — service detail meta
fetch("https://www.handyman.com/api/public/services/air-conditioning")
.then(function (r) { return r.json(); })
.then(function (data) {
console.log(data.service);
// { id, name, slug, description, post_project_url, contractors_url }
});Guidelines
- Do not expose homeowner emails, phone numbers, or street addresses from API data.
- Link project and question titles back to Handyman.com for the full experience.
- Cache widget responses reasonably; avoid hammering endpoints faster than once per minute.
- Attribute content: “Powered by Handyman.com” with a link is appreciated.