Lunogram

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.

Installation

npm install @lunogram/js-sdk

Or with yarn/pnpm:

yarn add @lunogram/js-sdk
# or
pnpm add @lunogram/js-sdk

Script 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",
});
FieldTypeRequiredDescription
identifierarrayYesArray of identity objects (see below)
emailstringNoUser's email address
phonestringNoPhone in E.164 format
timezonestringNoIANA timezone (e.g. America/Chicago)
localestringNoLocale code (e.g. en, es-MX)
dataobjectNoCustom user attributes

Each entry in the identifier array:

FieldTypeRequiredDescription
externalIdstringYesThe identifier value
sourcestringNoNamespace for the identifier. Defaults to "default"
metadataobjectNoOptional 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" } },
]);
FieldTypeRequiredDescription
namestringYesEvent name (use dot-notation, e.g. order.completed)
identifierarrayNoUser identifier array (auto-injected in browser). Mutually exclusive with match.
matchobjectNoJSONB 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.
dataobjectYesEvent 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",
});
FieldTypeRequiredDescription
namestringYesSchedule name — unique per user/organization (emits as scheduled.<name>)
identifierarrayNoUser or organization identifier array
scheduledAtstringNoISO 8601 timestamp for first execution
startAtstringNoEarliest time the schedule should begin
intervalstringNoRecurrence interval (e.g. 7 days, 1 month)
dataobjectNoCustom 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 classStatus codeDescription
NetworkErrorn/aRequest failed to reach the server
UnauthorizedError401Invalid API key
ForbiddenError403Forbidden
NotFoundError404Resource not found
ValidationError400Invalid request data
RequestErrorotherAny 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";

On this page