Skip to main content
The Monigo customer portal is a hosted billing UI that your customers access through a unique, revocable link. Each customer can:
  • View and pay outstanding invoices
  • See payout slips and track payment history
  • Monitor their active subscriptions with live usage estimates
  • Add and manage payout accounts (bank transfer or mobile money)
  • View and manage saved payment cards
There is no login — access is granted by the portal token embedded in the URL.

How portal tokens work

A portal token is an opaque 64-character random string that encodes a (org, customer) pair. Monigo validates the token on every request to the portal API.
https://monigo.co/portal/<token>
Key properties:
PropertyBehaviour
ScopeBound to one customer in one organisation
RevocationInstant — delete the token and all requests using it fail with 401
ExpiryOptional. Set expires_at for time-limited links (e.g. one-time email links). Omit for permanent links.
Multiple tokensA customer can have many active tokens simultaneously (e.g. one permanent link + one short-lived email link)

Use your API key to generate a portal link server-side. Pass the customer’s external_id — the same ID you use when ingesting events.
curl -X POST https://api.monigo.co/v1/portal/tokens \
  -H "Authorization: Bearer mk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: portal-acme-$(date +%s)" \
  -d '{
    "customer_external_id": "user_abc123",
    "label": "Main portal link"
  }'
Response:
{
  "token": {
    "id": "01936b2a-...",
    "label": "Main portal link",
    "expires_at": null,
    "created_at": "2026-02-26T09:00:00Z"
  },
  "portal_url": "https://monigo.co/portal/4a7f9c..."
}
When sending a portal link in an email or notification, use a short expiry so the link becomes invalid once clicked and acted on.
curl -X POST https://api.monigo.co/v1/portal/tokens \
  -H "Authorization: Bearer mk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: portal-email-acme-feb26" \
  -d '{
    "customer_external_id": "user_abc123",
    "label": "Invoice email — Feb 2026",
    "expires_at": "2026-03-05T23:59:59Z"
  }'
Keep one permanent token per customer for your in-app “Billing” link, and generate fresh short-lived tokens whenever you send an email or notification that includes a portal link. This way you can revoke the email link independently without breaking the in-app experience.

Fetch a permanent portal link at account creation and store it, or generate it on demand when the customer navigates to their billing settings.
TypeScript
// In your billing settings page handler
const resp = await monigoClient.portal.createToken({
  customer_external_id: req.user.id,
  label: 'In-app billing link',
})

// Redirect the customer
res.redirect(resp.portal_url)
Go
// In your billing settings route
resp, err := monigoClient.Portal.CreateToken(ctx, monigo.CreatePortalTokenRequest{
    CustomerExternalID: user.ID,
    Label:              "In-app billing link",
})
if err != nil {
    http.Error(w, "failed to generate portal link", 500)
    return
}
http.Redirect(w, r, resp.PortalURL, http.StatusFound)

Option 2 — Embedded iframe

Embed the portal directly inside your product. The portal is designed to be responsive and renders cleanly at any width.
<iframe
  src="https://monigo.co/portal/<token>"
  width="100%"
  height="700"
  style="border: none; border-radius: 8px;"
  title="Billing Portal"
></iframe>
For a seamless feel, generate the token server-side and inject it into the page template rather than exposing your API key to the browser.
<!-- In your server-rendered template (e.g. Jinja2, Go html/template, ERB) -->
<iframe
  src="{{ portal_url }}"
  width="100%"
  height="700"
  style="border: none; border-radius: 8px;"
  title="Billing Portal"
></iframe>
Never call the Monigo API from client-side JavaScript using your live API key. Generate the portal token on your server and pass only the URL to the browser.
Include portal_url directly in invoice emails, payment receipts, or dunning messages.
TypeScript
// When an invoice is finalized (invoice.finalized webhook)
async function onInvoiceFinalized(customerId: string, invoiceId: string) {
  const resp = await monigoClient.portal.createToken({
    customer_external_id: customerId,
    label: `Invoice ${invoiceId} email`,
    expires_at: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString(),
  })

  await mailer.send({
    to: customer.email,
    subject: 'Your invoice is ready',
    body: `View and pay your invoice: ${resp.portal_url}`,
  })
}

What customers see in the portal

Invoices

Customers can view all finalized, paid, and failed invoices. On finalized or failed invoices a Pay Now button is shown — clicking it redirects to Paystack (or your configured provider) for payment. Once paid, the invoice status updates automatically.

Payout Slips

For payout-type plans, customers see their payout slips — the amounts they are owed. They can drill into each slip to see line items, WHT deductions, and the transaction history showing when payouts were sent to their account.

Subscriptions

The subscriptions tab shows active and trial plans alongside live usage and an accrued estimate for the current billing period. This lets customers see how their usage is tracking before the invoice is generated.
FieldDescription
plan_nameThe name of the plan they are subscribed to
current_period_start / endThe dates for the billing period in progress
usage_itemsPer-metric usage with estimated charge
estimated_totalProjected invoice total based on current usage

Payout Accounts

Customers can add and manage the bank or mobile money accounts that Monigo uses when issuing payouts to them.
MethodRequired fields
bank_transferBank name, bank code, account number
mobile_moneyMobile money number

Payment Methods (Saved Cards)

When a customer pays an invoice via Paystack, their card is automatically saved as a reusable payment method. On the next invoice, Monigo charges the saved default card automatically — the customer only sees the portal if the charge fails. Customers can:
  • View all saved cards (card type, last 4 digits, expiry, bank)
  • Set a different card as the default
  • Remove a card they no longer want to use
Cards approaching expiry are flagged with an expiring_soon status 30 days in advance, and become expired after their expiry date. Customers receive email notifications for both.

Using the portal API directly (custom UI)

If you want to build your own billing UI rather than using the hosted portal, you can call the portal API endpoints directly from your frontend. Authenticate with the X-Portal-Token header instead of a Bearer token.
X-Portal-Token: <token>

Customer info

cURL
curl https://api.monigo.co/api/v1/portal/me \
  -H "X-Portal-Token: <token>"
{
  "customer_id": "01936b2a-...",
  "customer_name": "Acme Corp",
  "customer_email": "billing@acme.com",
  "org_id": "01936b2a-...",
  "org_name": "My SaaS"
}

List invoices

cURL
curl "https://api.monigo.co/api/v1/portal/invoices?page=1&limit=20" \
  -H "X-Portal-Token: <token>"

Initiate payment for an invoice

cURL
curl -X POST https://api.monigo.co/api/v1/portal/invoices/<invoice_id>/pay \
  -H "X-Portal-Token: <token>"
{
  "authorization_url": "https://checkout.paystack.com/...",
  "reference": "pay_abc123",
  "transaction_id": "01936b2a-...",
  "provider": "paystack"
}
Redirect the customer to authorization_url to complete payment.

Full endpoint reference

MethodPathDescription
GET/api/v1/portal/meCustomer info
GET/api/v1/portal/invoicesList invoices (?page, ?limit)
GET/api/v1/portal/invoices/:idGet a single invoice
POST/api/v1/portal/invoices/:id/payInitiate payment — returns authorization_url
GET/api/v1/portal/invoices/:id/transactionsPayment history for an invoice
GET/api/v1/portal/billsList payout slips
GET/api/v1/portal/bills/:idGet a single payout slip
GET/api/v1/portal/bills/:id/transactionsPayout history for a slip
GET/api/v1/portal/subscriptionsActive subscriptions with live usage
GET/api/v1/portal/subscriptions/:idSingle subscription detail
GET/api/v1/portal/payout-accountsList payout accounts
POST/api/v1/portal/payout-accountsAdd a payout account
GET/api/v1/portal/payment-methodsList saved cards
PUT/api/v1/portal/payment-methods/:id/defaultSet default card
DELETE/api/v1/portal/payment-methods/:idRemove a saved card
The portal API only returns documents that are visible to the customer: finalized, paid, and failed invoices. Draft invoices are never exposed. This is enforced server-side regardless of any query parameters.

Managing tokens (list & revoke)

List all tokens for a customer

curl "https://api.monigo.co/v1/portal/tokens?customer_external_id=user_abc123" \
  -H "Authorization: Bearer mk_live_..."

Revoke a token

Revoking a token immediately invalidates the portal link. Any browser tab or iframe using that token will receive a 401 on the next API call.
curl -X DELETE https://api.monigo.co/v1/portal/tokens/<token_id> \
  -H "Authorization: Bearer mk_live_..."
Revoke all tokens for a customer (e.g. after account closure or a suspected breach) by listing and revoking each one.

Security considerations

Keep token generation server-side. Your Monigo API key must never appear in client-side code. Generate the portal token on your server and pass only the resulting URL to the browser. Scope tokens to the minimum necessary lifetime. Use permanent tokens only for in-app links where you control the session. For emails and notifications, set expires_at to 7–30 days depending on your use case. Revoke tokens on account deletion or suspension. When a customer closes their account or is suspended, revoke all their portal tokens so they cannot continue to access billing data. Prefer external_id over UUID in your code. Your customer_external_id (the ID from your own database) is the most stable identifier. Using it avoids having to track Monigo-internal UUIDs in your application.
TypeScript
// Good — uses your own stable ID
const resp = await client.portal.createToken({
  customer_external_id: user.id,   // your DB's user ID
})

// Acceptable but requires tracking Monigo UUIDs
const resp = await client.portal.createToken({
  customer_id: monigoCustomerId,
})

Integration pattern: on-demand generation

The recommended pattern for most products: generate the portal URL on demand when the customer navigates to their billing section, then redirect or open in a modal/iframe. This avoids storing tokens and ensures the link is always fresh.
TypeScript
// Next.js API route: /api/billing-portal
import type { NextApiRequest, NextApiResponse } from 'next'
import { monigoClient } from '@/lib/monigo'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'GET') return res.status(405).end()

  // Identify the authenticated user from your session
  const session = await getSession(req)
  if (!session) return res.status(401).json({ error: 'Unauthorized' })

  const resp = await monigoClient.portal.createToken({
    customer_external_id: session.userId,
    label: `In-app — ${session.userId}`,
  })

  // Redirect straight to the portal, or return the URL to the frontend
  res.redirect(302, resp.portal_url)
}
Go
// Go HTTP handler
func BillingPortalHandler(w http.ResponseWriter, r *http.Request) {
    userID := sessionUserID(r) // from your session/auth middleware

    resp, err := monigoClient.Portal.CreateToken(r.Context(), monigo.CreatePortalTokenRequest{
        CustomerExternalID: userID,
        Label:              fmt.Sprintf("In-app — %s", userID),
    })
    if err != nil {
        http.Error(w, "failed to generate portal link", http.StatusInternalServerError)
        return
    }

    http.Redirect(w, r, resp.PortalURL, http.StatusFound)
}