Custom fields
Store arbitrary data on your contacts and use it for personalization, segmentation, and workflows
Every contact in Plunk has a data object alongside the built-in fields (email, subscribed, createdAt, updatedAt). You decide what goes in data — first names, plan tiers, signup dates, internal user IDs, anything you want to use later in templates, segments, or workflow conditions.
This guide covers how custom fields work end to end: setting them, the rules around types, using them, and cleaning them up.
Setting custom fields
Set custom fields any time you create or update a contact:
# Via /v1/track (auto-creates the contact)
curl -X POST https://next-api.useplunk.com/v1/track \
-H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{
"email": "ada@example.com",
"event": "signed_up",
"data": {
"firstName": "Ada",
"plan": "pro",
"signupDate": "2026-05-06T12:00:00Z",
"lifetimeValue": 240
}
}'
# Via PATCH /contacts/:id
curl -X PATCH https://next-api.useplunk.com/contacts/cnt_abc123 \
-H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{ "data": { "plan": "enterprise" } }'data is patched — only the keys you send change. Other keys keep their existing values.
Special values
| Value | Behaviour |
|---|---|
| Primitive | Stored. Available for templates, segments, workflows. |
null | Deletes the key from the contact. |
"" (empty) | Ignored — does not overwrite existing data. |
{ value, persistent: false } | Used for this send only; not stored on the contact. Good for one-shot codes (password resets, magic links). |
Type inference
Plunk infers a type for each field the first time it sees a non-null value in your project:
| Detected as | Examples |
|---|---|
| Number | 42, 3.14 |
| Boolean | true, false |
| Date | Strings matching YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS[.sss]Z |
| String | Anything else |
The inferred type powers the segment filter UI (showing date pickers for date fields, number inputs for numeric ones, etc.) and is sticky once set per project. If you mix types for a key, the original inference wins for the UI but Plunk still stores whatever value you send.
To use date-aware operators (within, olderThan) on a custom field, store the value as a full ISO 8601 string — "2026-05-06T12:00:00Z" works, "05/06/2026" doesn't.
Reserved keys
Some keys are managed by Plunk and silently filtered out of any data payload — sending them won't store them and won't return an error:
id, plunk_id, plunk_email, email, unsubscribeUrl, subscribeUrl, manageUrl
Plus these core contact fields, which exist outside data: email, subscribed, createdAt, updatedAt. To change email or subscribed, use the dedicated request fields, not data.
locale is a special soft-reserved key — it's stored under data like other fields, but Plunk reads it for localization.
Using custom fields
In templates
Reference any field by name as a Handlebars variable:
<p>Hi {{firstName ?? "there"}},</p>
<p>You've been on the {{plan}} plan since {{signupDate}}.</p>The ?? fallback syntax is Plunk-specific and lets you provide a default when the field is missing or null. Nested data (data.profile.tier) is accessible the same way: {{profile.tier}}.
In segments
Reference custom fields with the data. prefix in segment filters:
{ "field": "data.plan", "operator": "equals", "value": "pro" }
{ "field": "data.lifetimeValue", "operator": "greaterThan", "value": 100 }
{ "field": "data.signupDate", "operator": "within", "value": 30, "unit": "days" }See the Segments concept page for the full filter taxonomy.
In workflows
CONDITION steps inside workflows use the same data. notation. The UPDATE_CONTACT step lets you write to custom fields as a workflow progresses (e.g. tag { stage: "activated" } after the welcome email is opened).
Inspecting your custom fields
GET /contacts/fields returns every field — standard plus custom — that's been used in your project, with the inferred type and the share of contacts that have it set:
{
"fields": [
{ "field": "email", "type": "string", "isCustom": false, "coverage": 1.0 },
{ "field": "data.plan", "type": "string", "isCustom": true, "coverage": 0.62 },
{ "field": "data.signupDate", "type": "date", "isCustom": true, "coverage": 0.95 }
]
}Use this to audit your custom fields, find stale ones, and feed dropdowns in your own UI.
For a single field, GET /contacts/fields/:field/values returns the distinct values seen for that field — useful for showing a "Filter by plan" dropdown in your dashboard.
Cleaning up unused fields
Custom fields tend to accumulate. Two endpoints help:
GET /contacts/fields/:field/usage— lists every segment, campaign, and workflow that references the field. Run this before deleting to make sure you don't break anything.DELETE /contacts/fields/:field— removes the field from every contact in the project.
Deleting only affects contact data — segments and workflows that referenced the field continue to exist but their conditions on that field will stop matching anything. The usage endpoint helps you find and clean those up first.
Best practices
- Pick stable field names. Renaming a field means updating every segment, template, and workflow that references it.
- Use ISO 8601 for dates. Otherwise date-aware segment operators won't work.
- Don't put secrets in
data. Custom fields are visible in the dashboard and to anyone with API access. Use{ value, persistent: false }for one-shot codes that shouldn't persist. - Prefer flat keys for high-cardinality fields. Nested paths (
data.profile.tier) work but are slightly harder to discover in the segment UI.