# Custom fields

Workspaces add their own fields to records: "Gate code" on jobs, "Snow contract" on customers, "Shirt size" on contacts, "Lot size" on sites. A field is defined once per record type and every record of that type can hold a value.

Record types (`entity_type`):

| `entity_type` | Records | Values are on |
|---|---|---|
| `job` | Jobs | `GET /v1/jobs/{id}` → `custom_fields` |
| `account` | Customers (client accounts) | `GET /v1/client-accounts/{id}` → `custom_fields` |
| `contact` | Contacts | `GET /v1/contacts/{id}` → `custom_fields` |
| `location` | Sites (locations) | `GET /v1/locations/{id}` → `custom_fields` |

Values are stored under each field's `key` (a slug of its label, e.g. `gate_code`). `custom_fields` is an object of `{key: value}`; a field with no value is simply missing.

!!! note
    Partner keys that act for several workspaces must also send `firm_id` (query string, or JSON body on writes).

## The field object

| Field | Type | Description |
|---|---|---|
| `id` | integer | TruTec id. |
| `entity_type` | string | `job`, `account`, `contact` or `location`. |
| `key` | string | The key values are stored under. Never changes, even when the label is renamed in the app. |
| `label` | string | What people see. |
| `field_type` | string | `text`, `number`, `date`, `select`, `checkbox` or `url`. |
| `options` | array of strings | The choices of a `select` field. |
| `required` | boolean | The app asks for it when a record is created. |
| `filterable` | boolean | Shown as a filter on the jobs board or customer / contact lists. |
| `show_as_flag` | boolean | Jobs: shown on the job card. |
| `section` | string | Where the field shows on the record's create / edit screens (see [Field setup](#field-setup)). |
| `position` | integer | Order on the record page. |
| `archived_at`, `created_at`, `updated_at` | string or null | UTC with `Z`. |

## Values by type

| `field_type` | Send | Stored as |
|---|---|---|
| `text`, `url` | a string | the string (up to 500 characters) |
| `number` | a number or numeric string (`"1,250"` is fine) | a number |
| `date` | `"YYYY-MM-DD"` | the same string |
| `select` | one of `options`, any case | the option as spelled in `options` |
| `checkbox` | `true` / `false` (or `"yes"` / `"no"`) | a boolean |

`null` or `""` clears a field.

## Fields

### List custom fields

`GET /v1/custom-fields`

**Permission:** Read jobs

| Name | Type | Required | Description |
|---|---|---|---|
| `entity_type` | string | No | Only one record type. |
| `include_archived` | boolean | No | Include archived fields (their values stay on records). |

```bash
curl "https://trutec.ai/api/v1/custom-fields?entity_type=job" \
  -H "X-API-Key: sk_your_api_key_here"
```

```json
{
  "data": [
    {
      "id": 61,
      "entity_type": "job",
      "key": "gate_code",
      "label": "Gate code",
      "field_type": "text",
      "options": [],
      "required": false,
      "filterable": false,
      "show_as_flag": false,
      "section": "job_location",
      "position": 1,
      "archived_at": null,
      "created_at": "2026-10-12T14:00:00Z",
      "updated_at": "2026-10-12T14:00:00Z"
    }
  ]
}
```

### Create a custom field

`POST /v1/custom-fields`

**Permission:** Write jobs

Adds a field to every record of one type. If the type already has a field with the same label (any case), that field is returned instead with `200` and `"created": false`, and brought back if it was archived.

| Name | Type | Required | Description |
|---|---|---|---|
| `entity_type` | string | Yes | `job`, `account`, `contact` or `location`. |
| `label` | string | Yes | Max 120 characters. |
| `field_type` | string | No | Default `text`. |
| `options` | array of strings | For `select` | The choices. |
| `required` | boolean | No | Ask for it when a record is created in the app. |
| `filterable` | boolean | No | Jobs, customers and contacts only. |
| `show_as_flag` | boolean | No | Jobs only. |

```bash
curl -X POST "https://trutec.ai/api/v1/custom-fields" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"entity_type": "account", "label": "Service tier", "field_type": "select", "options": ["Gold", "Silver"]}'
```

Returns `201` with `{"data": {field}, "created": true}`.

**Errors:** `400` `entity_type must be one of: ...`, `field_type must be one of: ...`, `Give the field a name.`, `Add at least one dropdown option.`

To rename, reorder, archive or restore a field, see [Field setup](#field-setup).

## Values

### Set custom fields on a record

`PATCH /v1/jobs/{id}/custom-fields`

`PATCH /v1/client-accounts/{id}/custom-fields`

`PATCH /v1/contacts/{id}/custom-fields`

`PATCH /v1/locations/{id}/custom-fields`

**Permission:** Write jobs

Sets the fields you send and leaves the others alone. Every value is checked first; if any key is unknown or any value is invalid, nothing is saved. On a job, each change is written to the job's activity log as in the app.

| Name | Type | Required | Description |
|---|---|---|---|
| `custom_fields` | object | Yes | `{key: value}`. `null` clears a field. |

```bash
curl -X PATCH "https://trutec.ai/api/v1/jobs/41872/custom-fields" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"custom_fields": {"gate_code": "4411#", "snow_contract": true}}'
```

```json
{
  "data": {
    "entity_type": "job",
    "id": 41872,
    "custom_fields": {"gate_code": "4411#", "snow_contract": true},
    "changed": ["gate_code", "snow_contract"]
  }
}
```

**Errors:**

- `400` `Unknown custom fields: ...` (`code: unknown_custom_fields`, `fields` lists them)
- `400` `Lot size must be a number.`, `Choose one of the Service tier options.` and the like (`code: invalid_custom_field`, `field` names it)
- `404` for a record outside the workspace

Contacts also accept `custom_fields` on `POST /v1/contacts` and `PATCH /v1/contacts/{id}` (see [Contacts](contacts.md)).

## Field setup

Change a field the way **Settings › Custom fields** does. Only workspace owners change field setup, as in the app: a key created by anyone else gets `403`. Values already stored on records are never touched.

`section` values by record type:

| `entity_type` | `section` |
|---|---|
| `job` | `job_location`, `company_contact`, `job_details` |
| `account` | `company_profile`, `billing`, `company_notes` |
| `contact` | `contact_identity`, `communication`, `contact_notes` |
| `location` | `location_identity`, `property_context`, `location_notes` |

### Update a custom field

`PATCH /v1/custom-fields/{id}`

**Permission:** Write jobs (owner key)

Only the fields you send change.

| Name | Type | Description |
|---|---|---|
| `label` | string | New name, up to 120 characters. The `key` stays the same, so values keep working. |
| `required` | boolean | Ask for it when a record is created in the app. |
| `section` | string | Where it shows on the create / edit screens (table above). |
| `show_as_flag` | boolean | Job fields only. Show it on the job card. |
| `filterable` | boolean | Job fields only. A filter on the jobs board. |

```bash
curl -X PATCH "https://trutec.ai/api/v1/custom-fields/61" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"label": "Gate / lockbox code", "required": true}'
```

Returns `200` with `{"data": {field}}`.

**Errors:** `400` `A field named "..." already exists.`, `show_as_flag can only be changed on job fields`, `section must be one of: ...`; `403` when the key's creator is not a workspace owner.

### Move a custom field

`POST /v1/custom-fields/{id}/move`

**Permission:** Write jobs (owner key)

Send `{"direction": "up"}` or `{"direction": "down"}` to move the field one place among its type's active fields. `position` is renumbered from 1 for the whole type. Returns `200` with `{"data": {field}}`. An archived field returns `409` `archived`.

### Archive a custom field

`DELETE /v1/custom-fields/{id}`

**Permission:** Write jobs and Delete records (owner key)

Archives the field on every record of its type. Nothing is deleted: values stay stored and show read-only on the records that have them, and [restoring](#restore-a-custom-field) brings the field back. Returns `204`.

### Restore a custom field

`POST /v1/custom-fields/{id}/restore`

**Permission:** Write jobs (owner key)

Brings an archived field back, with the values it had. Returns `200` with `{"data": {field}}`.
