BitelioBitelio

Custom fields

Attach your own data to contacts and put it to work in templates, segment filters, and workflow logic

Alongside its built-in fields (email, subscribed, createdAt, updatedAt), every Bitelio contact carries a free-form data object. What lives there is entirely up to you: first names, plan tiers, signup dates, your own user IDs — anything you'll later want available in templates, segments, or workflow conditions.

This guide walks through the full lifecycle of custom fields: writing them, how typing works, where you can use them, and how to retire them.

Setting custom fields

You can write custom fields whenever a contact is created or updated:

# Via /v1/track (auto-creates the contact)
curl -X POST https://api.bitelio.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://api.bitelio.com/contacts/cnt_abc123 \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "data": { "plan": "enterprise" } }'

Updates to data are merges, not replacements — only the keys present in your payload are touched, and everything else stays as it was.

Special values

ValueBehaviour
PrimitiveStored. Available for templates, segments, workflows.
nullDeletes 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).

Changed August 2026

A non-persistent field is written as { value, persistent: false } and referenced as {{ field }} — the envelope is unwrapped for you. That was already true everywhere except workflow emails, SMS and webhooks, where until August 2026 the raw envelope reached the template and the only thing that rendered was {{ field.value }}.

Those three now behave like the rest of the platform. If you wrote {{ field.value }} in a workflow template as a workaround, change it to {{ field }} — the old form renders empty, and nothing warns you.

Type inference

The first time a field appears with a non-null value anywhere in your project, Bitelio assigns it a type:

Detected asExamples
Number42, 3.14
Booleantrue, false
DateStrings matching YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS[.sss]Z
StringAnything else

That type drives the segment builder's UI — date fields get date pickers, numeric fields get number inputs, and so on — and once established it sticks for the project. Should you later send a different type for the same key, the UI keeps honoring the original inference, though Bitelio will still store whatever value arrives.

Date-aware operators (within, olderThan) only apply to custom fields stored as full ISO 8601 strings: "2026-05-06T12:00:00Z" qualifies, "05/06/2026" does not.

Reserved keys

A handful of keys belong to Bitelio and are silently filtered out of every data payload. Sending them neither stores them nor raises an error:

id, bitelio_id, bitelio_email, email, unsubscribeUrl, subscribeUrl, manageUrl

The core contact fields — email, subscribed, createdAt, updatedAt — also live outside data. Changing email or subscribed requires the dedicated request fields rather than data.

One key sits in between: locale. It's stored inside data like any other custom field, but Bitelio also reads it to drive localization.

Using custom fields

In templates

Any field can be referenced by name as a Handlebars variable:

<p>Hi {{firstName ?? "there"}},</p>
<p>You've been on the {{plan}} plan since {{signupDate}}.</p>

The ?? fallback operator is a Bitelio extension for supplying a default whenever the field is missing or null. Nested data works too — a value stored at data.profile.tier is available as {{profile.tier}}.

In segments

In segment filters, prefix the field name with data.:

{ "field": "data.plan", "operator": "equals", "value": "pro" }
{ "field": "data.lifetimeValue", "operator": "greaterThan", "value": 100 }
{ "field": "data.signupDate", "operator": "within", "value": 30, "unit": "days" }

The complete operator taxonomy is documented on the Segments concept page.

In workflows

Workflow CONDITION steps take the same data. notation. And with the UPDATE_CONTACT step, a workflow can write custom fields as it runs — for instance, setting { stage: "activated" } once the welcome email has been opened.

Inspecting your custom fields

To see every field your project has ever used — built-in and custom alike — call GET /contacts/fields. Each entry reports the inferred type and what fraction of contacts carry a value:

{
  "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 }
  ]
}

This is handy for auditing your schema, spotting fields that have gone stale, or populating dropdowns in a UI of your own.

For any single field, GET /contacts/fields/:field/values lists the distinct values it has taken — a natural fit for something like a "Filter by plan" selector in your dashboard.

Cleaning up unused fields

Fields have a habit of piling up over time. Two endpoints make pruning safe:

  • GET /contacts/fields/:field/usage — reports every segment, campaign, and workflow that mentions the field. Check this before deleting so nothing breaks.
  • DELETE /contacts/fields/:field — strips the field from all contacts in the project.

Deletion touches contact data only. Segments and workflows that referenced the field aren't removed — their conditions on it will simply never match again. That's exactly why the usage endpoint exists: find those references first and tidy them up.

Best practices

  • Choose field names you won't rename. A rename ripples into every segment, template, and workflow that mentions the field.
  • Store dates as ISO 8601. Any other format locks you out of the date-aware segment operators.
  • Keep secrets out of data. Everything there is visible in the dashboard and to anyone holding API access. For one-shot codes that shouldn't stick around, use { value, persistent: false }.
  • Favor flat keys when a field has many values. Nested paths like data.profile.tier work fine, but they're a little harder to spot in the segment UI.