Skip to main content

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);
note

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.