TypeScript SDK

Typed client for the Owlat API, usable from Node.js, Bun, Deno, or any server-side JavaScript runtime.

The official @owlat/sdk-js package provides a typed client for interacting with the Owlat API from Node.js, Bun, Deno, or any server-side JavaScript runtime.

Installation

bun add @owlat/sdk-js
npm install @owlat/sdk-js
pnpm add @owlat/sdk-js

Quick start

import { Owlat } from '@owlat/sdk-js'

const owlat = new Owlat('lm_live_...')

// Send a transactional email
await owlat.transactional.send({
  email: 'recipient@example.com',
  slug: 'welcome-email',
  dataVariables: { firstName: 'Mira' }
})

You can also pass a configuration object:

const owlat = new Owlat({
  apiKey: 'lm_live_...',
  baseUrl: 'https://your-deployment.convex.site',
  timeout: 30000,
  retry: { maxRetries: 2, initialDelayMs: 500, backoffMultiplier: 2 },
})

baseUrl is optional and defaults to https://api.owlat.app. You only need to set it for self-hosted deployments — point it at your Convex deployment site URL (https://<your-deployment>.convex.site), which serves the API from /api/v1/*.

Retries and idempotency

The SDK retries transient failures automatically — up to 2 retries (3 attempts total) with exponential backoff (500 ms base, doubling each time). A 429 is always retried regardless of method, honoring the Retry-After header. A 5xx or network/timeout error is retried only for idempotent methods (GET/PUT/DELETE).

POST sends — transactional.send(...) and events.send(...) — are not auto-retried on a 5xx or network error, since the server has no idempotency key and a send it already processed must not be duplicated; those surface as errors for you to handle. Pass retry: false to disable retries entirely, or tune maxRetries / initialDelayMs / backoffMultiplier.

Resources

Contacts

Manage contacts in your audience.

// Create a contact
const contact = await owlat.contacts.create({
  email: 'mira@acme.io',
  firstName: 'Mira',
  lastName: 'Chen',
})

// Get a contact by ID
const found = await owlat.contacts.get('contact_abc123')

// Update a contact
const updated = await owlat.contacts.update('contact_abc123', {
  firstName: 'Mira',
  lastName: 'Chen-Lopez',
})

// List contacts with cursor-based pagination
const result = await owlat.contacts.list({ limit: 25 })
// result.data       — Contact[]
// result.pagination  — { limit, totalItems, cursor, isDone }

// Fetch the next page with the returned cursor
if (!result.pagination.isDone) {
  const next = await owlat.contacts.list({ cursor: result.pagination.cursor })
}

// Or iterate every contact automatically (follows cursors until isDone)
for await (const contact of owlat.contacts.listAll()) {
  console.log(contact.email)
}

// Delete a contact
await owlat.contacts.delete('contact_abc123')

Transactional

Send transactional emails using pre-built templates.

const result = await owlat.transactional.send({
  // Identify the template by slug or ID
  slug: 'order-confirmation',

  // Recipient
  email: 'buyer@example.com',

  // Data variables merged into the template
  dataVariables: {
    orderId: 'ORD-7291',
    total: '$142.00',
  },

  // Optional: override language
  language: 'de',
})
// result.status === 'queued'
// result.transactionalEmailId — id of the send record
// result.contactId, result.contactCreated, result.language

Attachments

You can attach files using base64-encoded content or HTTPS URLs (max 10 files, 10 MB total):

await owlat.transactional.send({
  email: 'buyer@example.com',
  slug: 'order-confirmation',
  attachments: [
    // Base64-encoded content
    {
      filename: 'invoice.pdf',
      content: base64String,
      contentType: 'application/pdf',
    },
    // URL (fetched server-side)
    {
      filename: 'receipt.pdf',
      url: 'https://your-api.com/receipts/ORD-7291.pdf',
    },
  ],
})

Events

Send custom events to trigger automations and build segments.

await owlat.events.send({
  email: 'mira@acme.io',
  eventName: 'plan_upgraded',
  eventProperties: {
    plan: 'pro',
    mrr: 49,
  },
})

Topics

Manage topic memberships. The API is write-only (add/remove) — there is no list/get endpoint, so copy the topicId from the dashboard (Topics → a topic).

// Add a contact to a topic
await owlat.topics.addContact({
  topicId: 'topic_abc123',
  email: 'mira@acme.io',
})

// Remove a contact from a topic
await owlat.topics.removeContact({
  topicId: 'topic_abc123',
  emailOrId: 'mira@acme.io',
})

Error handling

The SDK exports typed error classes for common failure modes:

import {
  Owlat,
  OwlatError,
  AuthenticationError,
  RateLimitError,
  NotFoundError,
  ValidationError,
  ConflictError,
  ForbiddenError,
  InvalidStateError,
  LimitReachedError,
} from '@owlat/sdk-js'

try {
  await owlat.transactional.send({ email: 'user@example.com', slug: 'welcome' })
} catch (error) {
  if (error instanceof ValidationError) {
    console.error('Validation failed:', error.message)
  } else if (error instanceof InvalidStateError) {
    // 422 — unverified sending domain, blocked recipient, unpublished template.
    // The most common transactional failure mode.
    console.error('Cannot send yet:', error.message)
  } else if (error instanceof ForbiddenError) {
    // 403 — suspended / abuse-blocked account.
    console.error('Not permitted:', error.message)
  } else if (error instanceof LimitReachedError) {
    console.error('Plan limit reached:', error.message)
  } else if (error instanceof RateLimitError) {
    console.error('Rate limited — retry after backoff')
  } else if (error instanceof AuthenticationError) {
    console.error('Bad API key')
  } else if (error instanceof ConflictError) {
    console.error('Contact already exists')
  } else if (error instanceof NotFoundError) {
    console.error('Resource not found')
  } else if (error instanceof OwlatError) {
    console.error('API error:', error.message, error.statusCode)
  }
}
Error classHTTP statusWhen
AuthenticationError401Invalid or missing API key
LimitReachedError402Plan or quota limit reached
ForbiddenError403Authenticated but not permitted (suspended / abuse-blocked)
NotFoundError404Resource does not exist
ConflictError409Duplicate resource (e.g. existing email)
InvalidStateError422Resource state blocks the operation (unverified domain, blocked recipient, unpublished template)
RateLimitError429Too many requests
ValidationError400Request body fails validation

Types

All request and response types are exported for use in your own code:

import type {
  Contact,
  CreateContactParams,
  UpdateContactParams,
  SendTransactionalParams,
  SendTransactionalResponse,
  TransactionalAttachment,
  SendEventParams,
  AddToTopicParams,
  PaginatedResponse,
  ApiResponse,
} from '@owlat/sdk-js'

Using Java?

See the Java SDK reference for JVM integration (Java 11+).