Skip to main content

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 — a name and a type, 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:

customableTypeApplies to
InquiryLeads
ProjectProjects
note

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"
}'
FieldTypeRequiredDescription
namestringyesField name — this is what you match on during lookup (e.g. external_id).
typestringyesOne of text, checkbox, link, date, single_select, duration.
customableTypestringyesInquiry (leads) or Project.
descriptionstringnoHuman-readable description.
optionsarraynoAllowed 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" }
]
}'
FieldTypeRequiredDescription
entityIdintegeryesID of the lead or project to write to.
entityTypestringyesInquiry (a lead) or Project.
customFieldEntriesarrayyesEntries 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.