Documentation

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 packages
Frontend
React Vue Svelte React Native Web Components Headless & theming Real-time
Backend API
Authentication Templates Trigger & status Contacts & preferences
More
Self-hosting Reference

Sync 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.

Create
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.

Typically your backend syncs the basic contact (identity, email, attributes), while the frontend adds device-facing bits like push tokens and preference overrides through the SDK.

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.

Transactional types are not listed. Password resets, email verification and the like are marked as not opt-out-able, and are omitted from the contact-facing matrix entirely. That does not mean "always sent": the type's own channel defaults still decide what fires. Only the recipient's veto is removed.

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.