Contacts & preferences
Address recipients by your own ids, keep the attributes your templates need on the contact, and let people choose what reaches them.
All documentation
Getting started
Overview Quickstart SDK packagesFrontend
React Vue Svelte React Native Web Components Headless & theming Real-timeBackend API
Authentication Templates Trigger & status Contacts & preferencesMore
Self-hosting ReferenceSync contacts
Create or update the contacts you'll notify. Address them by your own external_id; you never need to store Elaan's internal ids.
POST /v1/contacts Authorization: Bearer sk_… { "external_id": "crm-12345", "emails": ["ada@example.com"], "attributes": { "first_name": "Ada" } }
Every management route also accepts ext:<external_id> in place of the internal id (for example GET /v1/contacts/ext:crm-12345), so your backend operates entirely in your own identifiers.
Attributes reach your templates
Whatever you put in attributes becomes available to every template rendered for that contact, so first_name set once at sync time is usable in every email and inbox message without being passed on each trigger. Attributes are flat strings; anything you would otherwise repeat in every variables payload belongs here instead.
Preferences
The <Preferences /> component renders and edits the full matrix automatically. To build your own, read the matrix (every notification type × channel, with the effective on/off and whether the contact overrode it):
GET /v1/contacts/{contact_id}/preferences // contact or service token
PUT /v1/contacts/{contact_id}/preferences // { notification_type_key, channel, enabled }
In the SDK this is the usePreferences() hook / elaan.preferences store: setPreference(typeKey, channel, enabled) and clearPreference(typeKey, channel).
The matrix only ever offers a channel that type can actually reach, which means a channel with a template behind it. Offering a recipient a switch that silently does nothing is worse than not offering it, so a type with no push template shows no push column.
Preferred language
A contact can carry a preferred language, set at creation or updated on its own route:
PUT /v1/contacts/{contact_id}/language // { "language": "fr" }
Templates can have per-language variants, and the contact's language selects between them, falling back to the language-less default when there is no variant. In the SDK this is setLanguage(). It is deliberately a separate route rather than part of the whole-contact update, so a partial-shaped update cannot silently clear it.