npm package: @sendmailos/sdk
Description: Official JavaScript/TypeScript SDK for SendMailOS email API
Author: SendMailOS
Latest version: 1.5.1

# @sendmailos/sdk

Official JavaScript/TypeScript SDK for SendMailOS email API.

## Installation

```bash
npm install @sendmailos/sdk
# or
yarn add @sendmailos/sdk
# or
pnpm add @sendmailos/sdk
```

## Quick Start

```typescript
import { SendMailOS } from '@sendmailos/sdk';

const client = new SendMailOS('sk_live_your_api_key');

// Send a transactional email
await client.emails.send({
  to: 'user@example.com',
  fromName: 'Your Company',
  fromEmail: 'hello@yourcompany.com',
  subject: 'Welcome!',
  html: '<h1>Hello!</h1><p>Welcome to our platform.</p>'
});
```

## Features

- **Type-safe**: Full TypeScript support with exported types
- **Lightweight**: Zero dependencies for core SDK
- **Attachments**: Send files via Base64 or URL (auto-fetched)
- **Email Automation**: Build workflows with triggers, delays, conditions, and actions
- **Industry Templates**: 50+ pre-built templates for different industries
- **Agency Workspaces**: Manage multiple clients from one account
- **Pixel Tracking**: Track website visitors and link to email campaigns
- **Webhooks**: Real-time event notifications with signature verification
- **React Components**: Pre-built components and hooks
- **Error Handling**: Custom error classes for different scenarios
- **External IDs & Metadata**: Correlate records with your system and store custom data

## External IDs & Metadata

All resources support `externalId` and `metadata` fields for integrating with your existing systems.

```typescript
// Store your system's ID and custom data on any resource
await client.subscribers.create({
  email: 'user@example.com',
  externalId: 'your_system_user_123',  // Your internal ID
  metadata: {
    source: 'mobile_app',
    signup_campaign: 'summer_2024',
    customer_tier: 'gold'
  }
});

// Query by external ID
const { subscribers } = await client.subscribers.list({
  externalId: 'your_system_user_123'
});

// Works on all resources: subscribers, domains, campaigns, emails, workflows, pixels
await client.domains.create({
  domain: 'client.com',
  externalId: 'client_domain_456',
  metadata: { client_name: 'Acme Corp' }
});

await client.emails.send({
  to: 'user@example.com',
  subject: 'Order Confirmation',
  html: '<h1>Thanks!</h1>',
  fromEmail: 'orders@company.com',
  externalId: 'order_email_789',
  metadata: { order_id: 'ORD-123', amount: 99.99 }
});
```

## API Reference

### Emails

```typescript
// Send transactional email (Single Brand)
await client.emails.send({
  to: 'user@example.com',
  subject: 'Hello!',
  html: '<h1>Welcome!</h1>',
  fromName: 'Acme',
  fromEmail: 'hello@acme.com',
});

// Send email (Multi-Brand / SaaS Platform)
// Workspace auto-detected from fromEmail domain - no extra params needed!
await platform.emails.send({
  to: 'customer@example.com',
  subject: 'Your order is ready!',
  html: '<h1>Order Ready!</h1>',
  fromName: 'Pizza Palace',
  fromEmail: 'orders@pizzapalace.com',  // ← Workspace auto-detected
});

// Send with template
await client.emails.sendTemplate({
  to: 'user@example.com',
  templateId: 'tmpl_welcome',
  variables: { firstName: 'John' }
});

// Send with external ID and metadata (for tracking in your system)
await client.emails.send({
  to: 'customer@example.com',
  subject: 'Order Confirmation #12345',
  html: '<h1>Order Confirmed!</h1>',
  fromName: 'Acme Store',
  fromEmail: 'orders@acme.com',
  externalId: 'order_email_12345',
  metadata: {
    order_id: 'ORD-12345',
    order_total: 149.99,
    source: 'checkout_flow'
  }
});
```

### Attachments

Send emails with file attachments (max 5 files, 7MB total).

```typescript
// With Base64 content
await client.emails.send({
  to: 'user@example.com',
  subject: 'Invoice Attached',
  html: '<p>Please find your invoice attached.</p>',
  fromEmail: 'billing@company.com',
  fromName: 'Billing',
  attachments: [
    {
      filename: 'invoice.pdf',
      content: 'JVBERi0xLjQK...', // Base64 encoded
      contentType: 'application/pdf'
    }
  ]
});

// With URL (fetched automatically)
await client.emails.send({
  to: 'user@example.com',
  subject: 'Your Report',
  html: '<p>Here is your report.</p>',
  fromEmail: 'reports@company.com',
  attachments: [
    {
      filename: 'report.pdf',
      url: 'https://cdn.example.com/reports/123.pdf',
      contentType: 'application/pdf'
    }
  ]
});

// Inline images (using cid)
await client.emails.send({
  to: 'user@example.com',
  subject: 'Check out our logo',
  html: '<p>Our logo: <img src="cid:logo-image"></p>',
  fromEmail: 'hello@company.com',
  attachments: [
    {
      filename: 'logo.png',
      url: 'https://cdn.example.com/logo.png',
      contentType: 'image/png',
      cid: 'logo-image'  // Reference in HTML with cid:logo-image
    }
  ]
});
```

**Attachment Limits:**
| Limit | Value |
|-------|-------|
| Max attachments | 5 |
| Single file max | 5 MB |
| Total size | 7 MB |

### Subscribers

```typescript
// Create subscriber (Single Brand)
const { subscriber } = await client.subscribers.create({
  email: 'user@example.com',
  firstName: 'John',
  tags: ['newsletter', 'premium'],
  externalId: 'crm_contact_123',  // Optional: your system's ID
  metadata: { source: 'website' } // Optional: custom data
});

// Create subscriber (Multi-Brand / SaaS Platform)
const { subscriber } = await platform.subscribers.create({
  email: 'customer@example.com',
  firstName: 'Jane',
  domain: 'pizzapalace.com',  // Required for multi-brand
  tags: ['loyalty-member'],
  externalId: 'rb_customer_456',
  metadata: { restaurant_id: 'rest_789' }
});

// List subscribers
const { subscribers, total } = await client.subscribers.list({
  limit: 50,
  offset: 0
});

// List by external ID
const { subscribers } = await client.subscribers.list({
  externalId: 'crm_contact_123'
});

// List subscribers for a specific client (Multi-Brand)
const { subscribers } = await platform.subscribers.list({
  domain: 'pizzapalace.com',
  limit: 50
});
```

### Campaigns

```typescript
// Send campaign (Single Brand)
await client.campaigns.send({
  name: 'Weekly Newsletter',
  subject: 'What\'s new this week',
  fromName: 'Company Newsletter',
  fromEmail: 'news@company.com',
  html: '<h1>Hello {{first_name}}!</h1>',
  tags: ['newsletter'], // Filter by subscriber tags
  externalId: 'campaign_jan_2024',  // Optional: track in your system
  metadata: { campaign_type: 'weekly' }
});

// Send campaign (Multi-Brand / SaaS Platform)
// Workspace auto-detected from fromEmail domain
await platform.campaigns.send({
  name: 'Pizza Promo',
  subject: 'New menu items!',
  fromName: 'Pizza Palace',
  fromEmail: 'promo@pizzapalace.com',  // ← Workspace auto-detected
  html: '<h1>Check out our new items!</h1>',
  sourceDomains: ['pizzapalace.com'],  // Only send to subscribers from this domain
  externalId: 'rb_promo_123',
  metadata: { restaurant_id: 'rest_456' }
});
```

### Domains

```typescript
// Add sending domain (Single Brand)
const { domain } = await client.domains.create({
  domain: 'yourcompany.com',
  externalId: 'domain_001',  // Optional: your system's ID
  metadata: { environment: 'production' }
});

console.log('DNS Records to add:', domain.dnsRecords);

// Add client domain (Multi-Brand / SaaS Platform)
// Show these DNS records to your client in your UI
const { dns_records, domain_id } = await platform.domains.create({
  domain: 'pizzapalace.com',
  externalId: 'rb_domain_456',
  metadata: { client_name: 'Pizza Palace' }
});

// After client verifies DNS:
// ✓ Domain status becomes "verified"
// ✓ Workspace is AUTO-CREATED for this domain
// ✓ You can now send emails from @pizzapalace.com
```

## Organization & Industries

Set your industry during onboarding to get pre-built templates and workflows.

```typescript
import { SendMailOS, INDUSTRIES } from '@sendmailos/sdk';

const client = new SendMailOS('sk_live_...');

// Get current organization
const org = await client.organization.get();

// Set industries (multi-select)
await client.organization.setIndustries(['ecommerce', 'saas']);

// Convert to agency account (enables workspaces)
await client.organization.convertToAgency();

// Available industries (50+)
console.log(INDUSTRIES);
// { hotels: 'Hotels & Hospitality', restaurants: 'Restaurants & Food Service', ... }
```

### Supported Industries

Hotels, Restaurants, Gyms, Travel Agencies, Real Estate, Schools, E-commerce, Dentists, Car Dealerships, Events, Beauty Salons, Law Firms, Non-Profits, SaaS, Recruitment, Insurance, Online Courses, Municipalities, Personal Trainers, Influencers, Doctors/Clinics, Nightclubs, Coworking, Wedding Planners, Art Galleries, Car Rentals, Podcasters, Consultants, Bakeries, Veterinarians, Airbnb Hosts, Accountants, Developers, Musicians, Spas, Bookstores, Cleaning Services, Churches, Graphic Designers, Coffee Shops, Florists, Political Campaigns, Libraries, Taxis/Limos, Home Inspectors, Photographers, Universities, Fashion, Nutritionists, Startups

## Account Types

### Single Brand (Business)

For companies sending emails from their own domain. Simple setup, no workspaces needed.

```typescript
const client = new SendMailOS('sk_live_your_api_key');

await client.emails.send({
  to: 'customer@example.com',
  fromEmail: 'hello@yourcompany.com',
  subject: 'Welcome!',
  html: '<h1>Hello!</h1>'
});
```

### Multi-Brand / SaaS Platform

For agencies or SaaS platforms managing multiple clients. Each client gets their own workspace with isolated data.

```typescript
// Your platform's master API key
const platform = new SendMailOS('sk_live_platform_key');

// 1. Client signs up → Register their domain
const { dns_records } = await platform.domains.create({
  domain: 'pizzapalace.com'
});
// Show dns_records to client in your UI

// 2. Domain verified → Workspace auto-created!
//    (No manual workspace creation needed)

// 3. Send emails → Workspace auto-detected from fromEmail
await platform.emails.send({
  to: 'customer@example.com',
  fromEmail: 'orders@pizzapalace.com',  // ← Workspace detected automatically!
  fromName: 'Pizza Palace',
  subject: 'Your order is ready!',
  html: '<h1>Order Ready!</h1>'
});

// 4. Add subscribers → Use domain param
await platform.subscribers.create({
  email: 'customer@example.com',
  domain: 'pizzapalace.com'  // Required for multi-brand
});

// 5. List subscribers for a specific client
const { subscribers } = await platform.subscribers.list({
  domain: 'pizzapalace.com'
});

// 6. Send campaign → Workspace auto-detected
await platform.campaigns.send({
  subject: 'New Menu Items!',
  fromName: 'Pizza Palace',
  fromEmail: 'promo@pizzapalace.com',  // ← Auto-detected
  html: '<h1>Check out our new menu!</h1>'
});
```

### How Workspace Detection Works

| Action | How workspace is identified |
|--------|----------------------------|
| Register domain | Creates domain record (pending) |
| Domain verified | **Workspace auto-created** |
| Send email | Auto-detect from `fromEmail` domain |
| Send campaign | Auto-detect from `fromEmail` domain |
| Add subscriber | From `domain` parameter |
| List subscribers | From `domain` parameter |

**One verified domain = One workspace.** Simple.

### Master API Keys

Master API keys can access all workspaces in your organization with a single key. Ideal for SaaS platforms that need to manage multiple client workspaces programmatically.

#### Recommended Flow for SaaS Platforms

```typescript
// Your platform backend - when a new client signs up
const platform = new SendMailOS('sk_live_master_key');

// 1. Create workspace first (with your client's data)
const { workspace } = await platform.workspaces.create({
  name: 'Pizza Palace',           // From your user's profile
  industry: 'restaurants',        // Optional - can be null
  website: 'https://pizzapalace.com'
});

// 2. Add domain TO that workspace
const { dns_records } = await platform.domains.create({
  domain: 'pizzapalace.com',
  workspaceId: workspace.id       // Link to the workspace
});

// 3. Show DNS records to your user
// They verify DNS, domain becomes active

// 4. Now send emails using workspace_id or domain
await platform.emails.send({
  to: 'customer@example.com',
  fromEmail: 'hello@pizzapalace.com',
  subject: 'Welcome!',
  html: '<h1>Welcome!</h1>',
  workspaceId: workspace.id
});
```

Your end users only see: **"Add domain → Verify DNS → Done"**

Behind the scenes: Workspace created with proper settings → Domain linked → Ready to send.

**Master Key vs Workspace Key:**
| Feature | Master Key | Workspace Key |
|---------|-----------|---------------|
| Access | All workspaces | Single workspace |
| workspaceId param | Required | Not needed |
| Use case | Platform/SaaS backend | Per-client integrations |

## Agency Workspaces (Advanced)

For more control, you can manually manage workspaces:

```typescript
// Convert to agency account first
await client.organization.convertToAgency();

// Create client workspaces manually
const pizzaPlace = await client.workspaces.create({
  name: 'Pizza Palace',
  industry: 'restaurants',
  import_templates: true  // Auto-import restaurant templates
});

// List all clients
const { workspaces } = await client.workspaces.list();
for (const ws of workspaces) {
  console.log(`${ws.name}: ${ws.stats?.subscribers} subscribers`);
}

// Get detailed stats
const details = await client.workspaces.get(pizzaPlace.id);
console.log(details.stats);
// { total_subscribers: 1200, active_subscribers: 1150, total_campaigns: 24, ... }

// Invite client to view reports
await client.workspaces.inviteClient(pizzaPlace.id, 'owner@pizzapalace.com');
```

## Workflows

Automate email sequences with workflows. Create them via API, build the steps in your UI, then trigger them programmatically.

### Workflow Lifecycle

```
1. Create workflow (draft)
2. Add nodes & edges (build the flow)
3. Activate workflow (start accepting triggers)
4. Trigger for subscribers (via API or events)
```

### Create & Manage Workflows

```typescript
// Create a workflow for a client (Multi-Brand)
const { data } = await platform.workflows.create({
  name: 'Welcome Sequence',
  domain: 'pizzapalace.com',  // Auto-detect workspace
  trigger: { type: 'api' }    // Triggered via API
});

const workflowId = data.workflow.id;

// List workflows
const { data: list } = await platform.workflows.list({ domain: 'pizzapalace.com' });

// Get workflow with nodes/edges
const { data: workflow } = await platform.workflows.get(workflowId);

// Delete workflow
await platform.workflows.delete(workflowId);
```

### Build Workflow Steps

Update a workflow with nodes (steps) and edges (connections):

```typescript
await platform.workflows.update(workflowId, {
  nodes: [
    {
      id: 'trigger-1',
      type: 'trigger',
      data: { triggerType: 'api' }
    },
    {
      id: 'email-1',
      type: 'email',
      data: {
        subject: 'Welcome to {{restaurantName}}!',
        templateId: 'tmpl_welcome'
      }
    },
    {
      id: 'delay-1',
      type: 'delay',
      data: { duration: 3, unit: 'days' }
    },
    {
      id: 'email-2',
      type: 'email',
      data: {
        subject: 'Your first order discount',
        templateId: 'tmpl_promo'
      }
    }
  ],
  edges: [
    { id: 'e1', source: 'trigger-1', target: 'email-1' },
    { id: 'e2', source: 'email-1', target: 'delay-1' },
    { id: 'e3', source: 'delay-1', target: 'email-2' }
  ]
});
```

### Node Types

| Type | Description | Data Fields |
|------|-------------|-------------|
| `trigger` | Entry point | `triggerType` |
| `email` | Send email | `subject`, `templateId`, `fromName`, `fromEmail` |
| `delay` | Wait period | `duration`, `unit` (minutes/hours/days) |
| `condition` | Branch logic | `field`, `operator`, `value` |
| `tag` | Add/remove tags | `action` (add/remove), `tags` |
| `http` | Call webhook | `url`, `method`, `headers`, `body` |
| `wait_for_event` | Wait for event | `eventName`, `timeout` |
| `unsubscribe` | Unsubscribe user | - |
| `exit` | End workflow | - |

### Activate & Pause

```typescript
// Activate workflow (start accepting triggers)
await platform.workflows.activate(workflowId);

// Pause workflow (stop new triggers, existing runs continue)
await platform.workflows.pause(workflowId);
```

### Trigger Workflows

```typescript
// When a customer signs up at the restaurant
await platform.workflows.trigger(workflowId, {
  email: 'customer@example.com',
  data: {
    firstName: 'John',
    restaurantName: 'Pizza Palace',
    signupSource: 'website'
  }
});
```

### Wait for Events

Workflows can pause and wait for specific events:

```typescript
// Workflow has a "wait_for_event: purchase_completed" node
// When customer makes a purchase, send the event:
await platform.workflows.sendEvent({
  event: 'purchase_completed',
  email: 'customer@example.com',
  data: { orderTotal: 45.99 }
});
// The workflow resumes from where it was waiting
```

### Example: Restaurant Welcome Flow

```typescript
// 1. Create workflow
const { data } = await platform.workflows.create({
  name: 'New Customer Welcome',
  domain: 'pizzapalace.com'
});

// 2. Build the flow
await platform.workflows.update(data.workflow.id, {
  nodes: [
    { id: '1', type: 'trigger', data: { triggerType: 'api' } },
    { id: '2', type: 'email', data: { subject: 'Welcome!', templateId: 'tmpl_welcome' } },
    { id: '3', type: 'delay', data: { duration: 2, unit: 'days' } },
    { id: '4', type: 'condition', data: { field: 'has_ordered', operator: 'eq', value: true } },
    { id: '5', type: 'email', data: { subject: 'Thanks for ordering!', templateId: 'tmpl_thanks' } },
    { id: '6', type: 'email', data: { subject: '10% off your first order', templateId: 'tmpl_promo' } }
  ],
  edges: [
    { id: 'e1', source: '1', target: '2' },
    { id: 'e2', source: '2', target: '3' },
    { id: 'e3', source: '3', target: '4' },
    { id: 'e4', source: '4', target: '5', sourceHandle: 'yes' },
    { id: 'e5', source: '4', target: '6', sourceHandle: 'no' }
  ]
});

// 3. Activate
await platform.workflows.activate(data.workflow.id);

// 4. Trigger when new customer signs up
await platform.workflows.trigger(data.workflow.id, {
  email: 'newcustomer@gmail.com',
  data: { firstName: 'Jane', restaurantName: 'Pizza Palace' }
});
```

## Webhooks

Receive real-time notifications when events occur (emails sent, opened, clicked, bounced, etc.).

### Create & Manage Webhooks

```typescript
// Create a webhook endpoint
const { webhook } = await client.webhooks.create({
  url: 'https://yourserver.com/webhooks',
  events: ['email.sent', 'email.delivered', 'email.opened', 'email.clicked', 'email.bounced'],
});

console.log('Webhook secret:', webhook.secret); // Use this to verify signatures

// List all webhooks
const { webhooks } = await client.webhooks.list();

// Get webhook with delivery history
const { webhook, recentDeliveries, stats } = await client.webhooks.get('webhook-id');

// Update webhook
await client.webhooks.update('webhook-id', {
  events: ['email.sent', 'email.bounced', 'email.complained']
});

// Test webhook endpoint
const result = await client.webhooks.test('webhook-id');
if (result.test.sent) {
  console.log(`Test delivered in ${result.test.responseTimeMs}ms`);
}

// Disable/Enable webhook
await client.webhooks.disable('webhook-id');
await client.webhooks.enable('webhook-id');

// Delete webhook
await client.webhooks.delete('webhook-id');
```

### Event Types

| Event | Description |
|-------|-------------|
| `email.sent` | Email was sent to the recipient |
| `email.delivered` | Email was delivered to recipient's server |
| `email.opened` | Recipient opened the email |
| `email.clicked` | Recipient clicked a link in the email |
| `email.bounced` | Email bounced (hard or soft) |
| `email.complained` | Recipient marked email as spam |
| `subscriber.created` | New subscriber was added |
| `subscriber.updated` | Subscriber was updated |
| `subscriber.unsubscribed` | Subscriber unsubscribed |
| `campaign.sent` | Campaign finished sending |
| `workflow.started` | Workflow execution started |
| `workflow.completed` | Workflow execution completed |

## Pixel Tracking

Track website visitors and connect their behavior to email campaigns.

### Basic Setup

```typescript
import { SendmailPixel } from '@sendmailos/sdk';

const pixel = new SendmailPixel({
  token: 'pk_live_your_public_key',
  debug: false // Set true for console logging
});

pixel.init();
```

### Track Events

```typescript
// Track custom events
pixel.track('button_clicked', { button_id: 'cta-signup' });

// Track page views (called automatically on init)
pixel.pageView();

// Identify a visitor by email
pixel.identify('user@example.com', {
  first_name: 'John',
  plan: 'premium'
});
```

### Privacy Controls

```typescript
// Opt out of tracking (e.g., user declines cookies)
pixel.optOut();

// Opt back in
pixel.optIn();

// Check opt-out status
if (pixel.isOptedOut()) {
  console.log('Tracking disabled');
}

// Reset identity (e.g., on logout)
pixel.reset();
```

### Global smp() Function

For script tag usage, use `createGlobalPixel` to set up a global `smp()` function:

```typescript
import { createGlobalPixel } from '@sendmailos/sdk';

createGlobalPixel({ token: 'pk_live_...' });

// Now use anywhere:
smp('track', 'event_name', { property: 'value' });
smp('identify', 'user@example.com');
smp('optOut');
smp('optIn');
```

### React Pixel Hooks

```tsx
import { usePixel, usePageTracking, useIdentifyOnLogin } from '@sendmailos/sdk/react';

// Basic usage
function MyComponent() {
  const { track, identify, optOut } = usePixel('pk_live_...');

  return (
    <button onClick={() => track('button_clicked', { id: 'cta' })}>
      Click Me
    </button>
  );
}

// Auto page tracking (Next.js App Router)
function Layout({ children }) {
  const pathname = usePathname();
  usePageTracking('pk_live_...', pathname);
  return <>{children}</>;
}

// Auto identify on login
function App() {
  const { user } = useAuth();
  useIdentifyOnLogin('pk_live_...', user?.email, {
    first_name: user?.firstName,
    plan: user?.plan
  });
  return <Main />;
}
```

### Data Attributes

Track clicks without JavaScript using HTML data attributes:

```html
<button data-smp-track="signup_clicked">Sign Up</button>

<button
  data-smp-track="button_clicked"
  data-smp-track-button-id="cta-main"
  data-smp-track-variant="blue"
>
  Get Started
</button>
```

## React Integration

```tsx
import { SendMailOSProvider, SubscribeForm } from '@sendmailos/sdk/react';

function App() {
  return (
    <SendMailOSProvider publicKey="pk_live_...">
      <SubscribeForm
        tags={['newsletter']}
        onSuccess={() => console.log('Subscribed!')}
        buttonText="Join Newsletter"
      />
    </SendMailOSProvider>
  );
}
```

### Custom Form with Hook

```tsx
import { useSubscribe } from '@sendmailos/sdk/react';

function CustomForm() {
  const { subscribe, isLoading, error, isSuccess } = useSubscribe();
  const [email, setEmail] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    await subscribe({ email, tags: ['newsletter'] });
  };

  if (isSuccess) return <p>Thanks for subscribing!</p>;

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <button disabled={isLoading}>
        {isLoading ? 'Subscribing...' : 'Subscribe'}
      </button>
      {error && <p>{error.message}</p>}
    </form>
  );
}
```

## Webhook Verification

```typescript
import { verifyWebhookSignature } from '@sendmailos/sdk';

app.post('/webhooks', (req, res) => {
  const isValid = verifyWebhookSignature({
    payload: JSON.stringify(req.body),
    signature: req.headers['x-sendmailos-signature'],
    timestamp: req.headers['x-sendmailos-timestamp'],
    secret: process.env.WEBHOOK_SECRET
  });

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Process webhook event
  const event = req.body;
  console.log('Event:', event.type);

  res.status(200).json({ received: true });
});
```

## Error Handling

```typescript
import { SendMailOS, SendMailOSError, RateLimitError } from '@sendmailos/sdk';

try {
  await client.emails.send({ ... });
} catch (error) {
  if (error instanceof RateLimitError) {
    console.log(`Rate limited. Retry after ${error.retryAfter}s`);
  } else if (error instanceof SendMailOSError) {
    console.log(`Error: ${error.message} (${error.code})`);
  }
}
```

## Security

- **Never expose secret keys** (`sk_*`) in client-side code
- Use **public keys** (`pk_*`) for React components
- Verify webhook signatures to prevent spoofing
- Use environment variables for API keys

## License

MIT