@dravra/js
v0.1.1
Published
Dravra JavaScript SDK — identify users, track events, and send emails
Downloads
32
Readme
@dravra/js
Official JavaScript / TypeScript SDK for Dravra — the email automation platform for SaaS teams. Identify users, track lifecycle events, and send transactional emails directly from your application.
What is Dravra?
Dravra is an email automation platform built for SaaS applications. It lets you:
- Identify users — sync your users into Dravra as contacts with traits (plan, company, lifecycle stage)
- Track events — record lifecycle moments (
trial.started,subscription.upgraded, etc.) that trigger automated email sequences - Send emails — deliver transactional emails using templates you build inside Dravra's visual editor
- Automate flows — build drip campaigns, onboarding sequences, and behavioral triggers in the Dravra dashboard
The @dravra/js SDK gives you a typed, retrying HTTP client that talks to the Dravra REST API. It is designed to work in any Node.js, Edge Runtime, or Deno environment — including Next.js Server Actions, Route Handlers, Supabase Edge Functions, and plain Express servers.
Table of contents
- Installation
- Quick start
- Configuration
- API reference
- Next.js helpers (
@dravra/js/next) - Integration guides
- Standard event names
- TypeScript types
- Error handling & retries
- License
Installation
npm install @dravra/js
# or
yarn add @dravra/js
# or
pnpm add @dravra/jsRequires Node.js 18+ or any runtime with a native fetch implementation (Edge Runtime, Deno, Bun).
Quick start
import { Dravra } from "@dravra/js";
const dravra = new Dravra({ apiKey: "drv_live_..." });
// 1. Identify a user (creates or updates a contact)
await dravra.identify("user_123", "[email protected]", {
firstName: "Alice",
lastName: "Smith",
plan: "pro",
});
// 2. Track a lifecycle event
await dravra.track("trial.started", {
userId: "user_123",
email: "[email protected]",
properties: { trialDays: 14 },
});
// 3. Send a transactional email
await dravra.send(
{ email: "[email protected]" },
"tmpl_welcome_email",
{ variables: { firstName: "Alice" } }
);Configuration
Create a Dravra instance with your API key from the Dravra dashboard (Settings → API Keys):
const dravra = new Dravra({
apiKey: "drv_live_...", // required — must start with drv_live_
baseUrl: "https://api.dravra.ai", // optional — override for self-hosted instances
timeout: 10000, // optional — per-request timeout in ms (default: 10000)
});| Option | Type | Default | Description |
|---|---|---|---|
| apiKey | string | required | Dravra API key. Must start with drv_live_. |
| baseUrl | string | https://api.dravra.ai | Override the API base URL (useful for testing or self-hosted). |
| timeout | number | 10000 | Per-request timeout in milliseconds. |
Store your API key as an environment variable — never hard-code it or commit it:
# .env.local
DRAVRA_API_KEY=drv_live_...API reference
identify(userId, email, traits?)
Creates or updates a contact in Dravra. If a contact with the same email or externalUserId already exists, it is updated (upsert). Returns the full Contact object.
Signature
identify(
userId: string, // Your system's user ID — stored as externalUserId
email: string, // User's email address
traits?: Record<string, unknown> // Any additional contact properties
): Promise<Contact>Example
const contact = await dravra.identify("user_abc123", "[email protected]", {
firstName: "Alice",
lastName: "Smith",
plan: "pro",
company: "Acme Inc",
trialStartedAt: "2026-04-01T00:00:00Z",
});
console.log(contact.id); // Dravra contact ID e.g. "cnt_..."
console.log(contact.lifecycleStage); // "lead" | "subscriber" | "active" | etc.
console.log(contact.createdAt); // ISO 8601 timestampReturn type: Contact
interface Contact {
id: string;
tenantId: string;
email: string;
externalUserId?: string | null;
firstName?: string | null;
lastName?: string | null;
traits?: Record<string, unknown>;
lifecycleStage?: string;
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
}track(event, options?)
Tracks a named event and associates it with a contact. At least one of options.userId or options.email must be provided so the API can link the event to a contact.
Events drive Dravra automations — when you track trial.started, any automation that listens for that event will fire.
Signature
track(
event: StandardEventName, // Event name in dot notation, e.g. "trial.started"
options?: TrackOptions
): Promise<TrackResult>
interface TrackOptions {
userId?: string; // Your system's user ID
email?: string; // User's email address
properties?: Record<string, unknown>; // Arbitrary event properties
occurredAt?: string; // ISO 8601 — defaults to now
idempotencyKey?: string; // Deduplicate retries (auto-generated if omitted)
}Example
const result = await dravra.track("trial.started", {
userId: "user_abc123",
email: "[email protected]",
properties: { trialDays: 14, plan: "pro" },
occurredAt: "2026-04-01T09:00:00Z",
});
console.log(result.eventId); // Internal event ID
console.log(result.status); // "accepted" | "duplicate"Idempotency
Every track() call automatically generates a UUID as an idempotency key and sends it in the Idempotency-Key request header. If the API receives two requests with the same key, only the first is recorded and the second returns { status: "duplicate" } — safe to retry without double-counting.
Supply your own key to guarantee idempotency across process restarts:
await dravra.track("subscription.upgraded", {
userId: "user_abc123",
idempotencyKey: `upgrade-${userId}-${invoiceId}`,
});Return type: TrackResult
interface TrackResult {
eventId: string;
status: "accepted" | "duplicate";
}send(to, templateId, options?)
Sends a transactional email to a contact using a template published in the Dravra dashboard. The send is queued and processed asynchronously — the method returns as soon as the request is accepted.
Signature
send(
to: { contactId: string } | { email: string },
templateId: string,
options?: SendOptions
): Promise<SendResult>
interface SendOptions {
variables?: Record<string, unknown>; // Template variable substitutions
messageClass?: "transactional" | "marketing"; // Default: "transactional"
}Examples
// Send by email address — the contact is looked up or created automatically
const result = await dravra.send(
{ email: "[email protected]" },
"tmpl_welcome_email",
{
variables: { firstName: "Alice", trialDays: 14 },
messageClass: "transactional",
}
);
// Send by Dravra contact ID (more explicit)
await dravra.send(
{ contactId: "cnt_abc123" },
"tmpl_trial_ending",
{ variables: { daysLeft: 3 } }
);
console.log(result.sendId); // ID you can use to check delivery status
console.log(result.status); // Always "queued" on successmessageClass
| Value | When to use |
|---|---|
| "transactional" | Password resets, receipts, account notifications. Sent regardless of marketing opt-out. |
| "marketing" | Newsletters, promotions, drip campaigns. Respects unsubscribe preferences. |
Return type: SendResult
interface SendResult {
sendId: string;
status: "queued";
}Next.js helpers (@dravra/js/next)
The @dravra/js/next subpath export provides two helpers optimized for Next.js applications. They are imported separately so tree-shaking excludes them from non-Next.js builds.
import { withDravra, clerkAdapter } from "@dravra/js/next";withDravra(options)
A factory function that creates a Dravra instance. Functionally identical to new Dravra(options), but encourages the singleton pattern and reads more naturally in a lib/ file.
// lib/dravra.ts
import { withDravra } from "@dravra/js/next";
export const dravra = withDravra({
apiKey: process.env.DRAVRA_API_KEY!,
});Import this singleton in any Server Action or Route Handler — no need to re-instantiate the client on every request.
clerkAdapter(dravra, event)
Handles a Clerk webhook event and maps it to the appropriate Dravra calls. Supports user.created, user.updated, and user.deleted. For unsupported event types, it returns { handled: false } without throwing.
What it does per event type:
| Clerk event | Dravra calls |
|---|---|
| user.created | identify() + track("user.signed_up") |
| user.updated | identify() + track("user.updated") |
| user.deleted | track("user.deleted") |
| anything else | no-op, returns { handled: false } |
Signature
clerkAdapter(
dravra: Dravra,
event: ClerkWebhookEvent
): Promise<ClerkAdapterResult>
interface ClerkWebhookEvent {
type: string;
data: ClerkUserPayload;
}
interface ClerkAdapterResult {
handled: boolean;
eventType: string;
contactId?: string; // set for user.created and user.updated
eventId?: string; // Dravra event ID
}Example
// app/api/webhooks/clerk/route.ts
import { Webhook } from "svix";
import { withDravra, clerkAdapter } from "@dravra/js/next";
const dravra = withDravra({ apiKey: process.env.DRAVRA_API_KEY! });
export async function POST(req: Request) {
const payload = await req.text();
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
// Verify Svix signature — throws if invalid
const event = wh.verify(payload, Object.fromEntries(req.headers)) as any;
const result = await clerkAdapter(dravra, event);
return Response.json({ received: true, ...result });
}Note:
clerkAdapterdoes not verify the Svix signature — always callwh.verify()before passing the event to the adapter.
Integration guides
Next.js
1. Add your API key to .env.local:
DRAVRA_API_KEY=drv_live_...2. Create a singleton client:
// lib/dravra.ts
import { withDravra } from "@dravra/js/next";
export const dravra = withDravra({
apiKey: process.env.DRAVRA_API_KEY!,
});3. Use in a Route Handler:
// app/api/waitlist/route.ts
import { dravra } from "@/lib/dravra";
export async function POST(req: Request) {
const { email, firstName } = await req.json();
await dravra.identify("", email, { firstName, source: "waitlist" });
await dravra.track("user.signed_up", { email });
return Response.json({ ok: true });
}4. Use in a Server Action:
// app/actions/onboarding.ts
"use server";
import { auth } from "@clerk/nextjs/server";
import { dravra } from "@/lib/dravra";
export async function completeOnboarding(email: string) {
const { userId } = await auth();
if (!userId) throw new Error("Unauthorized");
await dravra.identify(userId, email);
await dravra.track("trial.started", { userId, email });
}Clerk webhooks
Automatically sync new Clerk users into Dravra when they sign up.
Prerequisites: Install svix (npm install svix) and configure a webhook endpoint in the Clerk dashboard pointing to /api/webhooks/clerk.
Using clerkAdapter (recommended):
// app/api/webhooks/clerk/route.ts
import { Webhook } from "svix";
import { withDravra, clerkAdapter } from "@dravra/js/next";
const dravra = withDravra({ apiKey: process.env.DRAVRA_API_KEY! });
export async function POST(req: Request) {
const payload = await req.text();
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
const event = wh.verify(payload, Object.fromEntries(req.headers)) as any;
const result = await clerkAdapter(dravra, event);
return Response.json({ received: true, ...result });
}Manual approach (full control):
// app/api/webhooks/clerk/route.ts
import { Webhook } from "svix";
import { dravra } from "@/lib/dravra";
export async function POST(req: Request) {
const payload = await req.text();
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
const event = wh.verify(payload, Object.fromEntries(req.headers)) as {
type: string;
data: {
id: string;
email_addresses: { email_address: string }[];
first_name?: string;
last_name?: string;
};
};
const email = event.data.email_addresses[0]?.email_address ?? "";
if (event.type === "user.created") {
await dravra.identify(event.data.id, email, {
firstName: event.data.first_name,
lastName: event.data.last_name,
});
await dravra.track("user.signed_up", { userId: event.data.id, email });
}
if (event.type === "user.deleted") {
await dravra.track("user.deleted", { userId: event.data.id });
}
return Response.json({ received: true });
}Supabase Edge Functions
Identify a new user automatically when they are inserted into your profiles table via a Supabase database webhook.
// supabase/functions/on-user-created/index.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { Dravra } from "https://esm.sh/@dravra/js";
const dravra = new Dravra({ apiKey: Deno.env.get("DRAVRA_API_KEY")! });
serve(async (req) => {
const { record } = await req.json() as {
record: {
id: string;
email: string;
raw_user_meta_data?: Record<string, string>;
};
};
const fullName = record.raw_user_meta_data?.full_name ?? "";
const [firstName, ...rest] = fullName.split(" ");
await dravra.identify(record.id, record.email, {
firstName,
lastName: rest.join(" ") || undefined,
});
await dravra.track("user.signed_up", {
userId: record.id,
email: record.email,
});
return new Response(JSON.stringify({ ok: true }), {
headers: { "Content-Type": "application/json" },
});
});Set the secret in your Supabase project:
supabase secrets set DRAVRA_API_KEY=drv_live_...Standard event names
StandardEventName is an open-ended union — it provides autocomplete for built-in names while still accepting any string. You will never get a type error for a custom event name.
import type { StandardEventName } from "@dravra/js";| Event name | When to fire |
|---|---|
| user.signed_up | User creates an account |
| user.verified_email | User verifies their email address |
| user.first_login | User logs in for the first time |
| user.updated | User profile is updated |
| user.deleted | User account is deleted |
| trial.started | Free trial begins |
| trial.ending_soon | Trial is 3–7 days from expiry |
| subscription.upgraded | User upgrades their plan |
| subscription.cancelled | User cancels their subscription |
Custom events — use any dot-notation name that makes sense for your domain:
await dravra.track("order.completed", { userId, properties: { orderId, total } });
await dravra.track("feature.used", { userId, properties: { feature: "exports" } });
await dravra.track("invoice.payment_failed", { userId, email });TypeScript types
All types are exported from the main entry point:
import type {
DravraOptions,
Contact,
TrackOptions,
TrackResult,
SendOptions,
SendResult,
StandardEventName,
} from "@dravra/js";Types from @dravra/js/next:
import type {
ClerkUserPayload,
ClerkWebhookEvent,
ClerkAdapterResult,
} from "@dravra/js/next";Full type reference:
interface DravraOptions {
apiKey: string;
baseUrl?: string;
timeout?: number;
}
interface Contact {
id: string;
tenantId: string;
email: string;
externalUserId?: string | null;
firstName?: string | null;
lastName?: string | null;
traits?: Record<string, unknown>;
lifecycleStage?: string;
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
}
interface TrackOptions {
userId?: string;
email?: string;
properties?: Record<string, unknown>;
occurredAt?: string; // ISO 8601, defaults to now
idempotencyKey?: string; // auto-generated UUID if omitted
}
interface TrackResult {
eventId: string;
status: "accepted" | "duplicate";
}
interface SendOptions {
variables?: Record<string, unknown>;
messageClass?: "transactional" | "marketing"; // default: "transactional"
}
interface SendResult {
sendId: string;
status: "queued";
}
// Open union — built-in names are autocompleted, custom strings are accepted
type StandardEventName =
| "user.signed_up"
| "user.verified_email"
| "trial.started"
| "trial.ending_soon"
| "subscription.upgraded"
| "subscription.cancelled"
| "user.first_login"
| "user.updated"
| "user.deleted"
| (string & {});Error handling & retries
Throwing behavior
All three methods (identify, track, send) throw an Error with a descriptive message if the request fails. Wrap calls in try/catch in production:
try {
await dravra.track("trial.started", { userId, email });
} catch (err) {
if (err instanceof Error) {
console.error("Dravra error:", err.message);
// Examples:
// "Dravra: apiKey is required"
// "Dravra API error: At least one of userId or email must be provided"
// "Dravra API error: Template not found"
// "Dravra API error: HTTP 429"
}
}Automatic retries
The SDK wraps every HTTP request with fetchWithRetry, which automatically retries on transient failures:
| Condition | Retried? |
|---|---|
| Network error / DNS failure | Yes |
| 5xx server error | Yes |
| 4xx client error | No — bad requests are not retried |
| 2xx success | No — request succeeded |
Retry schedule (2 attempts after the initial request):
| Attempt | Delay | |---|---| | 1st retry | ~100 ms + random jitter | | 2nd retry | ~200 ms + random jitter |
Jitter prevents thundering-herd problems when multiple clients retry simultaneously.
Idempotency and duplicate detection
track() sends an Idempotency-Key header on every request. If a retry delivers a duplicate event, the API returns { status: "duplicate" } instead of double-counting it. You can pass your own key for deterministic deduplication across retries:
await dravra.track("subscription.upgraded", {
userId: "user_abc123",
idempotencyKey: `upgrade-${userId}-${invoiceId}`,
});License
MIT — see LICENSE for details.
