Documentation

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

Two 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.
Never ship the service key to the browser. The frontend only ever holds a contact token, and only for its own contact. A contact token cannot read another contact's inbox, and cannot reach any management route at all.

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.

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