# Custom Fields Custom fields extend contacts with user-defined attributes. The `type` controls UI rendering and validation. For `dropdown`, `options` must be provided. ## List custom fields `GET /api/custom-fields` Returns all custom fields for the authenticated user's location, ordered by `sort_order` then alphabetically. ### Example request ```bash curl --request GET \ --get "https://klozzo.com/api/custom-fields" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" ``` ### Response `200` ```json { "data": [ { "id": null, "key": null, "label": null, "name": "quidem", "type": "text", "write_policy": "fill_if_empty", "options": null, "default_value": null, "is_required": false, "sort_order": 6, "created_at": null, "updated_at": null }, { "id": null, "key": null, "label": null, "name": "autem", "type": "date", "write_policy": "fill_if_empty", "options": null, "default_value": null, "is_required": false, "sort_order": 0, "created_at": null, "updated_at": null } ] } ``` ## Create a custom field `POST /api/custom-fields` Adds a property every contact in this location can carry. Create the field **before** you start sending its values: `POST /api/leads` keeps a value whose key does not exist yet but does not create the field, and `PUT /api/contacts/{id}/custom-field-values` rejects it outright. The `key` is what the API uses and it is **immutable** — renaming the field later changes what people see, never what your code sends. Choose it deliberately, or let it be derived from the name and read it back from the response. `write_policy` is the field's own rule for what happens when a new value arrives for one that already has one. It is the reason a lead capture can enrich a record without ever destroying what a seller typed by hand. ### Body parameters - `name` (string, required) — Display name (max 255). - `key` (string) — Stable, immutable identifier used by the API (lowercase a-z, 0-9, _). Derived from `name` if omitted. - `type` (string, required) — One of `text`, `number`, `date`, `dropdown`, `checkbox`, `url`, `phone`, `email`. - `write_policy` (string) — What happens when a new value arrives for this field. One of `fill_if_empty` (default — only fills a blank, never overwrites), `last_write_wins` (newest value always wins), `immutable` (written once, then locked). - `options` (string[]) — Required when `type=dropdown`; the available choices. - `default_value` (string) — Default applied to new contacts. - `is_required` (boolean) — Whether this field must be filled. - `sort_order` (integer) — Display order in forms (low values first). ### Example request ```bash curl --request POST \ "https://klozzo.com/api/custom-fields" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --data "{ \"name\": \"Lifecycle stage\", \"key\": \"lifecycle_stage\", \"type\": \"dropdown\", \"write_policy\": \"fill_if_empty\", \"options\": [ \"architecto\" ], \"default_value\": \"trial\", \"is_required\": false, \"sort_order\": 0 }" ``` ### Response `200` ```json { "data": { "id": null, "key": null, "label": null, "name": "eius", "type": "phone", "write_policy": "fill_if_empty", "options": null, "default_value": null, "is_required": false, "sort_order": 10, "created_at": null, "updated_at": null } } ``` ## Fetch a custom field `GET /api/custom-fields/{uuid}` Returns one custom field by its UUID: its `key`, its type, its options if it is a dropdown, and its `write_policy`. Read it when a value you sent did not land the way you expected — nine times out of ten the answer is in `write_policy`, not in your payload. ### Path parameters - `uuid` (integer, required) — Custom field ID. ### Example request ```bash curl --request GET \ --get "https://klozzo.com/api/custom-fields/9" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" ``` ### Response `200` ```json { "data": { "id": null, "key": null, "label": null, "name": "nostrum", "type": "number", "write_policy": "fill_if_empty", "options": null, "default_value": null, "is_required": false, "sort_order": 6, "created_at": null, "updated_at": null } } ``` ### Response `404` — Does not exist, or belongs to another location ```json { "message": "No query results for model." } ``` ## Update a custom field `PUT /api/custom-fields/{uuid}` Updates a custom field definition by its UUID. Renaming or changing `type` does not migrate existing stored values; callers are responsible for downstream cleanup. The `key` identifier is immutable. ### Path parameters - `uuid` (integer, required) — Custom field ID. ### Body parameters - `name` (string) — Display name. - `label` (string) — Must not be greater than 255 characters. - `type` (string) — One of the supported types (see create). - `write_policy` (string) One of: `immutable`, `fill_if_empty`, `last_write_wins`. - `options` (string[]) — Required when `type=dropdown`. - `default_value` (string) — Default applied to new contacts. - `is_required` (boolean) — Whether a contact form refuses to save without this field. Does not apply retroactively to contacts that already exist. - `sort_order` (integer) — Where the field sits in forms; low values first. ### Example request ```bash curl --request PUT \ "https://klozzo.com/api/custom-fields/9" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --data "{ \"name\": \"Lifecycle stage\", \"label\": \"n\", \"type\": \"architecto\", \"write_policy\": \"last_write_wins\", \"options\": [ \"architecto\" ], \"default_value\": \"architecto\", \"is_required\": false, \"sort_order\": 0 }" ``` ### Response `200` ```json { "data": { "id": null, "key": null, "label": null, "name": "et", "type": "url", "write_policy": "fill_if_empty", "options": null, "default_value": null, "is_required": false, "sort_order": 3, "created_at": null, "updated_at": null } } ``` ### Response `422` — A type that does not exist ```json { "message": "Type no es una opción válida.", "errors": { "type": [ "Type no es una opción válida." ] } } ``` ### Response `422` — A dropdown with no options ```json { "message": "Falta options cuando type es dropdown.", "errors": { "options": [ "Falta options cuando type es dropdown." ] } } ``` ### Response `422` — A key already used in this location ```json { "message": "El valor de key ya está en uso.", "errors": { "key": [ "El valor de key ya está en uso." ] } } ``` ## Delete a custom field `DELETE /api/custom-fields/{uuid}` Deletes a custom field definition by its UUID, so the field stops being offered anywhere in the CRM. Stored values on existing contacts are not removed automatically (orphaned values remain queryable by ID). ### Path parameters - `uuid` (integer, required) — Custom field ID. ### Example request ```bash curl --request DELETE \ "https://klozzo.com/api/custom-fields/9" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" ``` ### Response `200` ```json { "message": "Custom field deleted." } ```