Custom Fields
Custom fields let you attach your own structured data to leads and projects in Houzz Pro. The most valuable thing you can store in one is an identifier from your own system — a CRM record ID, an ERP job number, an accounting reference — so you can later look the Houzz record up by it. This turns custom fields into a two-way key that keeps your system and Houzz Pro in sync without maintaining a separate mapping table.
This guide covers the whole lifecycle: defining a custom field, writing values to it, and — the part most integrations care about — looking records up by value (external-ID mapping).
How custom fields work
There are two resources:
- A custom field definition (
CustomField) is the field itself — anameand atype, scoped to one entity type. You create it once. - A custom field entry (
CustomFieldEntry) is a value for that definition, stored on a specific lead or project.
A definition is scoped with customableType, which selects which kind of
record the field lives on. The allowed values are:
customableType | Applies to |
|---|---|
Inquiry | Leads |
Project | Projects |
Inquiry is the custom-field scope name for leads — leads and inquiries are
the same object. Use Inquiry when your field belongs on leads and Project
when it belongs on projects.
A field's type is one of text, checkbox, link, date,
single_select, or duration. For external-ID mapping, use text — it stores
an arbitrary string, and values are matched as strings on lookup.
1. Define a custom field
First, check whether the field already exists. List definitions for an entity
type with GET /v1/customFields?customableType=...:
curl --request GET \
--url 'https://pro-api.houzz.com/api/v1/customFields?customableType=Project' \
--header 'Authorization: Bearer <access_token>'
{
"data": [
{
"id": 8801,
"name": "external_id",
"type": "text",
"description": "Record ID in our CRM"
}
],
"limit": 20,
"offset": 0,
"total": 1
}
If it does not exist yet, create it with POST /v1/customFields:
curl --request POST \
--url https://pro-api.houzz.com/api/v1/customFields \
--header 'Authorization: Bearer <access_token>' \
--header 'Content-Type: application/json' \
--data '{
"name": "external_id",
"type": "text",
"customableType": "Project",
"description": "Record ID in our CRM"
}'
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Field name — this is what you match on during lookup (e.g. external_id). |
type | string | yes | One of text, checkbox, link, date, single_select, duration. |
customableType | string | yes | Inquiry (leads) or Project. |
description | string | no | Human-readable description. |
options | array | no | Allowed values, for single_select fields. |
Response (201 Created) returns the new definition, including its integer
id — hold on to it, you will need it to write values:
{
"data": {
"id": 8801,
"name": "external_id",
"type": "text",
"description": "Record ID in our CRM"
}
}
Listing and creating definitions requires the read:leads and write:leads
scopes respectively — the same scopes cover custom fields for both entity types.
2. Set values on a record
Write values with a single call to PATCH /v1/customFieldEntries. You identify
the target record with entityId + entityType, and pass one or more entries,
each pairing a customFieldId (the definition's id from step 1) with a
string value:
curl --request PATCH \
--url https://pro-api.houzz.com/api/v1/customFieldEntries \
--header 'Authorization: Bearer <access_token>' \
--header 'Content-Type: application/json' \
--data '{
"entityId": 12345,
"entityType": "Project",
"customFieldEntries": [
{ "customFieldId": 8801, "value": "crm_00042" }
]
}'
| Field | Type | Required | Description |
|---|---|---|---|
entityId | integer | yes | ID of the lead or project to write to. |
entityType | string | yes | Inquiry (a lead) or Project. |
customFieldEntries | array | yes | Entries to set; each is { customFieldId, value }. |
To set a value on a lead, use the lead's ID and entityType: "Inquiry":
curl --request PATCH \
--url https://pro-api.houzz.com/api/v1/customFieldEntries \
--header 'Authorization: Bearer <access_token>' \
--header 'Content-Type: application/json' \
--data '{
"entityId": 55501,
"entityType": "Inquiry",
"customFieldEntries": [
{ "customFieldId": 8802, "value": "crm_00042" }
]
}'
Writing entries requires write:projects (for projects) or write:leads (for
leads).
Values on the record
Once set, values come back on the record itself. Both projects and leads include
a customFieldEntries array in their responses (GET /v1/projects/{id},
GET /v1/leads/{id}), so a single read gives you the record and all of its
custom values:
{
"data": {
"id": 12345,
"name": "Maple Street Remodel",
"customFieldEntries": [
{
"id": 990001,
"customFieldId": 8801,
"customableId": 12345,
"customableType": "Project",
"value": "crm_00042",
"customField": { "id": 8801, "name": "external_id", "type": "text" }
}
]
}
}
3. Look records up by value (external-ID mapping)
This is the payoff. Instead of storing "Houzz project 12345 = my record 42" in your own database, store your ID on the Houzz record and fetch the record by it whenever you need to.
One endpoint does the reverse lookup, taking the entityType to search, a
fieldName (the definition's name), and a value — all three required:
GET /v1/customFields/getEntityByCustomFieldValue?entityType=...&fieldName=...&value=...
entityType is one of Project, Lead, Contact, Vendor, DailyLog, or Task.
curl --request GET \
--url 'https://pro-api.houzz.com/api/v1/customFields/getEntityByCustomFieldValue?entityType=Project&fieldName=external_id&value=crm_00042' \
--header 'Authorization: Bearer <access_token>'
The response identifies what was matched and nests the record itself under
entity, shaped like the corresponding resource:
{
"data": {
"entityType": "Project",
"entityId": 12345,
"entity": {
"id": 12345,
"name": "Maple Street Remodel",
"status": { "id": 2, "name": "In Progress" },
"client": { "id": 987, "name": "Jordan Rivera" },
"customFieldEntries": [
{
"id": 990001,
"customFieldId": 8801,
"value": "crm_00042",
"customField": { "id": 8801, "name": "external_id", "type": "text" }
}
]
}
}
}
If no record has that value, the lookup returns 404 Not Found with the
standard error envelope — use that to decide whether to
create a new Houzz record or update an existing one.
Looking up requires the read scope for the entityType you ask for — a token
without it receives no data.
End-to-end recipe: sync your system_id to Houzz
Putting the pieces together — a one-time setup, then an idempotent sync you can run whenever a record changes in your system.
Setup (once per company): create the definition and remember its id.
# Create the external-ID field on projects.
curl --request POST \
--url https://pro-api.houzz.com/api/v1/customFields \
--header 'Authorization: Bearer <access_token>' \
--header 'Content-Type: application/json' \
--data '{ "name": "system_id", "type": "text", "customableType": "Project" }'
# → { "data": { "id": 8801, ... } }
Sync (per record): try to find the Houzz project by your ID; create it if it does not exist yet; then stamp your ID onto it.
# 1. Does a Houzz project already carry this system_id?
curl --silent --request GET \
--url 'https://pro-api.houzz.com/api/v1/customFields/getEntityByCustomFieldValue?entityType=Project&fieldName=system_id&value=sys_42' \
--header 'Authorization: Bearer <access_token>'
# → 200 with { data: { entityType, entityId, entity } }, or 404 if none.
# 2. On 404, create the project.
curl --request POST \
--url https://pro-api.houzz.com/api/v1/projects \
--header 'Authorization: Bearer <access_token>' \
--header 'Content-Type: application/json' \
--data '{ "name": "Maple Street Remodel", "clientId": 987 }'
# → { "data": { "id": 12345, ... } }
# 3. Stamp your system_id onto the project so future lookups find it.
curl --request PATCH \
--url https://pro-api.houzz.com/api/v1/customFieldEntries \
--header 'Authorization: Bearer <access_token>' \
--header 'Content-Type: application/json' \
--data '{
"entityId": 12345,
"entityType": "Project",
"customFieldEntries": [
{ "customFieldId": 8801, "value": "sys_42" }
]
}'
After the first sync, step 1 finds the project on every subsequent run, so the
same flow becomes a safe upsert: look up by system_id, and PATCH the record
you get back instead of creating a new one. Because the ID lives on the Houzz
record, this works even if your local mapping is ever lost.
The identical pattern works for leads — create the field with
customableType: "Inquiry", write values with entityType: "Inquiry", and read
back with the same lookup using entityType=Lead.
Next steps
- Core Concepts — how projects, leads, and their sub-resources relate.
- Webhooks — get notified when a record changes, so you can sync reactively instead of polling.
- API Reference — full schemas for
CustomField,CustomFieldEntry, projects, and leads.