Contact fields
Define typed custom fields on your contacts so you can segment and personalise on them
Contact fields
A contact field is a typed definition for one key inside a contact's
attributes object. Registering city as an enum and date_of_birth as a
date is what turns free-form JSON into something you can segment on with real
operators — date ranges, age windows, list membership — and what stops jkt and
Jakarta becoming two different cities on import.
Every field key is also a merge tag: a field with key city can be referenced
in a campaign as {{ city }}.
All endpoints require an Authorization: Bearer YOUR_API_KEY header.
The field object
{
"id": "cf_abc123",
"key": "date_of_birth",
"label": "Date of birth",
"type": "date",
"options": null,
"archived_at": null,
"created_at": "2026-08-09T10:30:00Z",
"updated_at": "2026-08-09T10:30:00Z"
}| Field | Type | Description |
|---|---|---|
key | string | The attributes key and the merge-tag name. Lowercase snake_case, starts with a letter. Immutable. |
label | string | Human label shown in the dashboard. |
type | string | One of text, number, date, boolean, enum, list. Immutable. |
options | string[] | null | For enum and list: the allowed vocabulary. null means an open vocabulary. |
archived_at | string | null | Set when the field is archived. Archived fields keep their data but disappear from pickers and can no longer be segmented on. |
Types and what they accept
| Type | Stored as | Segment operators |
|---|---|---|
text | string | eq, neq, contains, starts_with, is_set, is_not_set |
number | number | eq, neq, gt, gte, lt, lte, between |
date | ISO-8601 "YYYY-MM-DD" string | before, after, between, in_last_days, age_between |
boolean | true / false | is_true, is_false |
enum | string, must be in options | eq, neq, in, not_in |
list | array of strings | contains_any, contains_all, not_contains |
Age is never stored. Register a date field for the birthday and use
age_between when segmenting — it is evaluated against today's date, so a
contact leaves the "25–35" segment on their birthday with no intervention from
you. See Segments.
List fields
GET /v1/contact-fieldsReturns live fields first, then archived ones, each alphabetical by key.
{ "fields": [ /* field objects */ ] }Create a field
POST /v1/contact-fields| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Lowercase snake_case, ≤ 63 characters. |
label | string | Yes | Human label. |
type | string | Yes | One of the six types above. |
options | string[] | No | Required vocabulary for enum / list; omit for an open vocabulary. |
curl -X POST https://api.envloped.com/v1/contact-fields \
-H "Authorization: Bearer en_abc123..." \
-H "Content-Type: application/json" \
-d '{
"key": "city",
"label": "City",
"type": "enum",
"options": ["Singapore", "Kuala Lumpur", "Jakarta", "Bangkok", "Manila"]
}'Creating a key that exists but is archived revives it rather than failing — otherwise an archived key would be burned forever.
Update a field
PATCH /v1/contact-fields/:idlabel and options can change. key and type cannot: changing a type would
reinterpret every value already stored under that key. To change a type, archive
the field and create a new one.
Removing a value from an enum's options does not rewrite contacts that
already hold it — it only stops new writes from using it.
Archive a field
DELETE /v1/contact-fields/:idThis archives, it never drops data. The values stay in
contacts.attributes and reappear if the key is re-created. Segments that
reference an archived field stop resolving and report unknown_field rather
than silently matching nobody.
How validation works on write
Once a field exists, every write to attributes is checked against it:
- Values are coerced to the field's type where unambiguous (
"42"→42for anumber,"true"→truefor aboolean). - A value that cannot be coerced is rejected.
- An
enumorlistvalue outside a declaredoptionsvocabulary is rejected. - An empty or whitespace-only value clears the attribute to
nullrather than storing""— which is what keepsis_setmeaningful. - An unknown key — one with no registered field — is rejected. A typo'd key is how a segment quietly stops matching, so it is an error rather than a silent create.
On PATCH /v1/contacts/:id an invalid attribute returns
422. On POST /v1/contacts/batch the offending row is
marked invalid and the rest of the batch still applies — one bad key must not
discard 999 good contacts.