Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

TypeScript SDK

The official TypeScript/JavaScript SDK for the Timonier CDP. It wraps the REST API with a type-safe client that handles retries, rate limits, batching, and deduplication automatically.

Package: timonier-sdk on npm

Features

  • TypeScript-first with full type definitions
  • Automatic retry with exponential backoff on 5xx and network errors
  • Rate limit handling with retry-after header support
  • Configurable request timeout (default 10s)
  • Auto-generated message_id (UUID v4) for event deduplication
  • Batch helper with auto-flush (size threshold + interval)
  • Zero external dependencies (uses native fetch)
  • ESM and CJS dual output

Requirements

Node.js 18+ (or any runtime with global fetch and crypto.randomUUID).

Installation

npm install timonier-sdk

Quick Start

import { Timonier } from "timonier-sdk";

const client = new Timonier({
  apiKey: "your-api-key",
});

await client.identify({
  customerId: "user-123",
  traits: { email: "jane@example.com", name: "Jane Doe" },
});

await client.track({
  customerId: "user-123",
  event: "Button Clicked",
  properties: { button: "signup", page: "/pricing" },
});

await client.page({
  customerId: "user-123",
  url: "https://example.com/pricing",
  title: "Pricing",
  referrer: "https://google.com",
});

Configuration

const client = new Timonier({
  apiKey: "your-api-key",       // Required
  host: "https://cdp.timonier.eu", // API base URL
  timeout: 10_000,              // Request timeout in ms
  maxRetries: 3,                // Max retries on 5xx/network errors
  flushAt: 100,                 // Auto-flush batch queue at N items
  flushInterval: 5_000,         // Auto-flush interval in ms
  fetchFn: fetch,               // Custom fetch implementation
  onError: (err) => {           // Called on background flush failures
    console.error("Flush failed:", err);
  },
});
OptionTypeDefaultDescription
apiKeystringrequiredAPI key for authentication (sent as Bearer token).
hoststringhttps://cdp.timonier.euBase URL of the Timonier CDP instance.
timeoutnumber10000Request timeout in milliseconds.
maxRetriesnumber3Maximum retry attempts on 5xx or network errors.
flushAtnumber100Number of queued items that triggers an automatic batch flush.
flushIntervalnumber5000Interval in milliseconds between automatic batch flushes.
fetchFntypeof fetchfetchCustom fetch implementation for testing or non-standard runtimes.
onError(err: unknown) => voidCalled when a background batch flush fails silently.

API Reference

client.identify(params)

Create or update a customer profile. Calls POST /v1/identify.

await client.identify({
  customerId: "user-123",
  traits: {
    email: "jane@example.com",
    name: "Jane Doe",
    plan: "pro",
  },
});
ParameterTypeRequiredDescription
customerIdstringYesUnique customer ID. Max 255 chars.
traitsRecord<string, unknown>NoCustomer attributes. Defaults to {}.

client.track(params)

Track a custom event. Calls POST /v1/track.

await client.track({
  customerId: "user-123",
  event: "Purchase Completed",
  properties: { amount: 49.99, currency: "USD" },
  timestamp: new Date("2026-01-15T10:30:00Z"),
});
ParameterTypeRequiredDescription
customerIdstringConditionalRequired if anonymousId is not provided.
anonymousIdstringConditionalRequired if customerId is not provided.
eventstringYesEvent name. Max 100 chars.
propertiesRecord<string, unknown>NoEvent-specific data.
contextRecord<string, unknown>NoContextual metadata (user agent, IP, etc.).
timestampDateNoWhen the event occurred. Defaults to server time.
messageIdstringNoDeduplication ID. Auto-generated (UUID v4) if omitted.

client.page(params)

Track a page view. Calls POST /v1/page.

await client.page({
  anonymousId: "anon-456",
  url: "https://example.com/blog/post-1",
  title: "Blog Post 1",
  referrer: "https://google.com",
});
ParameterTypeRequiredDescription
customerIdstringConditionalRequired if anonymousId is not provided.
anonymousIdstringConditionalRequired if customerId is not provided.
urlstringYesPage URL. Max 2048 chars.
titlestringNoPage title. Max 500 chars.
referrerstringNoReferrer URL. Max 2048 chars.
propertiesRecord<string, unknown>NoAdditional page metadata.
contextRecord<string, unknown>NoContextual metadata.
timestampDateNoWhen the page was viewed. Defaults to server time.
messageIdstringNoDeduplication ID. Auto-generated if omitted.

client.batch(items)

Send multiple operations in a single request. Calls POST /v1/batch.

const result = await client.batch([
  { type: "identify", customer_id: "user-1", traits: { name: "Jane" } },
  { type: "track", customer_id: "user-1", event: "Signup" },
  { type: "page", customer_id: "user-1", url: "https://example.com" },
]);

console.log(result.accepted); // 3
console.log(result.rejected); // 0

The batch accepts between 1 and 100 items. Each item must include a type field ("identify", "track", or "page") and the fields required by that operation. Field names use snake_case to match the REST API (e.g. customer_id, anonymous_id, message_id).


Auto-Batching

The SDK includes a built-in queue that buffers items and flushes them automatically via the batch endpoint. This reduces HTTP overhead for high-volume server-side ingestion.

client.enqueue({ type: "track", customer_id: "user-1", event: "Click" });
client.enqueue({ type: "track", customer_id: "user-2", event: "Click" });

Items are flushed when the queue reaches flushAt (default 100) or every flushInterval milliseconds (default 5 seconds), whichever comes first.

Manual flush

await client.flush();

Returns null if the queue is empty.

Graceful shutdown

Call shutdown() before your process exits to flush remaining items and stop the automatic flush timer:

await client.shutdown();

During shutdown, the SDK retries failed flushes up to 3 times. If retries are exhausted, the onError callback is called with the error and the affected items are dropped.


Error Handling

The SDK throws typed errors that all extend a common TimonierError base class. Client-side validation errors are thrown before any network call is made.

import {
  TimonierError,
  InputValidationError,
  ValidationError,
  AuthenticationError,
  RateLimitError,
  ServerError,
} from "timonier-sdk";

try {
  await client.track({ customerId: "user-1", event: "test" });
} catch (err) {
  if (err instanceof InputValidationError) {
    // Client-side validation failed (before network call)
    console.error(err.field, err.message);
  } else if (err instanceof ValidationError) {
    // Server returned 400 Bad Request
    console.error(err.body);
  } else if (err instanceof AuthenticationError) {
    // Server returned 401 Unauthorized
    console.error(err.body);
  } else if (err instanceof RateLimitError) {
    // Server returned 429 Too Many Requests
    console.error("Retry after", err.retryAfter, "seconds");
  } else if (err instanceof ServerError) {
    // Server returned 5xx (after all retries exhausted)
    console.error(err.status, err.body);
  }
}

Error hierarchy

ErrorConditionNotable fields
TimonierErrorBase class for all SDK errorsmessage
InputValidationErrorInvalid input caught before the network callfield, message
ValidationErrorServer returned 400status, body
AuthenticationErrorServer returned 401status, body
RateLimitErrorServer returned 429status, body, retryAfter
ServerErrorServer returned 5xx (after retries)status, body

All errors are instances of TimonierError, so you can use a single catch (err) { if (err instanceof TimonierError) { ... } } to handle every SDK error uniformly.

Retry behavior

The SDK automatically retries on 5xx errors and transient network failures (e.g. TypeError, timeout) with exponential backoff and jitter, up to maxRetries attempts (default 3). On 429 Too Many Requests, the SDK respects the retry-after header before retrying.

Non-retryable errors (400, 401) are thrown immediately.


Examples

Express middleware

import express from "express";
import { Timonier } from "timonier-sdk";

const app = express();
const client = new Timonier({ apiKey: process.env.TIMONIER_API_KEY! });

app.use(express.json());

app.post("/api/checkout", async (req, res) => {
  await client.track({
    customerId: req.user.id,
    event: "Checkout Completed",
    properties: { total: req.body.total },
  });
  res.json({ ok: true });
});

process.on("SIGTERM", async () => {
  await client.shutdown();
  process.exit(0);
});

app.listen(3000);

Next.js server action

"use server";

import { Timonier } from "timonier-sdk";

const client = new Timonier({ apiKey: process.env.TIMONIER_API_KEY! });

export async function trackSignup(userId: string) {
  await client.identify({
    customerId: userId,
    traits: { signedUpAt: new Date().toISOString() },
  });

  await client.track({
    customerId: userId,
    event: "Signup Completed",
  });
}

High-volume ingestion with auto-batching

import { Timonier } from "timonier-sdk";

const client = new Timonier({
  apiKey: process.env.TIMONIER_API_KEY!,
  flushAt: 50,
  flushInterval: 2_000,
  onError: (err) => console.error("Batch flush failed:", err),
});

for (const event of incomingEvents) {
  client.enqueue({
    type: "track",
    customer_id: event.userId,
    event: event.name,
    properties: event.data,
    message_id: event.id,
  });
}

// Flush remaining events before exiting
await client.shutdown();

Migrating from raw HTTP calls

If you are currently calling the REST API directly with fetch, switching to the SDK is straightforward. The SDK handles authentication headers, JSON serialization, retries, and deduplication for you.

Before (raw fetch):

const res = await fetch("https://cdp.timonier.eu/v1/track", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    customer_id: "user-123",
    event: "order_completed",
    properties: { total: 99.99 },
    message_id: crypto.randomUUID(),
  }),
});
if (!res.ok) throw new Error(`API error: ${res.status}`);

After (SDK):

await client.track({
  customerId: "user-123",
  event: "order_completed",
  properties: { total: 99.99 },
});

The SDK generates message_id automatically, retries on transient failures, and throws typed errors instead of requiring manual status code checks.