> ## Documentation Index
> Fetch the complete documentation index at: https://docs.monigo.co/llms.txt
> Use this file to discover all available pages before exploring further.

# JavaScript SDK

> The official JavaScript/TypeScript client library for the Monigo API. Zero dependencies, works in Node.js, browsers, and edge runtimes.

The Monigo JavaScript SDK (`@monigo/sdk`) is a thin, zero-dependency wrapper around the Monigo REST API. It follows a Stripe-style resource pattern — each API resource group is a typed service object hanging off the client. It ships as dual ESM and CommonJS bundles and works natively in Node.js (≥ 18), browsers, Cloudflare Workers, Bun, and Deno.

## Installation

```bash theme={null}
npm install @monigo/sdk
```

Or with other package managers:

```bash theme={null}
yarn add @monigo/sdk
pnpm add @monigo/sdk
bun add @monigo/sdk
```

Requires **Node.js 18 or later** for native `fetch` support. For Node.js 16, pass a fetch polyfill via the `fetch` option — see [Client configuration](#client-configuration) below.

## Quick start

```ts theme={null}
import { MonigoClient, Aggregation, BillingPeriod, PricingModel } from '@monigo/sdk'

const client = new MonigoClient({ apiKey: process.env.MONIGO_API_KEY! })

// Create a customer
const customer = await client.customers.create({
  external_id: 'user_abc123',
  name: 'Acme Corp',
  email: 'billing@acme.com',
})

// Ingest a usage event
await client.events.ingest({
  events: [
    {
      event_name: 'api_call',
      customer_id: customer.id,
      idempotency_key: 'evt_20260201_001',
      timestamp: new Date(),
      properties: { endpoint: '/v1/data', region: 'us-east-1' },
    },
  ],
})

console.log(`Customer ${customer.id} created and event ingested`)
```

<Tip>
  Use a test-mode API key (`mk_test_...`) during development. Test events are isolated from live data and won't trigger real charges.
</Tip>

## Client configuration

```ts theme={null}
const client = new MonigoClient({
  apiKey: 'mk_live_...',              // required
  baseURL: 'https://api.monigo.co',   // optional — defaults to production
  timeout: 30_000,                     // optional — request timeout in ms
  fetch: customFetch,                  // optional — custom fetch implementation
})
```

### Custom base URL

For self-hosted deployments or a local development server:

```ts theme={null}
const client = new MonigoClient({
  apiKey: process.env.MONIGO_API_KEY!,
  baseURL: 'http://localhost:8000',
})
```

### Polyfilling fetch (Node.js 16)

```ts theme={null}
import fetch from 'node-fetch'

const client = new MonigoClient({
  apiKey: process.env.MONIGO_API_KEY!,
  fetch: fetch as unknown as typeof globalThis.fetch,
})
```

### Testing

The `fetch` option makes the client fully testable without a real server:

```ts theme={null}
const client = new MonigoClient({
  apiKey: 'test_key',
  fetch: (_url, _init) =>
    Promise.resolve(new Response(JSON.stringify({ customer: mockCustomer }))),
})
```

## Idempotency

Every POST, PUT, and PATCH request to the Monigo API must include an `Idempotency-Key` header. The SDK handles this automatically — `crypto.randomUUID()` is called for each mutating request. To supply your own key (recommended for retryable operations), pass `idempotencyKey` in the optional options argument:

```ts theme={null}
import { MonigoClient } from '@monigo/sdk'

// Auto-generated key (default — safe for fire-and-forget)
const customer = await client.customers.create({
  external_id: 'user_abc123',
  name: 'Acme Corp',
  email: 'billing@acme.com',
})

// Explicit key — use a stable ID from your own system so retries are safe
const customer = await client.customers.create(
  {
    external_id: 'user_abc123',
    name: 'Acme Corp',
    email: 'billing@acme.com',
  },
  { idempotencyKey: `req_${yourRequestId}` },
)
```

<Tip>
  For critical mutations (subscription creation, invoice finalization) pass a key derived from your own request ID. Retrying with the same key is safe — the server will return the original response without applying the operation twice.
</Tip>

## Error handling

All methods return a `Promise`. A 4xx or 5xx response throws a `MonigoAPIError` with a typed status code, message, and optional field-level details.

```ts theme={null}
import { MonigoAPIError } from '@monigo/sdk'

try {
  const customer = await client.customers.get('cust_missing')
} catch (err) {
  if (err instanceof MonigoAPIError) {
    console.error(`API error ${err.statusCode}: ${err.message}`)
  }
}
```

### Static type-narrowing guards

```ts theme={null}
try {
  await client.subscriptions.create({ customer_id: 'c', plan_id: 'p' })
} catch (err) {
  if (MonigoAPIError.isConflict(err)) {
    // customer already has an active subscription of this plan type
  } else if (MonigoAPIError.isNotFound(err)) {
    // customer or plan not found
  } else if (MonigoAPIError.isRateLimited(err)) {
    // back off and retry
  } else {
    throw err
  }
}
```

| Guard                                 | HTTP Status | When it fires                      |
| ------------------------------------- | ----------- | ---------------------------------- |
| `MonigoAPIError.isNotFound(err)`      | 404         | Resource does not exist            |
| `MonigoAPIError.isUnauthorized(err)`  | 401         | Invalid or missing API key         |
| `MonigoAPIError.isForbidden(err)`     | 403         | API key lacks required scope       |
| `MonigoAPIError.isConflict(err)`      | 409         | Duplicate resource                 |
| `MonigoAPIError.isRateLimited(err)`   | 429         | Request rate limit exceeded        |
| `MonigoAPIError.isQuotaExceeded(err)` | 402         | Organisation event quota exhausted |
| `MonigoAPIError.isServerError(err)`   | 5xx         | Unexpected server-side error       |

## Events

The `client.events` service handles usage event ingestion and event replay.

### Ingest events

```ts theme={null}
const response = await client.events.ingest({
  events: [
    {
      event_name: 'api_call',
      customer_id: 'cust_abc',
      idempotency_key: 'evt_20260201_001',
      timestamp: new Date(),
      properties: {
        endpoint: '/v1/predict',
        region: 'us-east-1',
        tokens: 1500,
      },
    },
  ],
})

console.log('ingested:', response.ingested)
console.log('duplicates:', response.duplicates)
```

The server deduplicates events by `idempotency_key`. A single call can include up to **1,000 events**.

<Tip>
  Always supply a stable `idempotency_key` so retries after a network error don't double-count events. A good key is `${customer_id}_${event_type}_${request_id}`.
</Tip>

### Replay events

Replay reprocesses all events in a time window to correct rollups after an outage or metric definition change.

```ts theme={null}
let job = await client.events.startReplay({
  from: new Date('2026-01-01'),
  to: new Date('2026-01-31'),
  event_name: 'api_call', // omit to replay all event names
})

// Poll until complete
while (job.status === 'pending' || job.status === 'processing') {
  await new Promise(r => setTimeout(r, 5_000))
  job = await client.events.getReplay(job.id)
  console.log(`replayed ${job.events_replayed} / ${job.events_total}`)
}
```

## Customers

```ts theme={null}
// Create
const customer = await client.customers.create({
  external_id: 'user_123',
  name: 'Acme Corp',
  email: 'billing@acme.com',
})

// List
const { customers, count } = await client.customers.list()
console.log(`${count} customers`)

// Get
const customer = await client.customers.get('cust_abc')

// Update
const updated = await client.customers.update('cust_abc', {
  name: 'Acme Corp Ltd',
})

// Delete
await client.customers.delete('cust_abc')
```

<Tip>
  Set `external_id` to your own system's user ID. This lets you look up customers by your existing identifier without storing Monigo's UUID separately.
</Tip>

## Metrics

A metric defines what gets counted and how raw event values are aggregated.

```ts theme={null}
import { Aggregation } from '@monigo/sdk'

// Count events
const metric = await client.metrics.create({
  name: 'API Calls',
  event_name: 'api_call',
  aggregation: Aggregation.Count,
})

// Sum a numeric property
const metric = await client.metrics.create({
  name: 'Tokens Used',
  event_name: 'completion',
  aggregation: Aggregation.Sum,
  aggregation_property: 'tokens',
})
```

| Constant              | Value     | Description                          |
| --------------------- | --------- | ------------------------------------ |
| `Aggregation.Count`   | `count`   | Count the number of matching events  |
| `Aggregation.Sum`     | `sum`     | Sum a numeric property across events |
| `Aggregation.Max`     | `max`     | Maximum value of a property          |
| `Aggregation.Min`     | `minimum` | Minimum value of a property          |
| `Aggregation.Average` | `average` | Average value of a property          |
| `Aggregation.Unique`  | `unique`  | Count distinct values of a property  |

## Plans

A plan combines billing period, currency, and one or more prices. Each price links a metric to a pricing model.

```ts theme={null}
import { BillingPeriod, PricingModel } from '@monigo/sdk'

// Flat rate: ₦50 per API call
const plan = await client.plans.create({
  name: 'Starter',
  currency: 'NGN',
  billing_period: BillingPeriod.Monthly,
  prices: [
    {
      metric_id: 'metric_api_calls',
      model: PricingModel.Flat,
      unit_price: '50.000000',
    },
  ],
})

// Tiered pricing
const plan = await client.plans.create({
  name: 'Growth',
  currency: 'NGN',
  billing_period: BillingPeriod.Monthly,
  prices: [
    {
      metric_id: 'metric_api_calls',
      model: PricingModel.Tiered,
      tiers: [
        { up_to: 10_000, unit_amount: '5.00' },
        { up_to: 100_000, unit_amount: '3.00' },
        { up_to: null, unit_amount: '1.50' }, // null = infinity
      ],
    },
  ],
})
```

### Pricing models

| Constant                      | Value             | Description                                    |
| ----------------------------- | ----------------- | ---------------------------------------------- |
| `PricingModel.Flat`           | `flat`            | Fixed price per unit                           |
| `PricingModel.Tiered`         | `tiered`          | Graduated tiers — each unit priced at its tier |
| `PricingModel.Volume`         | `volume`          | Whole volume priced at one tier                |
| `PricingModel.Package`        | `package`         | Price per block of N units                     |
| `PricingModel.Overage`        | `overage`         | Base allowance + per-unit above threshold      |
| `PricingModel.WeightedTiered` | `weighted_tiered` | Average price across graduated tiers           |

### Plan types and billing periods

| Constant                  | Value        | Meaning              |
| ------------------------- | ------------ | -------------------- |
| `PlanType.Collection`     | `collection` | Charge customers     |
| `PlanType.Payout`         | `payout`     | Pay out to vendors   |
| `BillingPeriod.Daily`     | `daily`      | Billed every day     |
| `BillingPeriod.Weekly`    | `weekly`     | Billed every week    |
| `BillingPeriod.Monthly`   | `monthly`    | Billed every month   |
| `BillingPeriod.Quarterly` | `quarterly`  | Billed every quarter |
| `BillingPeriod.Annually`  | `annually`   | Billed every year    |

## Subscriptions

A subscription links a customer to a plan and defines the current billing period.

```ts theme={null}
import { SubscriptionStatus } from '@monigo/sdk'

// Create
const sub = await client.subscriptions.create({
  customer_id: 'cust_abc',
  plan_id: 'plan_starter',
})

// List with filters
const { subscriptions } = await client.subscriptions.list({
  customer_id: 'cust_abc',
  status: SubscriptionStatus.Active,
})

// Get
const sub = await client.subscriptions.get('sub_abc')

// Pause / resume
await client.subscriptions.updateStatus('sub_abc', SubscriptionStatus.Paused)
await client.subscriptions.updateStatus('sub_abc', SubscriptionStatus.Active)

// Cancel
await client.subscriptions.delete('sub_abc')
```

| Status constant               | Value      | Meaning                               |
| ----------------------------- | ---------- | ------------------------------------- |
| `SubscriptionStatus.Active`   | `active`   | Billing is running                    |
| `SubscriptionStatus.Paused`   | `paused`   | Billing paused; events still accepted |
| `SubscriptionStatus.Canceled` | `canceled` | Subscription ended                    |

<Warning>
  A customer can hold at most one active subscription per plan type. A second active **collection** or **payout** subscription returns a 409 — catch it with `MonigoAPIError.isConflict(err)`.
</Warning>

## Payout accounts

Payout accounts are bank or mobile-money accounts scoped to a customer. All methods take `customerId` as the first argument.

```ts theme={null}
import { PayoutMethod } from '@monigo/sdk'

// Bank transfer
const account = await client.payoutAccounts.create('cust_abc', {
  account_name: 'John Driver',
  payout_method: PayoutMethod.BankTransfer,
  bank_name: 'First Bank Nigeria',
  bank_code: '011',
  account_number: '3001234567',
  currency: 'NGN',
  is_default: true,
})

// Mobile money
const account = await client.payoutAccounts.create('cust_abc', {
  account_name: 'John Driver',
  payout_method: PayoutMethod.MobileMoney,
  mobile_money_number: '+2348012345678',
  currency: 'NGN',
})

// List / get / update / delete
const { payout_accounts } = await client.payoutAccounts.list('cust_abc')
const acct = await client.payoutAccounts.get('cust_abc', 'acct_xyz')
await client.payoutAccounts.update('cust_abc', 'acct_xyz', { account_name: 'Jane Driver' })
await client.payoutAccounts.delete('cust_abc', 'acct_xyz')
```

| Method constant             | Value           |
| --------------------------- | --------------- |
| `PayoutMethod.BankTransfer` | `bank_transfer` |
| `PayoutMethod.MobileMoney`  | `mobile_money`  |

## Invoices

Invoices are generated from subscriptions and contain line items derived from the customer's usage in the billing period.

```ts theme={null}
import { InvoiceStatus } from '@monigo/sdk'

// Generate a draft invoice
const invoice = await client.invoices.generate('sub_abc')
console.log(`Draft invoice ${invoice.id} — total: ${invoice.total} ${invoice.currency}`)

for (const item of invoice.line_items ?? []) {
  console.log(`  ${item.description}: qty ${item.quantity} × ${item.unit_price} = ${item.amount}`)
}

// List with filters
const { invoices } = await client.invoices.list({
  customer_id: 'cust_abc',
  status: InvoiceStatus.Draft,
})

// Get / finalize / void
const invoice = await client.invoices.get('inv_abc')
const finalized = await client.invoices.finalize('inv_abc')
const voided = await client.invoices.void('inv_abc')
```

| Status constant           | Value       | Meaning                               |
| ------------------------- | ----------- | ------------------------------------- |
| `InvoiceStatus.Draft`     | `draft`     | Not yet finalized; amounts may change |
| `InvoiceStatus.Finalized` | `finalized` | Locked and ready for payment          |
| `InvoiceStatus.Paid`      | `paid`      | Payment collected                     |
| `InvoiceStatus.Void`      | `void`      | Cancelled                             |

<Info>
  All monetary amounts (`subtotal`, `total`, `unit_price`) are returned as decimal strings (e.g. `"1500.00"`) to preserve precision across currencies.
</Info>

## Usage

Query aggregated usage rollups to see how much a customer has consumed in a period.

```ts theme={null}
// All usage for the current period
const result = await client.usage.query()

// Filter by customer and metric
const result = await client.usage.query({
  customer_id: 'cust_abc',
  metric_id: 'metric_api_calls',
})

// Custom time window — accepts Date objects or ISO strings
const result = await client.usage.query({
  customer_id: 'cust_abc',
  from: new Date('2026-01-01'),
  to: new Date('2026-01-31'),
})

for (const rollup of result.rollups) {
  console.log(
    `customer ${rollup.customer_id} — ${rollup.aggregation}: ${rollup.value}`,
    `(period: ${rollup.period_start} → ${rollup.period_end})`
  )
}
```

## Portal tokens

Portal tokens grant an end-customer read-only access to their invoices, payout slips, subscriptions, and payout accounts in the Monigo hosted portal. The `portal_url` on the returned token is what you share with your customer — embed it in an email, redirect the browser, or open it inside an iframe.

### Create a portal link

```ts theme={null}
const token = await client.portalTokens.create({
  customer_external_id: 'usr_abc123',
  label: 'Invoice portal link',
})

// Share with your customer
sendEmail(customer.email, { link: token.portal_url })
```

Set `expires_at` (ISO 8601) for a time-limited link:

```ts theme={null}
const expiry = new Date()
expiry.setDate(expiry.getDate() + 30)

const token = await client.portalTokens.create({
  customer_external_id: 'usr_abc123',
  label: '30-day invoice link',
  expires_at: expiry.toISOString(),
})
```

### List tokens for a customer

```ts theme={null}
const { tokens, count } = await client.portalTokens.list('usr_abc123')

for (const tok of tokens) {
  console.log(`${tok.id} — ${tok.label} (expires: ${tok.expires_at ?? 'never'})`)
}
```

`customerId` accepts either a Monigo UUID or the customer's `external_id`.

### Revoke a token

```ts theme={null}
await client.portalTokens.revoke(token.id)
```

Revocation is immediate. Any customer holding the corresponding URL will receive a 401 on their next request.

<Tip>
  Portal tokens are opaque 64-character hex strings stored in the database — not JWTs — so they can be instantly revoked without waiting for an expiry timestamp.
</Tip>

## Wallets

Customer wallets hold prepaid balances. They are the foundation of [Prepaid Billing](/guides/prepaid-billing) and can also be used for any credit/debit workflow. See the full [Wallet Management](/guides/wallet-management) guide for details.

```ts theme={null}
import { WalletEntryType, VirtualAccountProvider, MonigoAPIError } from '@monigo/sdk'

// Get or create a wallet
const wallet = await client.wallets.getOrCreate({
  customer_id: 'cust_abc',
  currency: 'NGN',
})

// List all wallets (org inferred from API key)
const all = await client.wallets.list()

// List wallets filtered by customer
const filtered = await client.wallets.list({ customer_id: 'cust_abc' })

// Or use the dedicated customer endpoint
const { wallets } = await client.wallets.listByCustomer('cust_abc')

// Get a wallet (includes virtual accounts)
const detail = await client.wallets.get('wallet_uuid')
console.log(`Balance: ${detail.wallet.balance}, VAs: ${detail.virtual_accounts.length}`)

// Credit (top up)
const creditResp = await client.wallets.credit(wallet.id, {
  amount: '5000.00',
  currency: 'NGN',
  description: 'Payment received',
  entry_type: WalletEntryType.Deposit,
  reference_type: 'payment',
  reference_id: 'pay_123',
  idempotency_key: 'topup_pay_123',
})

// Debit (charge) — throws 402 if balance insufficient
try {
  const debitResp = await client.wallets.debit(wallet.id, {
    amount: '1200.00',
    currency: 'NGN',
    description: 'Usage charge',
    entry_type: WalletEntryType.Usage,
    reference_type: 'invoice',
    reference_id: 'inv_456',
    idempotency_key: 'debit_inv_456',
  })
} catch (err) {
  if (err instanceof MonigoAPIError && err.statusCode === 402) {
    console.log('Insufficient balance')
  }
}

// Transaction history
const txns = await client.wallets.listTransactions(wallet.id, { limit: 25 })

// Virtual accounts
const va = await client.wallets.createVirtualAccount(wallet.id, {
  provider: VirtualAccountProvider.Paystack,
  currency: 'NGN',
})
const vaList = await client.wallets.listVirtualAccounts(wallet.id)
```

### Wallet constants

| Constant                             | Value         | Description                   |
| ------------------------------------ | ------------- | ----------------------------- |
| `WalletEntryType.Deposit`            | `deposit`     | Customer top-up               |
| `WalletEntryType.Withdrawal`         | `withdrawal`  | Customer withdrawal           |
| `WalletEntryType.Usage`              | `usage`       | Metered usage charge          |
| `WalletEntryType.Refund`             | `refund`      | Reversal of a previous charge |
| `WalletEntryType.Adjustment`         | `adjustment`  | Manual correction             |
| `VirtualAccountProvider.Paystack`    | `paystack`    | Paystack virtual account      |
| `VirtualAccountProvider.Flutterwave` | `flutterwave` | Flutterwave virtual account   |
| `VirtualAccountProvider.Monnify`     | `monnify`     | Monnify virtual account       |

<Info>
  All wallet amounts (`balance`, `reserved_balance`, ledger `amount`) are decimal strings to preserve precision.
</Info>

## Example programs

The SDK ships with seven runnable example programs under `examples/`:

| Program         | What it demonstrates                                                    |
| --------------- | ----------------------------------------------------------------------- |
| `quickstart`    | End-to-end: customer → metric → plan → subscription → ingest            |
| `metering`      | High-volume batch ingest with idempotency and rate-limit retry          |
| `billing`       | Generate, inspect, and finalize an invoice                              |
| `payouts`       | Register payout accounts and trigger event replay                       |
| `usage-report`  | Query rollups and print a terminal usage table                          |
| `portal-tokens` | Create, list, and revoke customer portal access links                   |
| `wallets`       | Wallet lifecycle: create, credit, debit, transactions, virtual accounts |

Run any example:

```bash theme={null}
cd js-sdk/examples
npm install
MONIGO_API_KEY=mk_test_... npm run portal-tokens
```

Each example reads `MONIGO_API_KEY` from the environment and accepts an optional `MONIGO_BASE_URL` to point at a local server.

## Testing

The SDK ships 98 unit tests with zero external dependencies. Each test uses a mock `fetch` implementation injected via the `fetch` option:

```bash theme={null}
cd js-sdk
npm run test:run
```

To test against a local Monigo server:

```ts theme={null}
const client = new MonigoClient({
  apiKey: 'mk_test_...',
  baseURL: 'http://localhost:8000',
})
```
