npm package: @rastova/rasti
Description: Node.js SDK for the Rasti Messaging API
Author: Rastova
Latest version: 0.1.5

# @rastova/rasti

The official Node.js SDK for the [Rasti Messaging API](https://rastova.com). Send WhatsApp messages, SMS, manage templates, channels, media, and webhooks with full TypeScript support.

## Features

- **WhatsApp & SMS** - Send text, images, video, documents, locations, contacts, interactive messages, templates, and more
- **Full TypeScript** - Strict types for every method, parameter, and response
- **Zero dependencies** - Uses native `fetch` (Node.js 18+)
- **Automatic retries** - Exponential backoff with jitter for transient failures
- **Idempotent requests** - Safe retries for POST requests with idempotency keys
- **Dual CJS + ESM** - Works with `require()` and `import`
- **Detailed errors** - Typed error classes for every HTTP status code

## Installation

```bash
npm install @rastova/rasti
```

```bash
yarn add @rastova/rasti
```

```bash
pnpm add @rastova/rasti
```

> **Requirements:** Node.js 18 or later

## Getting Your API Key

To get your Rasti API key and start sending WhatsApp and SMS messages, [contact us here](https://rastova.com/contact).

## Quick Start

```typescript
import Rasti from '@rastova/rasti';

const rasti = new Rasti({
  apiKey: 'rsk_your_api_key', // or set RASTI_API_KEY env var
});

// Send a WhatsApp text message
const response = await rasti.messages.sendText({
  to: '1234567890',
  text: { body: 'Hello from Rasti!' },
});

console.log(response.messageId); // => "msg_abc123"
```

## Configuration

```typescript
const rasti = new Rasti({
  apiKey: 'rsk_...',             // Default: process.env.RASTI_API_KEY
  baseURL: 'https://api.rastova.io/v1', // Default: https://api.rastova.io/v1
  timeout: 30_000,               // Default: 30 seconds (ms)
  maxRetries: 2,                 // Default: 2 retries
  fetch: customFetch,            // Default: globalThis.fetch
});
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `apiKey` | `string` | `RASTI_API_KEY` env var | Your Rasti API key |
| `baseURL` | `string` | `https://api.rastova.io/v1` | API base URL (or `RASTI_BASE_URL` env var) |
| `timeout` | `number` | `30000` | Request timeout in milliseconds |
| `maxRetries` | `number` | `2` | Max retry attempts for retryable errors |
| `fetch` | `function` | `globalThis.fetch` | Custom fetch implementation |

## API Reference

### Messages

Send messages across WhatsApp and SMS channels.

#### WhatsApp Messages

```typescript
// Text
await rasti.messages.sendText({
  to: '1234567890',
  text: { body: 'Hello!', previewUrl: true },
});

// Image
await rasti.messages.sendImage({
  to: '1234567890',
  image: { link: 'https://example.com/photo.jpg', caption: 'Check this out' },
});

// Video
await rasti.messages.sendVideo({
  to: '1234567890',
  video: { link: 'https://example.com/video.mp4', caption: 'Watch this' },
});

// Audio
await rasti.messages.sendAudio({
  to: '1234567890',
  audio: { link: 'https://example.com/audio.mp3' },
});

// Document
await rasti.messages.sendDocument({
  to: '1234567890',
  document: { link: 'https://example.com/file.pdf', filename: 'invoice.pdf' },
});

// Sticker
await rasti.messages.sendSticker({
  to: '1234567890',
  sticker: { link: 'https://example.com/sticker.webp' },
});

// Location
await rasti.messages.sendLocation({
  to: '1234567890',
  location: { latitude: 37.7749, longitude: -122.4194, name: 'San Francisco' },
});

// Contacts
await rasti.messages.sendContacts({
  to: '1234567890',
  contacts: [
    {
      name: { formattedName: 'Jane Doe', firstName: 'Jane', lastName: 'Doe' },
      phones: [{ phone: '+1555123456', type: 'WORK' }],
      emails: [{ email: 'jane@example.com', type: 'WORK' }],
    },
  ],
});

// Reaction
await rasti.messages.sendReaction({
  to: '1234567890',
  reaction: { messageId: 'wamid.abc123', emoji: '👍' },
});

// Mark as read
await rasti.messages.markAsRead({
  messageId: 'wamid.abc123',
});
```

#### Interactive Messages

```typescript
// Buttons
await rasti.messages.sendInteractiveButtons({
  to: '1234567890',
  interactiveButtons: {
    body: { text: 'Choose an option:' },
    footer: { text: 'Tap a button below' },
    buttons: [
      { id: 'btn_yes', title: 'Yes' },
      { id: 'btn_no', title: 'No' },
    ],
  },
});

// List
await rasti.messages.sendInteractiveList({
  to: '1234567890',
  interactiveList: {
    body: { text: 'Browse our menu:' },
    button: 'View Menu',
    sections: [
      {
        title: 'Drinks',
        rows: [
          { id: 'coffee', title: 'Coffee', description: '$3.50' },
          { id: 'tea', title: 'Tea', description: '$2.50' },
        ],
      },
    ],
  },
});

// Call-to-Action URL
await rasti.messages.sendCtaUrl({
  to: '1234567890',
  ctaUrl: {
    body: { text: 'Visit our website for more details' },
    button: { displayText: 'Visit Website', url: 'https://example.com' },
  },
});

// Carousel
await rasti.messages.sendCarousel({
  to: '1234567890',
  carousel: {
    body: { text: 'Check out our products:' },
    cards: [
      {
        header: { type: 'image', image: { link: 'https://example.com/product1.jpg' } },
        body: { text: 'Product A - $29.99' },
        buttons: [{ type: 'quick_reply', title: 'Buy Now', id: 'buy_a' }],
      },
      {
        header: { type: 'image', image: { link: 'https://example.com/product2.jpg' } },
        body: { text: 'Product B - $39.99' },
        buttons: [{ type: 'url', title: 'Details', url: 'https://example.com/b' }],
      },
    ],
  },
});

// Location Request
await rasti.messages.sendLocationRequest({
  to: '1234567890',
  locationRequest: { body: { text: 'Please share your location for delivery' } },
});

// Address
await rasti.messages.sendAddress({
  to: '1234567890',
  address: {
    body: { text: 'Confirm your shipping address' },
    country: 'US',
  },
});
```

#### Template Messages

```typescript
await rasti.messages.sendTemplate({
  to: '1234567890',
  template: {
    name: 'order_confirmation',
    language: 'en',
    components: [
      {
        type: 'body',
        parameters: [
          { type: 'text', text: 'John' },
          { type: 'text', text: '#12345' },
        ],
      },
    ],
  },
});
```

#### SMS Messages

```typescript
const sms = await rasti.messages.sendSms({
  to: '1234567890',
  body: 'Your verification code is 123456',
  senderId: 'MyApp',
});

console.log(sms.smsPartCount);    // => 1
console.log(sms.characterCount);  // => 38
console.log(sms.encoding);        // => "plain"
```

### WhatsApp Channels

Manage your WhatsApp Business connections.

```typescript
// Connect a new channel
const channel = await rasti.whatsapp.channels.connect({
  name: 'Main Business Line',
  wabaId: 'waba_123',
  businessId: 'biz_456',
  phoneNumberId: 'pn_789',
  phoneNumber: '+1234567890',
  accessToken: 'EAAx...',
});

// List all channels
const { data, meta } = await rasti.whatsapp.channels.list({ limit: 10 });

// Get current channel
const current = await rasti.whatsapp.channels.getCurrent();

// Update channel settings
await rasti.whatsapp.channels.update({
  connectionId: 'conn_123',
  displayName: 'Updated Name',
});

// Resync channel metadata
await rasti.whatsapp.channels.resync('conn_123');

// Disconnect
await rasti.whatsapp.channels.disconnect({ connectionId: 'conn_123' });
```

### WhatsApp Templates

Create and manage message templates.

```typescript
// Create a template
const template = await rasti.whatsapp.templates.create({
  name: 'welcome_message',
  language: 'en',
  category: 'MARKETING',
  components: [
    { type: 'HEADER', format: 'TEXT', text: 'Welcome, {{1}}!' },
    { type: 'BODY', text: 'Thanks for joining us. Your code is {{1}}.' },
    { type: 'FOOTER', text: 'Reply STOP to unsubscribe' },
    {
      type: 'BUTTONS',
      buttons: [
        { type: 'QUICK_REPLY', text: 'Get Started' },
        { type: 'URL', text: 'Visit Site', url: 'https://example.com/{{1}}' },
      ],
    },
  ],
});

// List templates (with filters)
const { data } = await rasti.whatsapp.templates.list({
  status: 'APPROVED',
  category: 'MARKETING',
  limit: 20,
});

// Get a specific template
const tmpl = await rasti.whatsapp.templates.get('tmpl_abc123');

// Edit a template
await rasti.whatsapp.templates.edit('tmpl_abc123', {
  components: [{ type: 'BODY', text: 'Updated body text with {{1}}.' }],
});

// Delete a template
await rasti.whatsapp.templates.delete('tmpl_abc123');

// Sync templates from WhatsApp
const sync = await rasti.whatsapp.templates.sync();
console.log(`Synced: ${sync.synced}, Created: ${sync.created}`);
```

### WhatsApp Media

Retrieve, download, and delete media files.

```typescript
// Get media metadata
const media = await rasti.whatsapp.media.get('media_abc123');
console.log(media.mimeType); // => "image/jpeg"
console.log(media.fileSize); // => 45230

// Download media (returns raw Response)
const response = await rasti.whatsapp.media.download('media_abc123');
const buffer = Buffer.from(await response.arrayBuffer());

// Delete media
await rasti.whatsapp.media.delete('media_abc123');
```

### WhatsApp Business Profile

```typescript
// Get profile
const profile = await rasti.whatsapp.profile.get('conn_123');

// Update profile
await rasti.whatsapp.profile.update('conn_123', {
  about: 'We help businesses grow',
  description: 'Leading provider of business solutions',
  email: 'hello@example.com',
  websites: ['https://example.com'],
  vertical: 'PROF_SERVICES',
  address: '123 Main St, San Francisco, CA',
});
```

### SMS Channels

```typescript
// Connect an SMS channel
const smsChannel = await rasti.sms.channels.connect({
  providerName: 'whysms',
  senderId: 'MyApp',
});

// Get current SMS channel
const current = await rasti.sms.channels.getCurrent();
```

### Webhooks

Subscribe to real-time events.

```typescript
// Create a webhook
const webhook = await rasti.webhooks.create({
  url: 'https://example.com/webhooks/rasti',
  events: ['message.received', 'message.delivered', 'message.failed'],
});

console.log(webhook.signingSecret); // Save this to verify webhook signatures

// List webhooks
const { data } = await rasti.webhooks.list({ limit: 10 });

// Delete a webhook
await rasti.webhooks.delete('wh_abc123');
```

#### Available Webhook Events

| Category | Events |
|----------|--------|
| **Messages** | `message.received`, `message.sent`, `message.delivered`, `message.read`, `message.failed` |
| **Account** | `account.violation`, `account.restriction`, `account.ban`, `account.review`, `account.alert` |
| **Phone** | `phone_number.quality_update`, `phone_number.name_update` |
| **Business** | `business.capability_update` |
| **Templates** | `template.status_update`, `template.quality_update` |
| **Security** | `security.alert` |
| **SMS** | `sms.sent`, `sms.delivered`, `sms.failed` |

You can access the full list programmatically:

```typescript
import { WEBHOOK_EVENT_TYPES } from '@rastova/rasti';
```

## Advanced Usage

### Idempotent Requests

Pass an `idempotencyKey` to safely retry message sends without duplicates:

```typescript
await rasti.messages.sendText({
  to: '1234567890',
  text: { body: 'Important notification' },
  idempotencyKey: 'unique-key-abc-123',
});
```

### Specifying a Connection

If you have multiple WhatsApp connections, pass `connectionId` to target a specific one:

```typescript
await rasti.messages.sendText({
  to: '1234567890',
  text: { body: 'Hello!' },
  connectionId: 'conn_my_second_line',
});
```

### Replying to Messages

Use the `context` field to reply to a specific message:

```typescript
await rasti.messages.sendText({
  to: '1234567890',
  text: { body: 'Got it, thanks!' },
  context: { messageId: 'wamid.original_message_id' },
});
```

### Media by ID or URL

Media methods accept either a hosted URL (`link`) or a Rasti media ID (`id`):

```typescript
// By URL
await rasti.messages.sendImage({
  to: '1234567890',
  image: { link: 'https://example.com/photo.jpg' },
});

// By media ID (previously uploaded)
await rasti.messages.sendImage({
  to: '1234567890',
  image: { id: 'media_abc123' },
});
```

### Pagination

List endpoints return paginated results with metadata:

```typescript
const result = await rasti.whatsapp.templates.list({ limit: 10, offset: 0 });

console.log(result.data);           // TemplateResponse[]
console.log(result.meta.pagination); // { total: 42, limit: 10, offset: 0 }
```

## Error Handling

All API errors throw typed error classes with rich metadata:

```typescript
import { Rasti, AuthenticationError, RateLimitError, APIError } from '@rastova/rasti';

try {
  await rasti.messages.sendText({ to: '123', text: { body: 'Hi' } });
} catch (error) {
  if (error instanceof RateLimitError) {
    console.log('Rate limited. Retry after:', error.headers.get('retry-after'));
  } else if (error instanceof AuthenticationError) {
    console.log('Invalid API key');
  } else if (error instanceof APIError) {
    console.log(error.status);    // HTTP status code
    console.log(error.code);      // API error code
    console.log(error.message);   // Human-readable message
    console.log(error.requestId); // x-request-id header
    console.log(error.details);   // Additional error details
  }
}
```

### Error Classes

| Class | Status | Description |
|-------|--------|-------------|
| `BadRequestError` | 400 | Invalid request parameters |
| `AuthenticationError` | 401 | Invalid or missing API key |
| `PermissionDeniedError` | 403 | Insufficient permissions |
| `NotFoundError` | 404 | Resource not found |
| `ConflictError` | 409 | Resource conflict |
| `UnprocessableEntityError` | 422 | Validation failed |
| `RateLimitError` | 429 | Too many requests |
| `InternalServerError` | 500 | Server error |
| `BadGatewayError` | 502 | Bad gateway |
| `ServiceUnavailableError` | 503 | Service unavailable |
| `APIConnectionError` | - | Network/connection failure |
| `APIConnectionTimeoutError` | - | Request timed out |

### Automatic Retries

The SDK automatically retries on transient errors (408, 429, 500, 502, 503, 504) with exponential backoff. POST requests are only retried when an `idempotencyKey` is provided.

- **Backoff formula:** `min(0.5 * 2^attempt, 8s)` with random jitter
- **429 responses:** Respects the `Retry-After` header
- **Default:** 2 max retries (configurable via `maxRetries`)

## TypeScript

All parameter and response types are exported for full type safety:

```typescript
import type {
  // Client
  ClientOptions,
  RequestOptions,
  PaginationParams,
  ListResponse,

  // Messages
  SendTextParams,
  SendImageParams,
  SendVideoParams,
  SendAudioParams,
  SendDocumentParams,
  SendStickerParams,
  SendLocationParams,
  SendContactsParams,
  SendReactionParams,
  SendInteractiveButtonsParams,
  SendInteractiveListParams,
  SendCtaUrlParams,
  SendCarouselParams,
  SendLocationRequestParams,
  SendAddressParams,
  SendTemplateParams,
  SendSmsParams,
  MarkAsReadParams,
  WhatsAppMessageResponse,
  SmsMessageResponse,

  // WhatsApp
  ConnectChannelParams,
  UpdateChannelParams,
  ChannelResponse,
  CreateTemplateParams,
  EditTemplateParams,
  ListTemplatesParams,
  TemplateResponse,
  MediaResponse,
  UpdateProfileParams,
  ProfileResponse,

  // SMS
  ConnectSmsChannelParams,
  SmsChannelResponse,

  // Webhooks
  CreateWebhookParams,
  WebhookResponse,
  WebhookEventType,
} from '@rastova/rasti';
```

## CommonJS

```javascript
const { Rasti } = require('@rastova/rasti');

const rasti = new Rasti({ apiKey: 'rsk_...' });
```

## License

MIT