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-afterheader 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);
},
});
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | required | API key for authentication (sent as Bearer token). |
host | string | https://cdp.timonier.eu | Base URL of the Timonier CDP instance. |
timeout | number | 10000 | Request timeout in milliseconds. |
maxRetries | number | 3 | Maximum retry attempts on 5xx or network errors. |
flushAt | number | 100 | Number of queued items that triggers an automatic batch flush. |
flushInterval | number | 5000 | Interval in milliseconds between automatic batch flushes. |
fetchFn | typeof fetch | fetch | Custom fetch implementation for testing or non-standard runtimes. |
onError | (err: unknown) => void | — | Called 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",
},
});
| Parameter | Type | Required | Description |
|---|---|---|---|
customerId | string | Yes | Unique customer ID. Max 255 chars. |
traits | Record<string, unknown> | No | Customer 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"),
});
| Parameter | Type | Required | Description |
|---|---|---|---|
customerId | string | Conditional | Required if anonymousId is not provided. |
anonymousId | string | Conditional | Required if customerId is not provided. |
event | string | Yes | Event name. Max 100 chars. |
properties | Record<string, unknown> | No | Event-specific data. |
context | Record<string, unknown> | No | Contextual metadata (user agent, IP, etc.). |
timestamp | Date | No | When the event occurred. Defaults to server time. |
messageId | string | No | Deduplication 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",
});
| Parameter | Type | Required | Description |
|---|---|---|---|
customerId | string | Conditional | Required if anonymousId is not provided. |
anonymousId | string | Conditional | Required if customerId is not provided. |
url | string | Yes | Page URL. Max 2048 chars. |
title | string | No | Page title. Max 500 chars. |
referrer | string | No | Referrer URL. Max 2048 chars. |
properties | Record<string, unknown> | No | Additional page metadata. |
context | Record<string, unknown> | No | Contextual metadata. |
timestamp | Date | No | When the page was viewed. Defaults to server time. |
messageId | string | No | Deduplication 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
| Error | Condition | Notable fields |
|---|---|---|
TimonierError | Base class for all SDK errors | message |
InputValidationError | Invalid input caught before the network call | field, message |
ValidationError | Server returned 400 | status, body |
AuthenticationError | Server returned 401 | status, body |
RateLimitError | Server returned 429 | status, body, retryAfter |
ServerError | Server 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.