Full betaYou're using the new Handyman.com — rebuilt for contractors. Send feedback

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 with Access-Control-Allow-Origin: *. Call from any domain with fetch.
  • 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.

EndpointUse for
GET /api/public/services?limit=100&q=plumbFull project/service catalog (Popular Services grid)
GET /api/public/projects?limit=6Latest open homeowner projects. Each item includes url, slug, excerpt, photoUrl. Add ?since= or ?after_id= for incremental poll.
GET /api/public/questions?limit=8Community Q&A threads. Each item includes url, slug, excerpt. Same ?since= / ?after_id= poll params (question_id cursor).
GET /api/public/contractors?limit=6Featured paid contractors
GET /api/public/posts?limit=3Latest 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-pad

postMessage 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.

ProjectEnv 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_KEYEncrypted 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-appHANDYMAN_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

EndpointNotes
GET /api/public/servicesReturns { 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-domainServer-side. Body { domain, apiKey } ref_id.

v2 — SEO + server lead forward

EndpointNotes
POST /api/partner/leadsServer-side only. Bearer PARTNER_API_KEY (or WORKER_API_KEY). Returns redirect_url to pre-filled project post.
GET /api/public/cities?limit=500Returns { 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-logosSlug → 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=contractor

Register 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.