npm package: @kelpi/sdk
Description: Official JavaScript/TypeScript SDK for Kelpi - Customer engagement automation
Author: Kelpi
Latest version: 1.0.0

# Kelpi SDK

Official JavaScript/TypeScript SDK for [Kelpi](https://kelpi.ai) - Customer engagement automation.

## Installation

```bash
# npm
npm install @kelpi/sdk

# yarn
yarn add @kelpi/sdk

# pnpm
pnpm add @kelpi/sdk
```

## Requirements

- Node.js 16+ (for server-side usage)
- Modern browser with localStorage support (for client-side usage)

## Quick Start

```typescript
import { Kelpi } from '@kelpi/sdk';

// Create SDK instance
const kelpi = new Kelpi();

// Initialize the SDK (do this once, typically in your app's entry point)
kelpi.init({
  apiKey: 'klp_pk_your_public_key',
});

// Identify a user (required before tracking)
await kelpi.identify('user_12345', {
  email: 'john@example.com',
  name: 'John Doe',
  plan: 'premium',
});

// Track events
kelpi.track('button_clicked', {
  button_id: 'signup_cta',
  page: '/pricing',
});

// The SDK automatically batches and sends events
// You can force send immediately with:
await kelpi.flush();
```

## API Reference

### `new Kelpi(options?)`

Create a new SDK instance.

```typescript
import { Kelpi } from '@kelpi/sdk';

const kelpi = new Kelpi();
// or with custom storage
const kelpi = new Kelpi({ storage: customStorageAdapter });
```

### `init(config)`

Initialize the SDK. Must be called before any other methods.

```typescript
interface KelpiSDKConfig {
  apiKey: string;          // Required. Your public API key (klp_pk_*)
  apiHost?: string;        // Optional. API host (default: https://api.kelpi.ai)
  flushInterval?: number;  // Optional. Batch flush interval in ms (default: 10000)
  maxQueueSize?: number;   // Optional. Max events before auto-flush (default: 100, max: 1000)
  debug?: boolean;         // Optional. Enable console logging (default: false)
  customLogger?: CustomLogger; // Optional. Custom logger function
}

kelpi.init({
  apiKey: 'klp_pk_your_public_key',
  debug: true,  // Enable for development
});
```

### `identify(userId, traits?)`

Identify a user. Must be called before tracking events.

```typescript
// Basic identification
await kelpi.identify('user_12345');

// With traits
await kelpi.identify('user_12345', {
  email: 'john@example.com',
  name: 'John Doe',
  plan: 'premium',
  company: 'Acme Inc',
});
```

**Parameters:**
- `userId` (string, required) - Unique identifier for the user (max 256 chars)
- `traits` (object, optional) - User properties to associate

**Returns:** `Promise<IdentifyResponse>` with `{ success, contact_id, is_new }`

**Throws:** `KelpiError` on failure

### `track(event, properties?)`

Track an event for the identified user. Events are queued and sent in batches.

```typescript
// Basic event
kelpi.track('page_viewed');

// With properties
kelpi.track('item_purchased', {
  item_id: 'SKU-123',
  price: 29.99,
  currency: 'USD',
});
```

**Parameters:**
- `event` (string, required) - Event name (max 256 chars, cannot start with `$`)
- `properties` (object, optional) - Event properties

**Returns:** `void` (events are queued, not sent immediately)

**Throws:**
- `KelpiError` with code `NOT_INITIALIZED` if SDK not initialized
- `KelpiError` with code `NOT_IDENTIFIED` if user not identified
- `KelpiError` with code `INVALID_EVENT_NAME` if event name is invalid

### `setTraits(traits)`

Update user traits by merging with existing traits.

```typescript
await kelpi.setTraits({
  plan: 'enterprise',
  last_purchase: '2024-01-15',
});
```

**Parameters:**
- `traits` (object, required) - Traits to merge with existing traits

**Returns:** `Promise<void>`

### `reset()`

Clear the current user identity (for logout). Device ID is preserved.

```typescript
// Flushes pending events and clears identity
kelpi.reset();
```

### `flush()`

Force send all queued events immediately.

```typescript
// Useful before navigation or logout
await kelpi.flush();
```

**Returns:** `Promise<void>`

### `getUserId()`

Get the current user ID.

```typescript
const userId = kelpi.getUserId();
// Returns: 'user_12345' or null
```

### `isIdentified()`

Check if a user is currently identified.

```typescript
if (kelpi.isIdentified()) {
  kelpi.track('some_event');
}
```

### `getContactId()`

Get the Kelpi internal contact ID.

```typescript
const contactId = kelpi.getContactId();
// Returns: '550e8400-e29b-41d4-a716-446655440000' or null
```

### `getDeviceId()`

Get the device ID (persisted across sessions).

```typescript
const deviceId = kelpi.getDeviceId();
// Returns: UUID string or null if not initialized
```

## Error Handling

The SDK throws `KelpiError` instances with specific error codes:

```typescript
import { Kelpi, KelpiError, ErrorCodes } from '@kelpi/sdk';

const kelpi = new Kelpi();

try {
  kelpi.track('event_name');
} catch (error) {
  if (error instanceof KelpiError) {
    switch (error.code) {
      case ErrorCodes.NOT_INITIALIZED:
        console.error('SDK not initialized. Call kelpi.init() first.');
        break;
      case ErrorCodes.NOT_IDENTIFIED:
        console.error('User not identified. Call kelpi.identify() first.');
        break;
      case ErrorCodes.INVALID_EVENT_NAME:
        console.error('Invalid event name:', error.message);
        break;
      default:
        console.error('Kelpi error:', error.message);
    }
  }
}
```

### Error Codes

| Code | Description | Retryable |
|------|-------------|-----------|
| `NOT_INITIALIZED` | SDK not initialized | No |
| `NOT_IDENTIFIED` | User not identified before tracking | No |
| `INVALID_CONFIG` | Invalid configuration provided | No |
| `INVALID_EVENT_NAME` | Event name is empty or starts with `$` | No |
| `ALREADY_INITIALIZED` | SDK already initialized | No |
| `RATE_LIMITED` | API rate limit exceeded | Yes |
| `NETWORK_ERROR` | Network request failed | Yes |
| `SERVER_ERROR` | Server returned 5xx error | Yes |

### Automatic Retries

The SDK automatically retries failed API requests:
- **Rate limited (429):** Respects `Retry-After` header
- **Network errors:** Retries with exponential backoff
- **Server errors (5xx):** Retries with exponential backoff
- **Client errors (4xx):** No retry (indicates a bug in your code)

## TypeScript Support

The SDK is written in TypeScript and provides full type definitions.

```typescript
import {
  Kelpi,
  KelpiError,
  ErrorCodes,
  type KelpiSDKConfig,
  type UserTraits,
  type IdentifyResponse,
} from '@kelpi/sdk';

// All methods are fully typed
const config: KelpiSDKConfig = {
  apiKey: 'klp_pk_...',
  debug: true,
};

const kelpi = new Kelpi();
kelpi.init(config);

// Type-safe traits
const traits: UserTraits = {
  email: 'user@example.com',
  name: 'John Doe',
  customField: 'value', // Custom properties allowed
};

await kelpi.identify('user_123', traits);
```

### Exported Types

```typescript
// Configuration
interface KelpiSDKConfig {
  apiKey: string;
  apiHost?: string;
  flushInterval?: number;
  maxQueueSize?: number;
  debug?: boolean;
  customLogger?: CustomLogger;
}

// User traits (string-keyed record)
interface UserTraits {
  email?: string;
  name?: string;
  [key: string]: unknown;
}

// Identify response
interface IdentifyResponse {
  success: boolean;
  contact_id: string;
  is_new: boolean;
}

// Error class
class KelpiError extends Error {
  code: string;
  retryable: boolean;
}

// Error codes
const ErrorCodes = {
  NOT_INITIALIZED: 'not_initialized',
  NOT_IDENTIFIED: 'not_identified',
  INVALID_CONFIG: 'invalid_config',
  INVALID_EVENT_NAME: 'invalid_event_name',
  ALREADY_INITIALIZED: 'already_initialized',
  RATE_LIMITED: 'rate_limited',
  NETWORK_ERROR: 'network_error',
  SERVER_ERROR: 'server_error',
} as const;
```

## Storage

The SDK automatically selects the appropriate storage:
- **Browser:** Uses `localStorage` for persistence
- **Node.js/SSR:** Uses in-memory storage

You can provide a custom storage adapter:

```typescript
import { Kelpi, type StorageAdapter } from '@kelpi/sdk';

const customStorage: StorageAdapter = {
  get: (key) => { /* ... */ },
  set: (key, value) => { /* ... */ },
  remove: (key) => { /* ... */ },
  getJSON: (key) => { /* ... */ },
  setJSON: (key, value) => { /* ... */ },
};

const kelpi = new Kelpi({ storage: customStorage });
```

## License

MIT