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.
Wallets
Customer wallets hold prepaid balances. They are the foundation of Prepaid Billing and can also be used for any credit/debit workflow. See the full Wallet Management guide for details.
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 |
All wallet amounts (balance, reserved_balance, ledger amount) are decimal strings to preserve precision.
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:
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',
})