> ## 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.

# Go SDK

> The official Go client library for the Monigo API. Zero dependencies, idiomatic design.

The Monigo Go SDK (`github.com/monigo-africa/go-monigo`) is a thin, zero-dependency wrapper around the Monigo REST API. It follows the same resource-based structure as the REST API and uses functional options for configuration — familiar to anyone who has used the Stripe or Plaid Go SDKs.

## Installation

```bash theme={null}
go get github.com/monigo-africa/go-monigo
```

Requires Go 1.25 or later. No third-party dependencies — the SDK uses only the Go standard library.

## Quick start

```go theme={null}
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	monigo "github.com/monigo-africa/go-monigo"
)

func main() {
	client := monigo.New(os.Getenv("MONIGO_API_KEY"))
	ctx := context.Background()

	// Create a customer
	customer, err := client.Customers.Create(ctx, monigo.CreateCustomerRequest{
		ExternalID: "user_abc123",
		Name:       "Acme Corp",
		Email:      "billing@acme.com",
	})
	if err != nil {
		log.Fatal(err)
	}

	// Ingest a usage event
	_, err = client.Events.Ingest(ctx, monigo.IngestRequest{
		Events: []monigo.IngestEvent{
			{
				EventName:      "api_call",
				CustomerID:     customer.ID,
				IdempotencyKey: "evt_unique_id_001",
				Timestamp:      time.Now(),
				Properties:     map[string]any{"endpoint": "/v1/data"},
			},
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Customer %s created and event ingested\n", customer.ID)
}
```

<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

### Default client

```go theme={null}
client := monigo.New(os.Getenv("MONIGO_API_KEY"))
```

The default client sends requests to `https://api.monigo.co` with a standard `http.Client` and no timeout. For production use, always set a timeout.

### Custom HTTP client (recommended)

```go theme={null}
import "net/http"

httpClient := &http.Client{Timeout: 30 * time.Second}

client := monigo.New(
	os.Getenv("MONIGO_API_KEY"),
	monigo.WithHTTPClient(httpClient),
)
```

### Custom base URL

For self-hosted deployments or a local development server:

```go theme={null}
client := monigo.New(
	os.Getenv("MONIGO_API_KEY"),
	monigo.WithBaseURL("https://api.yourcompany.com"),
)
```

## Idempotency

Every POST, PUT, and PATCH request to the Monigo API must include an `Idempotency-Key` header. The SDK handles this automatically — a UUID v4 is generated for each request. To supply your own key (recommended for retryable operations), pass `WithIdempotencyKey`:

```go theme={null}
// Auto-generated key (default — safe for fire-and-forget)
customer, err := client.Customers.Create(ctx, monigo.CreateCustomerRequest{
    ExternalID: "user_abc123",
    Name:       "Acme Corp",
    Email:      "billing@acme.com",
})

// Explicit key — use a stable ID from your own system so retries are safe
customer, err := client.Customers.Create(
    ctx,
    monigo.CreateCustomerRequest{
        ExternalID: "user_abc123",
        Name:       "Acme Corp",
        Email:      "billing@acme.com",
    },
    monigo.WithIdempotencyKey("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 standard `error`. When the API responds with a 4xx or 5xx status, the error is an `*APIError` with a status code, message, and optional field-level details.

```go theme={null}
customer, err := client.Customers.Get(ctx, "cust_missing")
if err != nil {
	var apiErr *monigo.APIError
	if errors.As(err, &apiErr) {
		fmt.Printf("API error %d: %s\n", apiErr.StatusCode, apiErr.Message)
	}
	return err
}
```

### Sentinel helpers

Instead of inspecting status codes manually, use the provided helpers:

```go theme={null}
_, err := client.Subscriptions.Create(ctx, req)
switch {
case monigo.IsConflict(err):
    // customer already has an active subscription of this plan type (collection or payout)
case monigo.IsNotFound(err):
    // customer or plan not found
case monigo.IsRateLimited(err):
    // back off and retry
case monigo.IsQuotaExceeded(err):
    // org has exhausted its event quota
case err != nil:
    // unexpected error
}
```

| Helper                   | HTTP Status | When it fires                                                                               |
| ------------------------ | ----------- | ------------------------------------------------------------------------------------------- |
| `IsNotFound(err)`        | 404         | Resource does not exist                                                                     |
| `IsUnauthorized(err)`    | 401         | Invalid or missing API key                                                                  |
| `IsForbidden(err)`       | 403         | API key lacks the required scope                                                            |
| `IsConflict(err)`        | 409         | Duplicate resource (e.g. customer already has an active subscription of the same plan type) |
| `IsRateLimited(err)`     | 429         | Request rate limit exceeded                                                                 |
| `IsQuotaExceeded(err)`   | 402         | Organisation event quota exhausted                                                          |
| `IsValidationError(err)` | 400         | Request body failed validation                                                              |

## Events

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

### Ingest events

```go theme={null}
resp, err := client.Events.Ingest(ctx, monigo.IngestRequest{
	Events: []monigo.IngestEvent{
		{
			EventName:      "api_call",
			CustomerID:     "cust_abc",
			IdempotencyKey: "evt_20260201_001",
			Timestamp:      time.Now(),
			Properties: map[string]any{
				"endpoint": "/v1/predict",
				"region":   "us-east-1",
			},
		},
	},
})
if err != nil {
    log.Fatal(err)
}
fmt.Printf("ingested: %v, duplicates: %v\n", resp.Ingested, resp.Duplicates)
```

The server deduplicates events by `idempotency_key`. Sending the same key twice is safe — the second call will appear in `resp.Duplicates` and won't be counted again.

A single `Ingest` call can contain up to 1,000 events.

### Replay events

Replay reprocesses all events in a time window through the metering pipeline. This corrects rollups after an outage or after a metric definition change.

```go theme={null}
from := time.Now().Add(-24 * time.Hour)
to := time.Now()
eventName := "api_call" // pass nil to replay all event names

job, err := client.Events.StartReplay(ctx, from, to, &eventName)
if err != nil {
    log.Fatal(err)
}

// Poll until complete
for job.Status == "pending" || job.Status == "processing" {
    time.Sleep(5 * time.Second)
    job, err = client.Events.GetReplay(ctx, job.ID)
    if err != nil {
        log.Fatal(err)
    }
}
fmt.Printf("replayed %d / %d events\n", job.EventsReplayed, job.EventsTotal)
```

## Customers

```go theme={null}
// Create
customer, err := client.Customers.Create(ctx, monigo.CreateCustomerRequest{
	ExternalID: "user_123",
	Name:       "Acme Corp",
	Email:      "billing@acme.com",
})

// List
list, err := client.Customers.List(ctx)
fmt.Printf("%d customers\n", list.Count)

// Get
customer, err := client.Customers.Get(ctx, "cust_abc")

// Update
customer, err := client.Customers.Update(ctx, "cust_abc", monigo.UpdateCustomerRequest{
	Name: "Acme Corp Ltd",
})

// Delete
err = client.Customers.Delete(ctx, "cust_abc")
```

<Tip>
  Set `ExternalID` to your own system's user ID. This makes it easy to look up a customer without storing Monigo's UUID separately.
</Tip>

## Metrics

A metric defines what gets counted (e.g. "API calls", "GB stored") and how the raw event values are aggregated.

```go theme={null}
metric, err := client.Metrics.Create(ctx, monigo.CreateMetricRequest{
	Name:        "API Calls",
	EventName:   "api_call",
	Aggregation: monigo.AggregationCount,
})

// Sum a numeric property (e.g. bytes transferred)
metric, err := client.Metrics.Create(ctx, monigo.CreateMetricRequest{
	Name:                "Data Transferred",
	EventName:           "data_transfer",
	Aggregation:         monigo.AggregationSum,
	AggregationProperty: "bytes",
})
```

| Constant             | Value     | Description                          |
| -------------------- | --------- | ------------------------------------ |
| `AggregationCount`   | `count`   | Count the number of matching events  |
| `AggregationSum`     | `sum`     | Sum a numeric property across events |
| `AggregationMax`     | `max`     | Maximum value of a property          |
| `AggregationMin`     | `minimum` | Minimum value of a property          |
| `AggregationAverage` | `average` | Average value of a property          |
| `AggregationUnique`  | `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.

```go theme={null}
// Flat rate: $2.50 per API call
plan, err := client.Plans.Create(ctx, monigo.CreatePlanRequest{
	Name:          "Starter",
	Currency:      "USD",
	BillingPeriod: monigo.BillingPeriodMonthly,
	Prices: []monigo.CreatePriceRequest{
		{
			MetricID:  "metric_api_calls",
			Model:     monigo.PricingModelFlat,
			UnitPrice: "2.500000",
		},
	},
})

// Tiered pricing
plan, err := client.Plans.Create(ctx, monigo.CreatePlanRequest{
	Name:          "Growth",
	Currency:      "NGN",
	BillingPeriod: monigo.BillingPeriodMonthly,
	Prices: []monigo.CreatePriceRequest{
		{
			MetricID: "metric_api_calls",
			Model:    monigo.PricingModelTiered,
			Tiers: []monigo.PriceTier{
				{UpTo: ptr(int64(10_000)), UnitAmount: "5.00"},
				{UpTo: ptr(int64(100_000)), UnitAmount: "3.00"},
				{UpTo: nil, UnitAmount: "1.50"}, // nil = infinity
			},
		},
	},
})
```

### Pricing models

| Constant                     | Value             | Description                                    |
| ---------------------------- | ----------------- | ---------------------------------------------- |
| `PricingModelFlat`           | `flat`            | Fixed price per unit                           |
| `PricingModelTiered`         | `tiered`          | Graduated tiers — each unit priced at its tier |
| `PricingModelVolume`         | `volume`          | Whole volume priced at one tier                |
| `PricingModelPackage`        | `package`         | Price per block of N units                     |
| `PricingModelOverage`        | `overage`         | Base allowance + per-unit above threshold      |
| `PricingModelWeightedTiered` | `weighted_tiered` | Average price across graduated tiers           |

### Plan types and billing periods

| Constant                 | Value                           |
| ------------------------ | ------------------------------- |
| `PlanTypeCollection`     | `collection` — charge customers |
| `PlanTypePayout`         | `payout` — pay out to vendors   |
| `BillingPeriodDaily`     | `daily`                         |
| `BillingPeriodWeekly`    | `weekly`                        |
| `BillingPeriodMonthly`   | `monthly`                       |
| `BillingPeriodQuarterly` | `quarterly`                     |
| `BillingPeriodAnnually`  | `annually`                      |

## Subscriptions

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

```go theme={null}
// Create
sub, err := client.Subscriptions.Create(ctx, monigo.CreateSubscriptionRequest{
	CustomerID: "cust_abc",
	PlanID:     "plan_starter",
})

// List with filters
subs, err := client.Subscriptions.List(ctx, monigo.ListSubscriptionsParams{
	CustomerID: "cust_abc",
	Status:     monigo.SubscriptionStatusActive,
})

// Get
sub, err := client.Subscriptions.Get(ctx, "sub_abc")

// Pause / resume / cancel
sub, err = client.Subscriptions.UpdateStatus(ctx, "sub_abc", monigo.SubscriptionStatusPaused)
sub, err = client.Subscriptions.UpdateStatus(ctx, "sub_abc", monigo.SubscriptionStatusActive)

// Cancel (soft-delete)
err = client.Subscriptions.Delete(ctx, "sub_abc")
```

| Status constant              | Value      | Meaning                               |
| ---------------------------- | ---------- | ------------------------------------- |
| `SubscriptionStatusActive`   | `active`   | Billing is running                    |
| `SubscriptionStatusPaused`   | `paused`   | Billing paused; events still accepted |
| `SubscriptionStatusCanceled` | `canceled` | Subscription ended                    |

<Warning>
  A customer can have at most one active subscription per plan type. A customer may hold one active **collection** subscription and one active **payout** subscription simultaneously, but attempting to create a second active subscription of the same type returns a 409 conflict — check with `monigo.IsConflict(err)`.
</Warning>

## Payout accounts

Payout accounts are bank or mobile-money accounts that a customer can receive payouts to. They are scoped to a customer.

```go theme={null}
// Add a bank account
acct, err := client.PayoutAccounts.Create(ctx, "cust_abc",
	monigo.CreatePayoutAccountRequest{
		AccountName:   "John Driver",
		PayoutMethod:  monigo.PayoutMethodBankTransfer,
		BankName:      "First Bank Nigeria",
		BankCode:      "011",
		AccountNumber: "3001234567",
		Currency:      "NGN",
		IsDefault:     true,
	},
)

// Add a mobile money account
acct, err := client.PayoutAccounts.Create(ctx, "cust_abc",
	monigo.CreatePayoutAccountRequest{
		AccountName:       "John Driver",
		PayoutMethod:      monigo.PayoutMethodMobileMoney,
		MobileMoneyNumber: "+2348012345678",
		Currency:          "NGN",
	},
)

// List all accounts for a customer
list, err := client.PayoutAccounts.List(ctx, "cust_abc")

// Get, update, delete
acct, err = client.PayoutAccounts.Get(ctx, "cust_abc", "acct_xyz")
acct, err = client.PayoutAccounts.Update(ctx, "cust_abc", "acct_xyz",
	monigo.UpdatePayoutAccountRequest{AccountName: "Jane Driver"},
)
err = client.PayoutAccounts.Delete(ctx, "cust_abc", "acct_xyz")
```

| Method constant            | Value           |
| -------------------------- | --------------- |
| `PayoutMethodBankTransfer` | `bank_transfer` |
| `PayoutMethodMobileMoney`  | `mobile_money`  |

## Invoices

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

```go theme={null}
// Generate a draft invoice for a subscription
invoice, err := client.Invoices.Generate(ctx, "sub_abc")
fmt.Printf("Draft invoice %s — total: %s %s\n",
	invoice.ID, invoice.Total, invoice.Currency)

for _, item := range invoice.LineItems {
	fmt.Printf("  %s: qty %s × %s = %s\n",
		item.Description, item.Quantity, item.UnitPrice, item.Amount)
}

// List invoices with filters
invoices, err := client.Invoices.List(ctx, monigo.ListInvoicesParams{
	CustomerID: "cust_abc",
	Status:     monigo.InvoiceStatusDraft,
})

// Get a specific invoice
invoice, err = client.Invoices.Get(ctx, "inv_abc")

// Finalize — makes the invoice payable
invoice, err = client.Invoices.Finalize(ctx, "inv_abc")

// Void — cancels the invoice
invoice, err = client.Invoices.Void(ctx, "inv_abc")
```

| Status constant          | Value       | Meaning                               |
| ------------------------ | ----------- | ------------------------------------- |
| `InvoiceStatusDraft`     | `draft`     | Not yet finalized; amounts may change |
| `InvoiceStatusFinalized` | `finalized` | Locked and ready for payment          |
| `InvoiceStatusPaid`      | `paid`      | Payment collected                     |
| `InvoiceStatusVoid`      | `void`      | Cancelled                             |

<Info>
  All monetary amounts (`Subtotal`, `VATAmount`, `Total`, `UnitPrice`) 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.

```go theme={null}
// All usage for the current period
result, err := client.Usage.Query(ctx, monigo.UsageParams{})

// Filter by customer and metric
result, err := client.Usage.Query(ctx, monigo.UsageParams{
	CustomerID: "cust_abc",
	MetricID:   "metric_api_calls",
})

// Custom time window
from := time.Now().AddDate(0, -1, 0)
to := time.Now()
result, err := client.Usage.Query(ctx, monigo.UsageParams{
	CustomerID: "cust_abc",
	From:       &from,
	To:         &to,
})

for _, rollup := range result.Rollups {
	fmt.Printf("customer %s — %s: %.2f (period: %s → %s)\n",
		rollup.CustomerID, rollup.Aggregation, rollup.Value,
		rollup.PeriodStart.Format("2006-01-02"),
		rollup.PeriodEnd.Format("2006-01-02"),
	)
}
```

## 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 `PortalURL` field of 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

```go theme={null}
tok, err := client.PortalTokens.Create(ctx, monigo.CreatePortalTokenRequest{
	CustomerExternalID: "usr_abc123",
	Label:              "Invoice portal link",
})
if err != nil {
	log.Fatal(err)
}

fmt.Println("Share this link:", tok.PortalURL)
```

Set `ExpiresAt` (RFC3339) for a time-limited link:

```go theme={null}
tok, err := client.PortalTokens.Create(ctx, monigo.CreatePortalTokenRequest{
	CustomerExternalID: "usr_abc123",
	Label:              "30-day invoice link",
	ExpiresAt:          time.Now().Add(30 * 24 * time.Hour).Format(time.RFC3339),
})
```

### List tokens for a customer

```go theme={null}
resp, err := client.PortalTokens.List(ctx, "usr_abc123")
if err != nil {
	log.Fatal(err)
}
for _, tok := range resp.Tokens {
	fmt.Printf("token %s — %s (expires: %v)\n", tok.ID, tok.Label, tok.ExpiresAt)
}
```

`customerID` accepts either a Monigo UUID or the customer's `ExternalID`.

### Revoke a token

```go theme={null}
if err := client.PortalTokens.Revoke(ctx, tok.ID); err != nil {
	log.Fatal(err)
}
```

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.

```go theme={null}
// Get or create a wallet
wallet, err := client.Wallets.GetOrCreate(ctx, monigo.GetOrCreateWalletRequest{
	CustomerID: "cust_abc",
	Currency:   "NGN",
})

// List all wallets (org inferred from API key)
resp, err := client.Wallets.List(ctx)

// List wallets filtered by customer
resp, err = client.Wallets.List(ctx, monigo.ListWalletsParams{CustomerID: "cust_abc"})

// Or use the dedicated customer endpoint
resp, err = client.Wallets.ListByCustomer(ctx, "cust_abc")

// Get a wallet (includes virtual accounts)
detail, err := client.Wallets.Get(ctx, "wallet_uuid")
fmt.Printf("Balance: %s, Virtual accounts: %d\n",
	detail.Wallet.Balance, len(detail.VirtualAccounts))

// Credit (top up)
creditResp, err := client.Wallets.Credit(ctx, wallet.ID, monigo.CreditWalletRequest{
	Amount:         "5000.00",
	Currency:       "NGN",
	Description:    "Payment received",
	EntryType:      monigo.WalletEntryTypeDeposit,
	ReferenceType:  "payment",
	ReferenceID:    "pay_123",
	IdempotencyKey: "topup_pay_123",
})

// Debit (charge)
debitResp, err := client.Wallets.Debit(ctx, wallet.ID, monigo.DebitWalletRequest{
	Amount:         "1200.00",
	Currency:       "NGN",
	Description:    "Usage charge",
	EntryType:      monigo.WalletEntryTypeUsage,
	ReferenceType:  "invoice",
	ReferenceID:    "inv_456",
	IdempotencyKey: "debit_inv_456",
})
if monigo.IsQuotaExceeded(err) {
	// 402 — insufficient balance
}

// Transaction history
txns, err := client.Wallets.ListTransactions(ctx, wallet.ID, monigo.ListTransactionsParams{
	Limit: 25,
})

// Virtual accounts
va, err := client.Wallets.CreateVirtualAccount(ctx, wallet.ID, monigo.CreateVirtualAccountRequest{
	Provider: monigo.VirtualAccountProviderPaystack,
	Currency: "NGN",
})
vaList, err := client.Wallets.ListVirtualAccounts(ctx, wallet.ID)
```

### Wallet constants

| Constant                            | Value         | Description                   |
| ----------------------------------- | ------------- | ----------------------------- |
| `WalletEntryTypeDeposit`            | `deposit`     | Customer top-up               |
| `WalletEntryTypeWithdrawal`         | `withdrawal`  | Customer withdrawal           |
| `WalletEntryTypeUsage`              | `usage`       | Metered usage charge          |
| `WalletEntryTypeRefund`             | `refund`      | Reversal of a previous charge |
| `WalletEntryTypeAdjustment`         | `adjustment`  | Manual correction             |
| `VirtualAccountProviderPaystack`    | `paystack`    | Paystack virtual account      |
| `VirtualAccountProviderFlutterwave` | `flutterwave` | Flutterwave virtual account   |
| `VirtualAccountProviderMonnify`     | `monnify`     | Monnify virtual account       |

<Info>
  All wallet amounts (`Balance`, `ReservedBalance`, 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                                                    |
| ------------------------ | ----------------------------------------------------------------------- |
| `examples/quickstart`    | End-to-end: customer → metric → plan → subscription → ingest            |
| `examples/metering`      | High-volume batch ingest with idempotency and rate-limit retry          |
| `examples/billing`       | Generate, inspect, and finalize an invoice                              |
| `examples/payouts`       | Register payout accounts and trigger event replay                       |
| `examples/usage-report`  | Query rollups and print a terminal usage table                          |
| `examples/portal-tokens` | Create, list, and revoke customer portal access links                   |
| `examples/wallets`       | Wallet lifecycle: create, credit, debit, transactions, virtual accounts |

Run any example by setting `MONIGO_API_KEY` and executing:

```bash theme={null}
cd examples/quickstart
MONIGO_API_KEY=mk_test_... go run .
```

## Testing

The SDK has 77 unit tests with zero external dependencies. Each test spins up an in-process mock HTTP server via `net/http/httptest`:

```bash theme={null}
cd gosdk
go test ./...
```

To run tests in your own project against a local Monigo server, point `WithBaseURL` at it:

```go theme={null}
client := monigo.New("sk_test_...",
    monigo.WithBaseURL("http://localhost:8000"),
)
```
