JavaScript
Install and use the Lunogram JavaScript SDK for web browsers and Node.js
The Lunogram JavaScript SDK provides user identification, event tracking, organization management, and scheduling for both browser and Node.js environments.
- npm:
@lunogram/js-sdk - Source: github.com/lunogram/js-sdk
Installation
npm install @lunogram/js-sdkOr with yarn/pnpm:
yarn add @lunogram/js-sdk
# or
pnpm add @lunogram/js-sdkScript tag (CDN)
For quick prototyping or simple websites:
<script src="https://unpkg.com/@lunogram/js-sdk/lib/esm/index.js"></script>This exposes a global Lunogram object on window.
Getting started
import { Lunogram } from "@lunogram/js-sdk";
const lunogram = new Lunogram("pk_your_public_api_key");If you are self-hosting, pass the URL endpoint as the second argument:
const lunogram = new Lunogram(
"pk_your_public_api_key",
"https://your-instance.com/api",
);In the browser, the SDK automatically generates an anonymous identifier
(source: "anonymous") and includes it with every request. When you upsert a
user with an explicit identifier, the anonymous profile is merged with the
identified user. The anonymous ID persists for the lifetime of the Lunogram
instance.
Users
Upsert a user
Create or update a user profile. If any of the provided identifiers match an existing user, their attributes are updated.
await lunogram.user.upsert({
identifier: [{ externalId: "user_123" }],
email: "user@example.com",
phone: "+1234567890",
timezone: "America/Chicago",
locale: "en",
data: {
firstName: "John",
lastName: "Doe",
plan: "premium",
},
});You can attach multiple identifiers from different sources:
await lunogram.user.upsert({
identifier: [
{ externalId: "user_123" },
{ externalId: "ph_abc", source: "posthog" },
],
email: "user@example.com",
});| Field | Type | Required | Description |
|---|---|---|---|
identifier | array | Yes | Array of identity objects (see below) |
email | string | No | User's email address |
phone | string | No | Phone in E.164 format |
timezone | string | No | IANA timezone (e.g. America/Chicago) |
locale | string | No | Locale code (e.g. en, es-MX) |
data | object | No | Custom user attributes |
Each entry in the identifier array:
| Field | Type | Required | Description |
|---|---|---|---|
externalId | string | Yes | The identifier value |
source | string | No | Namespace for the identifier. Defaults to "default" |
metadata | object | No | Optional key-value metadata associated with this identifier |
Delete a user
await lunogram.user.delete({
identifier: [{ externalId: "user_123" }],
});Events
Record user actions that can trigger journeys and update list membership.
await lunogram.user.events.post([
{
name: "order.completed",
identifier: [{ externalId: "user_123" }],
data: {
orderId: "ord_456",
total: 99.99,
currency: "USD",
},
},
]);You can send multiple events in a single call. In the browser, identifier is auto-injected and can be omitted:
await lunogram.user.events.post([
{ name: "page.viewed", data: { path: "/pricing" } },
{ name: "cta.clicked", data: { buttonId: "signup" } },
]);| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Event name (use dot-notation, e.g. order.completed) |
identifier | array | No | User identifier array (auto-injected in browser). Mutually exclusive with match. |
match | object | No | JSONB containment filter to match users by their data attributes. When set, the event is delivered to every user whose data contains the given key/value pairs. Mutually exclusive with identifier. |
data | object | Yes | Event properties |
Broadcasting with match
Instead of targeting a single user by identifier, you can use match to deliver an event to every user whose data column contains the specified key/value pairs. The filter uses PostgreSQL's @> (containment) operator under the hood.
// Send an event to all users on the "enterprise" plan
await lunogram.user.events.post([
{
name: "plan.feature.released",
match: { plan: "enterprise" },
data: {
feature: "advanced-analytics",
releaseDate: "2025-07-01",
},
},
]);match and identifier are mutually exclusive — you must provide one or the other, never both. When match is used, Lunogram resolves all matching users server-side and delivers a separate event to each one.
Matched events are de-duplicated automatically. If a message is redelivered internally, each user still receives exactly one event.
Organizations
Group users into organizations with shared attributes and events.
Upsert an organization
await lunogram.organization.upsert({
identifier: [{ externalId: "org_123" }],
name: "Acme Inc",
data: {
plan: "enterprise",
industry: "saas",
},
});Add or remove users
// Add a user to an organization
await lunogram.organization.addUser({
organization: { identifier: [{ externalId: "org_123" }] },
user: { identifier: [{ externalId: "user_456" }] },
});
// Remove a user from an organization
await lunogram.organization.removeUser({
organization: { identifier: [{ externalId: "org_123" }] },
user: { identifier: [{ externalId: "user_456" }] },
});Organization events
await lunogram.organization.events.post([
{
identifier: [{ externalId: "org_123" }],
name: "subscription.upgraded",
data: { from: "starter", to: "enterprise" },
},
]);Just like user events, you can use match instead of identifier to broadcast an event to every organization whose data contains the given key/value pairs:
// Send an event to all organizations on the "enterprise" plan
await lunogram.organization.events.post([
{
name: "compliance.update",
match: { plan: "enterprise" },
data: { policy: "gdpr-v2", effectiveDate: "2025-08-01" },
},
]);For organization events, exactly one of match or identifier is required.
Delete an organization
await lunogram.organization.delete({
identifier: [{ externalId: "org_123" }],
});Schedules
Create recurring or one-time schedules that emit events through the standard event pipeline. See Schedules for how these integrate with journeys.
User schedules
// Create a recurring weekly schedule
await lunogram.user.schedule.upsert({
name: "usage.report",
identifier: [{ externalId: "user_123" }],
scheduledAt: "2025-04-01T09:00:00Z",
interval: "7 days",
});
// Delete a schedule
await lunogram.user.schedule.delete({
name: "usage.report",
identifier: [{ externalId: "user_123" }],
});Organization schedules
await lunogram.organization.schedule.upsert({
name: "billing.reminder",
identifier: [{ externalId: "org_123" }],
scheduledAt: "2025-04-01T09:00:00Z",
interval: "1 month",
});| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Schedule name — unique per user/organization (emits as scheduled.<name>) |
identifier | array | No | User or organization identifier array |
scheduledAt | string | No | ISO 8601 timestamp for first execution |
startAt | string | No | Earliest time the schedule should begin |
interval | string | No | Recurrence interval (e.g. 7 days, 1 month) |
data | object | No | Custom data included with each emitted event |
Server-side usage
For Node.js or other server environments, use the Client class directly. Unlike Lunogram (which is browser-oriented and auto-manages anonymous IDs), Client is stateless and requires you to pass identifiers on each call.
import { Client } from "@lunogram/js-sdk";
const client = new Client({
apiKey: "pk_your_api_key",
urlEndpoint: "https://your-instance.com/api",
});
await client.user.upsert({
identifier: [{ externalId: "user_123" }],
email: "user@example.com",
});
await client.user.events.post([
{
name: "invoice.paid",
identifier: [{ externalId: "user_123" }],
data: { amount: 49.99 },
},
]);The API surface is identical to Lunogram. The only difference is that Client does not auto-inject the anonymous identifier.
Error handling
All methods are async and return promises. The SDK provides typed error classes:
import { Lunogram, UnauthorizedError, ValidationError } from "@lunogram/js-sdk";
const lunogram = new Lunogram("pk_your_api_key");
try {
await lunogram.user.upsert({
identifier: [{ externalId: "user_123" }],
});
} catch (error) {
if (error instanceof UnauthorizedError) {
// Invalid or expired API key
} else if (error instanceof ValidationError) {
// Invalid request data
}
}| Error class | Status code | Description |
|---|---|---|
NetworkError | n/a | Request failed to reach the server |
UnauthorizedError | 401 | Invalid API key |
ForbiddenError | 403 | Forbidden |
NotFoundError | 404 | Resource not found |
ValidationError | 400 | Invalid request data |
RequestError | other | Any other server error |
TypeScript
The SDK ships with full TypeScript definitions. All request and response types are exported:
import type {
UpsertUserRequest,
UserEvent,
ExternalID,
UserIdentifier,
} from "@lunogram/js-sdk";