Skip to main content

Custom Fields API

Extend the contact schema with typed fields specific to your business — lifetime_value, plan_tier, last_login_at, account_manager, etc. Custom fields are referenced from segment rules, flow conditions, message personalization, and the inbox UI. Base path: /api/v1/custom-fields Authentication: API key (X-API-Key) or session JWT.

Using the SDKs

The Python SDK doesn’t wrap custom fields — its client.request escape hatch defines a field and sets it on a contact (source-only; vendor it from packages/sdk-python):
Returns the typed ApiResponse envelope. See the SDK index at SDK quickstart.

Definitions

A field’s key and type are immutable: they are fixed at creation and cannot be changed by PATCH (changing either would orphan the values already stored on contacts). To switch a field’s type, delete it and create a new one.

Create a definition

POST /api/v1/custom-fields/ creates the definition and returns it with the server-assigned id, plus any options (required for select / multiselect) and validators you sent:

List definitions

GET /api/v1/custom-fields/ returns every definition in display order — display_order ascending, never-ordered fields trailing, then by key. A freshly provisioned tenant returns an empty list.

Update a definition

PATCH /api/v1/custom-fields/{id} renames the field or updates its description, options, default value, required/searchable flags, and validators. key and type stay frozen — pass null on a validator or the default value to clear it.

Reorder definitions

Pass the list’s definition ids top-to-bottom; the response returns the refreshed list so you can reconcile in one round-trip. Unknown ids are ignored, omitted ids keep their current position.
The response is the full definition list in the new order (same envelope as the list endpoint above).

Dependencies and explorer

Before you delete or repurpose a field, check what references it.

Dependency scan

GET /api/v1/custom-fields/{id}/dependencies returns the count of contacts holding a value plus the segments, campaigns, drip enrollments, and flows that reference the field — the same snapshot the 409 below carries on a guarded delete:
An unused field returns zero counts and empty arrays. The endpoint 404s only when the definition itself doesn’t exist.

Explorer payload

GET /api/v1/custom-fields/{id}/explorer returns the definition, the usage block (the same counts as the scan above, plus a total), and up to ten of the most recently written values — every sample is PII-redacted and tail-truncated to 120 characters:
Use it to sanity-check what people are actually storing under a key before you narrow its options or validators.

Deleting a field that is in use

DELETE is guarded so you don’t silently break the segments, campaigns, and flows that reference a field. If the field is still referenced by any segment, campaign, or flow — or has values stored on any contact — the request returns 409 Conflict with code CUSTOM_FIELD_IN_USE. The response details list every dependent so you can review or clean up first:
To delete anyway, pass ?force=true:
With force=true the delete proceeds: the field’s values are stripped from every contact, and each dependent is flagged so you can find it afterward. Dependent segments are set to status: "broken". Dependent campaigns keep running — an in-flight send is never interrupted — but are flagged with a broken_custom_fields marker in their metadata. Dependent flows are set to status: "broken" so the automation runner skips them. The reference to the deleted field is not stripped from the campaign or flow filter: the filter JSON is left intact and only flagged, so the stale field-leaf remains until you edit or rebuild the filter. A field that is not referenced anywhere deletes without needing force.

Values

Per-contact values are written via a single endpoint and read inline as part of the contact resource. The value is validated against the definition — a wrong type or a violate-the-validator value returns 422. A successful write echoes the contact id and key with ok: true, and the next contact GET reflects the new value because the write invalidates the contact-detail cache:
On the contact, values come back inline in the custom_fields object — keyed by the field key:
The full contact-readback walk-through, including filter + pagination, is on the Contacts page.

Example — define a lifetime-value field, then set it

See also