Skip to main content

Authentication

The Houzz Pro API uses the OAuth 2.0 Client Credentials grant for machine-to-machine (M2M) access. Your application authenticates as itself — not on behalf of an individual end user — by exchanging a client ID and client secret for a short-lived access token, then presents that token as a Bearer credential on every API request.

Use this flow when your backend integration calls the Houzz Pro API directly (server-to-server). It is not an interactive login flow, so there is no browser redirect, no consent screen, and no refresh token.

The flow at a glance​

client_id + client_secret + audience + scope
Your server ───────────────────────────────────────────▶ Houzz authorization server
(token endpoint)
access_token (Bearer, expires_in seconds)
Your server ◀───────────────────────────────────────────
Authorization: Bearer <access_token>
Your server ───────────────────────────────────────────▶ Houzz Pro API
  1. Your server sends its credentials to the Houzz token endpoint.
  2. The authorization server validates them and returns a signed access token carrying the scopes your account is allowed to use.
  3. Your server sends that token in the Authorization header on each API call.
  4. When the token expires, repeat step 1 to obtain a new one.

Credentials​

A partner integration is issued a service account, which represents your company to the API. Each service account has:

CredentialDescription
client_idPublic identifier for your integration. Safe to log; not a secret.
client_secretConfidential secret used to authenticate at the token endpoint. Treat it like a password — never expose it in client-side code, mobile apps, or public repositories.

The service account is scoped to a single Houzz Pro company. Any data you read or write through the API is automatically constrained to that company — you cannot access another company's data with your credentials.

Obtaining credentials​

Service accounts are provisioned by Houzz. There is currently no self-serve developer portal for creating credentials.

  • To request access, contact your Houzz representative to have a service account created for your company. You will receive a client_id and a client_secret, along with the set of scopes granted to your account.
  • The client_secret is shown once at creation time. Store it securely (for example, in a secrets manager). If it is lost or compromised, contact your Houzz representative to rotate it — rotation invalidates the previous secret.
  • Scopes granted to your service account are configured by Houzz. To add or remove scopes later, contact your Houzz representative.
TODO(product)

Add the help-article link for requesting API access.

Requesting an access token​

Exchange your credentials for an access token with a single POST to the token endpoint.

Endpoint

POST https://auth.houzz.com/oauth/token

Headers

Content-Type: application/x-www-form-urlencoded

Body parameters

ParameterRequiredValue
grant_typeyesclient_credentials
client_idyesYour service account's client ID.
client_secretyesYour service account's client secret.
audienceyesThe API you are requesting access to: https://pro-api.houzz.com.
scopeoptionalSpace-separated list of scopes you want on the token (see Scopes). If omitted, the token is issued with the scopes granted to your service account.

cURL

curl --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 write:projects'

Node (fetch)

async function getAccessToken() {
const res = await fetch('https://auth.houzz.com/oauth/token', {
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: 'read:projects write:projects',
}),
});

if (!res.ok) {
throw new Error(`Token request failed: ${res.status}`);
}

const { access_token, expires_in } = await res.json();
return { accessToken: access_token, expiresIn: expires_in };
}

The audience value tells the authorization server which API the token is for. It must match the API you intend to call, otherwise the resulting token will be rejected by the API.

Token response​

A successful request returns 200 OK with a JSON body:

{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 86400,
"scope": "read:projects write:projects"
}
FieldDescription
access_tokenThe signed token to send on API requests. Opaque to you — do not parse or depend on its internal structure.
token_typeAlways Bearer.
expires_inLifetime of the token in seconds from the time of issue. Currently 86400 (24 hours) — read the value rather than hardcoding it.
scopeThe scopes actually granted on this token.

Token lifetime and refresh​

Access tokens for the Houzz Pro API are currently issued with a 24-hour lifetime (expires_in: 86400). That figure is a setting on the Houzz authorization server, not part of the API contract — every token response carries its own lifetime in expires_in, so read it from the response and derive your refresh schedule from that.

  • Never hardcode a duration. The configured lifetime can be changed without a breaking API change. Code that assumes a fixed number of seconds will keep presenting expired tokens if it is ever shortened — and will mint tokens far more often than necessary if it is lengthened.
  • Cache and reuse a token until shortly before it expires rather than requesting one per API call. A common pattern is to fetch a new token when the current one is within ~60 seconds of expiry.
  • There are no refresh tokens. The Client Credentials grant does not issue them. When a token expires, request a new one with the same client_id / client_secret.
  • Keep a 401 retry as your safety net. On a 401, discard the cached token, request a fresh one, and retry the call once. This is what keeps your integration correct if a token expires in flight or the configured lifetime changes underneath you.
expires_in is the authority, not this page

The 24 hours above is what the API issues today, published so you can plan around it — not a guarantee. If it ever changes, expires_in changes with it and correctly-written clients keep working without a code change.

Rate limits are documented in the Errors guide.

Using the token​

Send the access token in the Authorization header on every Houzz Pro API request, using the Bearer scheme:

Authorization: Bearer <access_token>

Example

curl --request GET \
--url https://pro-api.houzz.com/api/v1/projects \
--header 'Authorization: Bearer eyJhbGciOi...'

API base URL

EnvironmentBase URL
Productionhttps://pro-api.houzz.com/api

The token's audience must match the API you call. A token minted with audience=https://pro-api.houzz.com is intended for the production API at https://pro-api.houzz.com/api.

Note that the two differ by the trailing /api: the audience is the identifier of the API you are requesting access to, while the base URL is where requests are sent. Send the audience without /api, and call the API with it.

Token lifecycle​

Access tokens are short-lived — the token response tells you exactly how long each one lives via expires_in (seconds from the time of issue). Treat the token as a reusable credential for that window, not something to mint per request.

  • Cache and reuse the token until it nears expiry. Requesting a fresh token on every API call adds a round-trip to the authorization server to each request and will quickly run you into token-endpoint throttling. Fetch once, hold it in memory, and reuse it for the life of the token.
  • Re-request when expired or about to expire. Track the issue time and expires_in, and fetch a new token a little before the deadline — a common pattern is to refresh ~60 seconds before expires_in so an in-flight request never rides an already-expired token.
  • No refresh tokens. The Client Credentials grant does not issue a refresh token. "Refreshing" simply means requesting a brand-new token with the same client_id / client_secret, exactly as in Requesting an access token.
  • Keep client_secret server-side. Load it from a secrets manager or environment variable at runtime. Never ship it in client-side code, a mobile app, or a public repository. If it leaks, rotate it (see Obtaining credentials).

Node — cache the token, reuse until near expiry

let cached = null; // { accessToken, expiresAt } — expiresAt is epoch ms

async function getToken() {
// Reuse the cached token until it is within ~60s of expiry.
if (cached && Date.now() < cached.expiresAt - 60_000) {
return cached.accessToken;
}

const res = await fetch('https://auth.houzz.com/oauth/token', {
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: 'read:projects write:projects',
}),
});

if (!res.ok) {
throw new Error(`Token request failed: ${res.status}`);
}

const { access_token, expires_in } = await res.json();
cached = { accessToken: access_token, expiresAt: Date.now() + expires_in * 1000 };
return cached.accessToken;
}

401 vs 403​

Authentication and authorization failures are distinguished by HTTP status:

StatusMeaningWhat to do
401The access token is missing, malformed, or expired — a problem with who you are.Request a new token and retry. Verify you are sending the Authorization: Bearer header and the correct audience.
403Either the token lacks a required scope, or the account's plan does not include API integrations access — a problem with what you're allowed to do.For a scope gap, request a token that includes the required scope. If the scope was never granted, or the plan lacks API access, contact your Houzz representative.

Both use the standard error envelope:

{
"error": {
"code": "FORBIDDEN",
"message": "You do not have permission to perform this action."
}
}

Re-authenticating with the same scopes will not resolve a 403 — you need the correct scope on the token, or a plan change. Both causes return the FORBIDDEN code, so use the message to tell them apart when triaging; see 403: scope vs. plan.

Scope checks use OR, not AND

When an operation accepts more than one scope, a token satisfying any one of them is enough — it does not need all of them. See Scopes.

Don't treat 503 as an auth failure

If a Houzz dependency needed to authorize your request is briefly unavailable, the API returns 503 SERVICE_UNAVAILABLE with Retry-After: 5 — not a 401 or 403. It is transient: retry rather than rotating credentials or disabling your integration.

See Errors for the full status-code reference.