npm package: @netiva-ai/sdk
Description: Official Node.js SDK for Netiva
Author: Kelvin Cobanaj
Latest version: 0.0.6

# @netiva-ai/sdk

Official Node.js SDK for Netiva.

## Installation

```bash
npm install @netiva-ai/sdk
```

## Quick Start

```typescript
import { Netiva } from '@netiva-ai/sdk'

const netiva = new Netiva({
  workspaceId: process.env.NETIVA_WORKSPACE_ID!,
  apiKey: process.env.NETIVA_API_KEY!,  // Format: nsk_xxx
})
```

## Customers

```typescript
// Create with metadata (store your internal IDs for mapping)
const customer = await netiva.customers.create({
  email: 'john@acme.com',
  firstName: 'John',
  lastName: 'Doe',
  metadata: {
    userUid: 'your-internal-user-id',
    organizationUid: 'your-internal-org-id',
  },
})
// Returns: { id, email, firstName, lastName, metadata }

// Get customer
const found = await netiva.customers.get(customer.id)

// List customers
const { data, meta } = await netiva.customers.list({ search: 'john', page: 1, limit: 25 })

// Update customer
const updated = await netiva.customers.update(customer.id, { firstName: 'Jane' })

// Delete customer
await netiva.customers.delete(customer.id)

// List organizations a customer belongs to
const orgs = await netiva.customers.listOrganizations(customer.id)
// Returns: Organization[] - all orgs where the customer is a member
```

## Organizations

```typescript
// Create with metadata
const org = await netiva.organizations.create({
  name: 'Acme Corp',
  domain: 'acme-corp',  // slug/key
  metadata: {
    organizationUid: 'your-internal-org-id',
  },
})
// Returns: { id, name, domain, metadata }

// Get organization
const found = await netiva.organizations.get(org.id)

// List organizations
const { data } = await netiva.organizations.list()

// Update organization
const updated = await netiva.organizations.update(org.id, { name: 'Acme Inc' })

// Delete organization
await netiva.organizations.delete(org.id)

// Add member to organization
await netiva.organizations.addMember(org.id, {
  customerId: customer.id,
  role: 'admin',  // or 'member'
})

// Remove member
await netiva.organizations.removeMember(org.id, customer.id)
```

## Add-ons

Manage add-ons (fixed, quantity-based, or metered) for organization subscriptions:

```typescript
// List all available add-ons for the workspace
const { addons } = await netiva.addons.list()
// Returns: { addons: [{ id, name, description, type, unitLabel, prices, ... }] }

// List active add-ons for an organization
const { addons: orgAddons } = await netiva.addons.listForOrganization(org.id)
// Returns: { addons: [{ subscriptionItemId, name, type, quantity, price, ... }] }

// Add an add-on to an organization's subscription
const result = await netiva.addons.add(org.id, {
  priceId: 'price_xxx',
  quantity: 5,  // optional, defaults to 1 for fixed/quantity types
})
// Returns: { success, subscriptionItem: { id, priceId, quantity } }

// Update add-on quantity
await netiva.addons.updateQuantity(org.id, 'si_xxx', 10)
// Returns: { success, subscriptionItem: { id, quantity } }

// Remove an add-on
await netiva.addons.remove(org.id, 'si_xxx')
// Returns: { success }
```

### Metered Usage

For metered add-ons, report usage and retrieve summaries:

```typescript
// Report metered usage
await netiva.addons.reportUsage(org.id, {
  subscriptionItemId: 'si_xxx',
  quantity: 100,
  timestamp: Math.floor(Date.now() / 1000),  // optional, unix timestamp in seconds, defaults to now
  action: 'increment',  // optional: 'increment' (default) or 'set'
})
// Returns: { success, usageRecord: { id, quantity, timestamp } }

// Get usage summary for current billing period
const { usage } = await netiva.addons.getUsage(org.id, 'si_xxx')
// Returns: { usage: { subscriptionItemId, totalUsage, period: { start, end } } }
```

## Magic Links

Generate links for billing portal access. Use for both checkout and subscription management:

### Checkout Flow (New Subscriptions)

Redirect users to select a plan and enter payment:

```typescript
// Option 1: Let customer browse all plans
const { link } = await netiva.magicLink.generate({
  customerId: customer.id,
  organizationId: org.id,
  returnUrl: 'https://app.example.com/dashboard?billing=complete',
  redirectTo: '/plans',
})

// Option 2: Skip plan selection, go directly to checkout
const { link } = await netiva.magicLink.generate({
  customerId: customer.id,
  organizationId: org.id,
  returnUrl: 'https://app.example.com/dashboard?billing=complete',
  planId: 'prod_xxx',  // Stripe Product ID - bypasses /plans, goes straight to checkout
})

// Redirect user to checkout
res.redirect(link)
```

**Checkout Flow:**
1. User signs up on your app
2. You create customer + org in Netiva
3. You redirect user to magic link (with `redirectTo: '/plans'`)
4. User selects plan, enters payment
5. Netiva redirects back to returnUrl
6. Webhook `subscription.created` fires

### Manage Subscription

Generate links for "Manage Subscription" button - logs user into billing portal:

```typescript
const { link } = await netiva.magicLink.generate({
  customerId: customer.id,
  returnUrl: 'https://app.example.com/settings/billing',
})

// Redirect user to manage their subscription
res.redirect(link)
```

### Parameters

| Parameter | Required | Description |
|-----------|----------|-------------|
| `customerId` | Yes | Customer to authenticate |
| `organizationId` | No | Organization context |
| `returnUrl` | No | URL to return to after portal session |
| `expiresInMinutes` | No | Link expiry (default: 15 minutes) |
| `redirectTo` | No | Portal destination: `/plans`, `/payment-methods`, `/invoices`, `/addons` |
| `planId` | No | Skip plan selection, go directly to checkout for this plan |

## SSO Verification

When customers click "Launch App" in the portal, verify the token:

```typescript
app.get('/auth/sso', (req, res) => {
  const result = netiva.sso.verify(req.query.token as string)

  if (!result.valid) {
    return res.status(401).json({ error: result.error })
  }

  // Authenticated! result.customer contains { id, email, firstName, lastName }
  req.session.userId = result.customer!.id
  res.redirect('/dashboard')
})
```

## Webhooks

Verify incoming webhooks:

```typescript
app.post('/webhooks/netiva', express.raw({ type: 'application/json' }), (req, res) => {
  const result = netiva.webhooks.verify(req.body, req.headers)

  if (!result.valid) {
    return res.status(401).json({ error: result.error })
  }

  const { event, data } = result.payload!
  
  switch (event) {
    case 'customer.created':
      // data: { id, email, metadata }
      break
    case 'subscription.created':
      // data: { subscriptionId, customerId, organizationId, planId, planName, status, currentPeriodEnd, metadata }
      break
    case 'subscription.updated':
      // data: { subscriptionId, customerId, organizationId, planId, currentPeriodEnd, metadata }
      break
    case 'subscription.cancelled':
      // data: { subscriptionId, organizationId, cancelledAt, metadata }
      break
    case 'subscription.reactivated':
      // data: { subscriptionId, organizationId, metadata }
      break
    case 'subscription.upgraded':
    case 'subscription.downgraded':
      // data: { subscriptionId, organizationId, previousPlanId, newPlanId, metadata }
      break
    case 'invoice.payment_succeeded':
      // data: { invoiceId, subscriptionId, organizationId, amount, currency, metadata }
      break
    case 'invoice.payment_failed':
      // data: { invoiceId, subscriptionId, organizationId, failureReason, metadata }
      break
    case 'addon.added':
      // data: { subscriptionItemId, priceId, quantity, organizationId, metadata }
      break
    case 'addon.removed':
      // data: { subscriptionItemId, organizationId, metadata }
      break
    case 'addon.quantity_updated':
      // data: { subscriptionItemId, quantity, organizationId, metadata }
      break
    case 'addon.usage_reported':
      // data: { subscriptionItemId, quantity, organizationId, metadata }
      break
  }

  res.json({ received: true })
})
```

### Webhook Events

| Event | Description |
|-------|-------------|
| `customer.created` | New customer registered in the members portal |
| `lead.created` | New lead captured from a landing page form |
| `subscription.created` | Customer subscribed to a plan |
| `subscription.updated` | Subscription details changed (e.g., quantity) |
| `subscription.cancelled` | Customer cancelled their subscription |
| `subscription.reactivated` | Cancelled subscription was reactivated |
| `subscription.upgraded` | Customer upgraded to a higher plan |
| `subscription.downgraded` | Customer downgraded to a lower plan |
| `invoice.payment_succeeded` | Payment was successfully processed |
| `invoice.payment_failed` | Payment failed (card declined, expired, etc.) |
| `addon.added` | Add-on added to an organization's subscription |
| `addon.removed` | Add-on removed from an organization's subscription |
| `addon.quantity_updated` | Add-on quantity changed |
| `addon.usage_reported` | Metered usage reported for an add-on |

**Metadata Propagation:** Metadata passed when creating customers/orgs is included in webhook payloads.

## Error Handling

```typescript
import { NetivaApiError, NetivaNetworkError, NetivaTimeoutError } from '@netiva-ai/sdk'

try {
  await netiva.customers.get('invalid-id')
} catch (error) {
  if (error instanceof NetivaApiError) {
    console.error(error.message, error.statusCode, error.errorCode)
  }
}
```

## Configuration Options

| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `workspaceId` | Yes | — | Your Netiva workspace ID |
| `apiKey` | Yes | — | API key (format: `nsk_xxx`) |
| `baseUrl` | No | `https://api.netiva.ai` | API base URL |
| `timeout` | No | `30000` | Request timeout (ms) |
| `retries` | No | `0` | Retry attempts |

## License

MIT