Authentication
Two credential kinds, and one rule: the browser never holds anything that can act for more than one contact.
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 ReferenceTwo credential kinds
Every request carries Authorization: Bearer <token>. The account is derived from the verified token, never from a header you send.
- Service key (
sk_…): held by your backend. Authorizes management calls, triggering events, and minting contact tokens. - Contact token: short-lived, scoped to a single contact. Minted by your backend and handed to the browser; the SDK uses it for the inbox, preferences, push tokens, and the stream.
Minting a contact token
Your backend exchanges a contact's external_id (your own user id) for a short-lived token plus Elaan's internal contact id.
POST /v1/contacts/tokens Authorization: Bearer sk_… { "external_id": "crm-12345" }Response
{
"token": "eyJ…",
"token_type": "bearer",
"expires_in": 900,
"contact_id": "01J…"
}
Return token and contact_id to the browser from your own endpoint; the SDK refreshes automatically when the token expires by calling your tokenProvider again.
The tokenProvider contract
The SDK never talks to /v1/contacts/tokens itself. It calls the function you supply, which is expected to hit your endpoint, where your own session tells you which user is asking:
async function tokenProvider() { const res = await fetch("/api/elaan-token", { credentials: "include" }); const { token, contact_id } = await res.json(); return { token, contactId: contact_id }; }
That indirection is the whole security model: the contact identity comes from your session on your server, never from anything the browser claims. The SDK calls this again on expiry, so keep it cheap and idempotent.
Rate limits
The trigger endpoint and the contact-token mint are rate limited per account, and rejections are a 429 with a Retry-After header. Limits are a token bucket rather than a fixed window, so a burst of triggers is absorbed while sustained abuse is still throttled.