Contact properties
Declare the custom fields your contacts carry — their type, allowed values and whether they are required — so a wrong value is rejected instead of stored.
What a definition does#
A contact's properties object accepts any key, defined or not. A definition adds rules to one key. Once plan is defined as a select with options free, pro and scale, every write through the Contacts API, an import or a signup form checks the value, converts it to the declared type, and rejects it with 422 when it does not fit. Keys with no definition are still stored exactly as sent.
Definitions are also what the dashboard uses to render typed inputs on the contact page and what signup forms match submitted fields against — a form field is saved only when its name equals a defined property's name.
Endpoints live under https://api.poststack.dev/contact-properties. Reading needs the contacts:read scope, writing needs contacts:manage. In the dashboard, manage them under Contacts → Properties.
Property types#
null, a missing value or "" always passes (it clears the value) unless the property is required and the contact is being created.
| Type | Accepts | Stored as | Rejected with |
|---|---|---|---|
text | Any string; numbers and booleans are converted to strings. | string | expected a string |
number | A finite number, or a string that parses as one ("42", "4.5"). | number | expected a number |
boolean | true, false, "true", "false". Signup forms also accept a checkbox's "on". | boolean | expected true or false |
date | YYYY-MM-DD, or any ISO 8601 date-time. | YYYY-MM-DD string — a date-time is cut to its UTC date. | expected an ISO date string |
select | A value equal to one of options (numbers and booleans are compared as strings). | string | must be one of free, pro, scale |
The full error reads Invalid value for property "seats": expected a number. A missing required property on create reads Missing required property "plan".
Endpoints#
List properties#
Returns every property definition in your team, sorted by name. Not paginated.
https://api.poststack.dev/contact-propertiesconst { properties } = await poststack.contactProperties.list();curl https://api.poststack.dev/contact-properties \
-H "Authorization: Bearer sk_live_..."{
"properties": [
{
"id": 7,
"teamId": 42,
"name": "plan",
"label": "Subscription plan",
"type": "select",
"options": ["free", "pro", "scale"],
"required": false,
"createdAt": "2026-09-20T10:00:00.000Z"
},
{
"id": 8,
"teamId": 42,
"name": "seats",
"label": "Seats",
"type": "number",
"options": null,
"required": false,
"createdAt": "2026-09-20T10:05:00.000Z"
}
]
}Response fields
propertiesobject[]- The definitions.
properties[].idinteger- The property id — the
:idthe update and delete endpoints take. properties[].teamIdinteger- The team that owns the definition.
properties[].namestring- The key in a contact's
propertiesobject. properties[].labelstring- Display name in the dashboard.
properties[].type"text" | "number" | "boolean" | "date" | "select"- How values are checked and stored.
properties[].optionsstring[] | null- Allowed values of a select property; null when none were given.
properties[].requiredboolean- Whether contact creation requires a value.
properties[].createdAtstring (ISO 8601)- When the definition was created.
Status codes
| Status | Meaning |
|---|---|
| 200 | The definitions. |
| 401 | Missing, invalid or revoked API key. |
| 403 | insufficient_scope: the key does not hold contacts:read (a sending-access key never does), or the team is suspended. |
Create a property#
Defines a property. Existing contacts are not checked or changed: values they already hold under that key stay as they are until the contact is next written.
https://api.poststack.dev/contact-properties// The SDK returns the property itself, not the { property } envelope.
const property = await poststack.contactProperties.create({
name: 'plan',
label: 'Subscription plan',
type: 'select',
options: ['free', 'pro', 'scale'],
});curl -X POST https://api.poststack.dev/contact-properties \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "plan",
"label": "Subscription plan",
"type": "select",
"options": ["free", "pro", "scale"]
}'{
"name": "plan",
"label": "Subscription plan",
"type": "select",
"options": ["free", "pro", "scale"]
}HTTP/1.1 201 Created
{
"property": {
"id": 7,
"teamId": 42,
"name": "plan",
"label": "Subscription plan",
"type": "select",
"options": ["free", "pro", "scale"],
"required": false,
"createdAt": "2026-09-20T10:00:00.000Z"
}
}Body parameters
namestringrequired- 1–100 characters, unique in your team. This is the key in
propertiesand the name segment rules use (properties.plan). It cannot be changed later. labelstringrequired- 1–200 characters. Shown in the dashboard.
type"text" | "number" | "boolean" | "date" | "select"defaulttext- Cannot be changed later.
optionsstring[]- The allowed values of a
selectproperty. A select with no options rejects every non-empty value. Ignored by the other types. requiredbooleandefaultfalse- Require a non-empty value when a contact is created.
Response fields
propertyobject- The new definition.
property.idinteger- The property id — the
:idthe update and delete endpoints take. property.teamIdinteger- The team that owns the definition.
property.namestring- The key in a contact's
propertiesobject. property.labelstring- Display name in the dashboard.
property.type"text" | "number" | "boolean" | "date" | "select"- How values are checked and stored.
property.optionsstring[] | null- Allowed values of a select property; null when none were given.
property.requiredboolean- Whether contact creation requires a value.
property.createdAtstring (ISO 8601)- When the definition was created.
Status codes
| Status | Meaning |
|---|---|
| 201 | Created. The body is { property }. |
| 400 | Body failed validation, e.g. an unknown type. |
| 401 | Missing, invalid or revoked API key. |
| 403 | insufficient_scope: the key does not hold contacts:manage (a sending-access key never does), or the team is suspended. |
| 409 | Property with this name already exists. |
| 429 | More than 30 requests in a minute from this key to this method and path (each property id is counted separately). |
Update a property#
Changes the label, options or required flag. name and type are fixed once created; to change them, create a new property and move the values over.
https://api.poststack.dev/contact-properties/:idconst property = await poststack.contactProperties.update(7, {
label: 'Current plan',
options: ['free', 'pro', 'scale', 'enterprise'],
});curl -X PATCH https://api.poststack.dev/contact-properties/7 \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "options": ["free", "pro", "scale", "enterprise"] }'{
"label": "Current plan",
"options": ["free", "pro", "scale", "enterprise"]
}{
"property": {
"id": 7,
"teamId": 42,
"name": "plan",
"label": "Current plan",
"type": "select",
"options": ["free", "pro", "scale", "enterprise"],
"required": false,
"createdAt": "2026-09-20T10:00:00.000Z"
}
}Path parameters
idintegerrequired- The property's numeric
id(property definitions have no prefixed public id).
Body parameters
labelstring- 1–200 characters.
optionsstring[]- Replaces the whole options list. Contacts holding a removed option keep it, but any later write that sends their
propertiesfails with422until the value is changed — check first with Count option usage. requiredboolean- Turn the create-time requirement on or off.
Response fields
propertyobject- The definition after the update.
Status codes
| Status | Meaning |
|---|---|
| 200 | Updated (or unchanged, when the body was empty). |
| 400 | Body or :id failed validation — :id must be a positive integer. |
| 401 | Missing, invalid or revoked API key. |
| 403 | insufficient_scope: the key does not hold contacts:manage (a sending-access key never does), or the team is suspended. |
| 404 | Property not found. |
| 429 | More than 30 requests in a minute from this key to this method and path (each property id is counted separately). |
Count option usage#
For a select property, how many contacts hold each stored value — including values no longer in options. Use it before removing an option. Returns an empty object for any other type.
https://api.poststack.dev/contact-properties/:id/option-usagecurl https://api.poststack.dev/contact-properties/7/option-usage \
-H "Authorization: Bearer sk_live_..."{
"usage": { "free": 802, "pro": 311, "legacy": 4 }
}Path parameters
idintegerrequired- The property's numeric
id(property definitions have no prefixed public id).
Response fields
usageobject- Stored value → number of contacts holding it. Contacts without a value are not counted.
Status codes
| Status | Meaning |
|---|---|
| 200 | The counts. |
| 401 | Missing, invalid or revoked API key. |
| 403 | insufficient_scope: the key does not hold contacts:read (a sending-access key never does), or the team is suspended. |
| 404 | Property not found. |
Delete a property#
Deletes the definition only. Values stored under that key on contacts are kept and become free-form: no longer type-checked, still usable in segment rules.
https://api.poststack.dev/contact-properties/:idawait poststack.contactProperties.delete(7);curl -X DELETE https://api.poststack.dev/contact-properties/7 \
-H "Authorization: Bearer sk_live_..."{
"success": true
}Path parameters
idintegerrequired- The property's numeric
id(property definitions have no prefixed public id).
Response fields
successboolean- Always true.
Status codes
| Status | Meaning |
|---|---|
| 200 | Deleted. |
| 401 | Missing, invalid or revoked API key. |
| 403 | insufficient_scope: the key does not hold contacts:manage (a sending-access key never does), or the team is suspended. |
| 404 | Property not found. |
| 429 | More than 30 requests in a minute from this key to this method and path (each property id is counted separately). |
Where property values are used#
| Feature | How to refer to a property |
|---|---|
| Segment rules | properties.plan as the condition field. A bare plan is not a valid field. |
| Workflow condition steps | The bare name, plan. |
| Workflow webhook steps | The whole properties object is sent in contact.properties. |
| Signup forms | An input whose name is plan. |
Broadcast emails fill in only {{first_name}}, {{last_name}} and {{email}} from the contact. Workflow emails also fill in every text, number or boolean property by name ({{plan}}).
Troubleshooting#
422 Invalid value for property … must be one of …
The value is not in the select's options. Values are matched exactly, including case. Add the value with PATCH /contact-properties/:id or send one of the listed options.
Updating some contacts' properties suddenly fails with 422
An option was removed while those contacts still hold it. Because PATCH /contacts/:id sends the whole properties object, the stale value is re-checked and rejected. Find the affected values with option-usage, then restore the option or replace the value in the same request.
Creating contacts fails with Missing required property
A required property applies to every create path except signup forms, imports included. Send the value, or set required to false.