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
Or with other package managers:
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 below.
Quick start
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`)
Use a test-mode API key (mk_test_...) during development. Test events are isolated from live data and won’t trigger real charges.
Client configuration
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:
const client = new MonigoClient({
apiKey: process.env.MONIGO_API_KEY!,
baseURL: 'http://localhost:8000',
})
Polyfilling fetch (Node.js 16)
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:
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:
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}` },
)
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.
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.
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
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
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.
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}.
Replay events
Replay reprocesses all events in a time window to correct rollups after an outage or metric definition change.
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
// 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')
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.
Metrics
A metric defines what gets counted and how raw event values are aggregated.
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.
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.
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 |
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).
Payout accounts
Payout accounts are bank or mobile-money accounts scoped to a customer. All methods take customerId as the first argument.
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.
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 |
All monetary amounts (subtotal, total, unit_price) are returned as decimal strings (e.g. "1500.00") to preserve precision across currencies.
Usage
Query aggregated usage rollups to see how much a customer has consumed in a period.
// 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
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:
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
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
await client.portalTokens.revoke(token.id)
Revocation is immediate. Any customer holding the corresponding URL will receive a 401 on their next request.
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.
Example programs
The SDK ships with six 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 |
Run any example:
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:
cd js-sdk
npm run test:run
To test against a local Monigo server:
const client = new MonigoClient({
apiKey: 'mk_test_...',
baseURL: 'http://localhost:8000',
})