Examples & Recipes
Practical, end-to-end recipes for common integration tasks. Each recipe shows a curl
version and an equivalent Node.js (fetch) version. Replace <access_token> with a
token obtained as shown in the first recipe.
Get a token and list projects
Exchange your service-account credentials for an access token, then use it to list projects. This token flow is the foundation for every other recipe.
curl
# 1. Request an access token
TOKEN=$(curl --silent --request POST \
--url https://auth.houzz.com/oauth/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data grant_type=client_credentials \
--data client_id=YOUR_CLIENT_ID \
--data client_secret=YOUR_CLIENT_SECRET \
--data audience=https://pro-api.houzz.com \
--data 'scope=read:projects' \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')
# 2. List projects
curl --request GET \
--url 'https://pro-api.houzz.com/api/v1/projects?limit=20&offset=0' \
--header "Authorization: Bearer $TOKEN"
Node.js
const AUTH_URL = "https://auth.houzz.com/oauth/token";
const API_BASE = "https://pro-api.houzz.com/api";
async function getToken(scope) {
const res = await fetch(AUTH_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.HOUZZ_CLIENT_ID,
client_secret: process.env.HOUZZ_CLIENT_SECRET,
audience: "https://pro-api.houzz.com",
scope,
}),
});
if (!res.ok) throw new Error(`token request failed: ${res.status}`);
const { access_token } = await res.json();
return access_token;
}
async function listProjects() {
const token = await getToken("read:projects");
const res = await fetch(`${API_BASE}/v1/projects?limit=20&offset=0`, {
headers: { Authorization: `Bearer ${token}` },
});
const body = await res.json();
return body; // { data, limit, offset, total }
}
Representative response
{
"data": [
{
"id": 90811,
"name": "Maple St. Kitchen Remodel",
"status": { "id": 3, "name": "In Progress" },
"client": { "id": 4471, "name": "Jane Doe" },
"budget": 85000,
"archived": false,
"createdAt": "2026-03-01T15:22:10Z",
"updatedAt": "2026-04-18T09:04:55Z"
}
],
"limit": 20,
"offset": 0,
"total": 1
}
Create a project
Requires the write:projects scope. Only name is required; the rest are optional.
curl
curl --request POST \
--url https://pro-api.houzz.com/api/v1/projects \
--header "Authorization: Bearer $TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"name": "Oak Ave. Bathroom Renovation",
"clientId": 4471,
"budget": 42000,
"notes": "Master bath, walk-in shower."
}'
Node.js
async function createProject() {
const token = await getToken("write:projects");
const res = await fetch(`${API_BASE}/v1/projects`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Oak Ave. Bathroom Renovation",
clientId: 4471,
budget: 42000,
notes: "Master bath, walk-in shower.",
}),
});
if (!res.ok) throw new Error(`create failed: ${res.status}`);
const { data } = await res.json();
return data;
}
Representative response (201 Created)
{
"data": {
"id": 90812,
"name": "Oak Ave. Bathroom Renovation",
"client": { "id": 4471, "name": "Jane Doe" },
"budget": 42000,
"notes": "Master bath, walk-in shower.",
"archived": false,
"createdAt": "2026-04-30T18:10:00Z",
"updatedAt": "2026-04-30T18:10:00Z"
}
}
List leads with pagination
List endpoints accept limit (default 20, max 300) and offset, and return a
{ data, limit, offset, total } envelope. Page through results by advancing offset
until offset + data.length >= total.
curl
curl --request GET \
--url 'https://pro-api.houzz.com/api/v1/leads?limit=50&offset=0' \
--header "Authorization: Bearer $TOKEN"
Node.js
async function listAllLeads() {
const token = await getToken("read:leads");
const limit = 50;
let offset = 0;
const all = [];
while (true) {
const res = await fetch(
`${API_BASE}/v1/leads?limit=${limit}&offset=${offset}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
const { data, total } = await res.json();
all.push(...data);
offset += data.length;
if (offset >= total || data.length === 0) break;
}
return all;
}
Representative response
{
"data": [
{
"id": 55021,
"name": "Kitchen inquiry — Riverside",
"budget": 60000,
"status": { "value": "new", "displayName": "New" },
"client": { "id": 4490, "name": "Sam Rivera", "email": "sam@example.com" }
}
],
"limit": 50,
"offset": 0,
"total": 1
}
Create a webhook subscription and verify a delivery
Register an endpoint to receive project events, then verify the signature on each incoming delivery. See the Webhooks guide for the full trigger catalog and signature details.
Create the subscription
curl --request POST \
--url https://pro-api.houzz.com/api/v1/webhookSubscriptions \
--header "Authorization: Bearer $TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"url": "https://example.com/hooks/houzz",
"description": "Project events",
"triggers": [
{ "resourceName": "project", "eventType": "create" },
{ "resourceName": "project", "eventType": "update" }
]
}'
The 201 response contains a one-time secret (HMAC-SHA256 signing key). Store it
securely — it is only returned here.
{
"data": {
"id": 42,
"url": "https://example.com/hooks/houzz",
"status": "active",
"triggers": [
{ "id": 1, "resourceName": "project", "eventType": "create", "isActive": true },
{ "id": 2, "resourceName": "project", "eventType": "update", "isActive": true }
],
"secret": "whsec_9f2b1c..."
}
}
Receive and verify a delivery
import express from "express";
import crypto from "node:crypto";
const app = express();
const SECRET = process.env.HOUZZ_WEBHOOK_SECRET;
// Capture the RAW body so the HMAC is computed over the exact bytes received.
app.post(
"/hooks/houzz",
express.raw({ type: "application/json" }),
(req, res) => {
const rawBody = req.body; // Buffer
const timestamp = req.get("X-Webhook-Timestamp");
const received = req.get("X-Webhook-Signature"); // "sha256=<hex>"
// Sign "{timestamp}.{rawBody}" — see the Webhooks guide's Signature section.
const expected =
"sha256=" +
crypto
.createHmac("sha256", SECRET)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const valid =
received &&
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.status(401).send("invalid signature");
// Acknowledge fast, then process asynchronously.
res.status(200).send("ok");
const event = JSON.parse(rawBody.toString("utf8"));
void event; // enqueue for background work
}
);
app.listen(3000);
Houzz Pro sends two headers on every delivery: X-Webhook-Signature (sha256=<hex>) and
X-Webhook-Timestamp (unix seconds). The signature is an HMAC-SHA256 of {timestamp}.{rawBody},
keyed with the subscription secret, computed over the raw request body. The
Webhooks guide's Signature section is the
source of truth for the signing contract and payload envelope.