Billing

This is the Huudis-own billing surface — the plan, limits, usage, and invoice history for the Huudis IdP itself. Each enabled Forjio service (Plugipay, Storlaunch, etc.) has its own billing surface in the respective product.

Huudis bills through Plugipay (it's a Plugipay customer like any other). Subscriptions and invoices live on the Plugipay side; this surface is the read-through and the checkout-initiator.

All endpoints require a bearer admin JWT — see Authentication. Only owner and admin can checkout or cancel; member can read.

Endpoints

Method Path Purpose
GET /v1/account/billing Plan summary for the active workspace
GET /v1/account/billing/plans Tier catalog
GET /v1/account/billing/usage Current usage counts per limit
GET /v1/account/billing/invoices Invoice history from Plugipay
POST /v1/account/billing/checkout Start an upgrade to a paid tier
POST /v1/account/billing/cancel Cancel at period end

Get plan summary

GET /v1/account/billing

Response

{
  "data": {
    "accountId": "acc_01KPG…",
    "plan": "pro",
    "planName": "Pro",
    "priceMonthlyIdr": 449000,
    "limits": {
      "monthlyActiveUsers": 10000,
      "orgs": 5,
      "seats": 25,
      "serviceAccounts": 20,
      "samlConnections": 3,
      "scim": false,
      "customIdp": true,
      "auditLogDays": 90
    },
    "planValidUntil": "2026-06-12T00:00:00.000Z",
    "hasActiveSubscription": true,
    "isForjioInternal": false
  }
}

isForjioInternal: true flags Huudis's own operational workspaces — they bypass billing entirely. Most callers ignore this flag.

List plans

GET /v1/account/billing/plans

Returns the tier catalog Huudis offers, in display order. Use for the marketing/upgrade grid.

{
  "data": [
    {
      "tier": "free",
      "name": "Free",
      "priceMonthlyIdr": 0,
      "monthlyActiveUsers": 1000,
      "seats": 5,
      "serviceAccounts": 3,
      "samlConnections": 0,
      "scim": false,
      "customIdp": false,
      "auditLogDays": 7
    },
    { "tier": "pro", … },
    { "tier": "business", … },
    { "tier": "scale", … }
  ]
}

Currency is always IDR — Huudis bills only in Indonesian Rupiah today.

Get usage

GET /v1/account/billing/usage

Real-time usage counts vs. tier limits.

{
  "data": {
    "seats": { "used": 4, "limit": 25 },
    "serviceAccounts": { "used": 2, "limit": 20 },
    "samlConnections": { "used": 1, "limit": 3 },
    "monthlyActiveUsers": { "used": 832, "limit": 10000 }
  }
}

limit: null means unlimited on this tier. The MAU count is the distinct users with a lastUsedAt session in the last 30 days — computed on every call (not cached); cheap on small workspaces, can be hundreds of milliseconds on workspaces with millions of sessions.

List invoices

GET /v1/account/billing/invoices

Reads through to Plugipay's /v1/invoices?customerId={huudis-side-customer-id} and reshapes for the dashboard. Returns the last 24 invoices, newest first.

{
  "data": [
    {
      "id": "inv_01KPG…",
      "number": "INV-2026-05-001234",
      "status": "paid",
      "amountIdr": 449000,
      "periodStart": "2026-04-12T00:00:00.000Z",
      "periodEnd": "2026-05-12T00:00:00.000Z",
      "paidAt": "2026-04-12T00:01:23.000Z",
      "hostedInvoiceUrl": "https://plugipay.com/i/inv_01KPG…"
    }
  ]
}

The hosted URL is the Plugipay-rendered invoice PDF/HTML — useful for forwarding to a finance team.

Start checkout

POST /v1/account/billing/checkout

Initiates an upgrade. Creates (or reuses) a Plugipay customer for this workspace, starts a subscription on the chosen tier, and returns a hosted checkout URL.

Request body

Field Type Required Description
tier pro | business | scale yes Target tier. Cannot pick free here — downgrade via cancel.

Response

{
  "data": {
    "checkoutUrl": "https://plugipay.com/c/sess_01KPG…",
    "checkoutSessionId": "sess_01KPG…",
    "subscriptionId": "sub_01KPG…",
    "invoiceId": "inv_01KPG…"
  }
}

Redirect the user to checkoutUrl. On successful payment, Plugipay fires subscription.created back into Huudis (we're a subscriber to our own webhook); Huudis flips the workspace's plan field and the new tier's limits take effect immediately.

Errors

Status error.code When
409 FORJIO_INTERNAL This is a Forjio-internal workspace — it doesn't pay.
409 SAME_TIER Already on the requested tier.
400 NO_ACTIVE_ACCOUNT Bearer has no active workspace.
403 FORBIDDEN Caller isn't owner/admin.

Cancel

POST /v1/account/billing/cancel

Cancels the subscription at period end. The workspace stays on the paid tier until the period closes, then drops to free automatically (with the lower limits enforced from that moment).

200 OK returns { canceled: true }.

To immediately downgrade (lose the rest of the paid period), contact support — the API is intentionally cautious here.

Plan summary object

Field Type Description
accountId string Active workspace.
plan free | pro | business | scale Current tier.
planName string Display name.
priceMonthlyIdr integer Monthly price in IDR. 0 for free.
limits object Per-limit caps. null (in usage response) means unlimited.
planValidUntil ISO 8601 | null Period-end. null for free.
hasActiveSubscription boolean Whether there's a live Plugipay subscription backing the row.
isForjioInternal boolean Internal workspaces don't pay.

Usage limits

The four hard limits Huudis enforces:

Limit What it counts
seats Workspace members (account_member rows).
serviceAccounts Service accounts created.
samlConnections SAML or OIDC identity providers configured.
monthlyActiveUsers Distinct end users with a session in the last 30 days.

Exceed seats or serviceAccounts and the next mutation that would push past the limit returns 400 PLAN_LIMIT_EXCEEDED. MAU is observed but not blocked — you can go over for one period; overages bill at the configured per-MAU rate on the next invoice.

Soft limits (scim, customIdp) are boolean — if the tier doesn't allow them, the relevant endpoints (e.g. SCIM provisioning) return 403 FEATURE_NOT_ON_PLAN.

Reconciliation with Plugipay

Huudis stores plugipayCustomerId and plugipaySubscriptionId on the workspace row. Most of the time these stay in sync; if you suspect a mismatch (a paid workspace stuck on the free tier, for instance), reconcile by:

  1. Fetch the Plugipay subscription via Plugipay's own API.
  2. If active and the tier doesn't match, contact support — the resync is operator-driven, not customer-driven.

Plugipay webhooks (subscription.updated, invoice.paid) drive the in-band reconciliation, so transient mismatches typically self-heal within minutes.

Events

No webhook events from Huudis's billing surface itself. Plugipay fires subscription.created, subscription.updated, subscription.canceled, invoice.paid, invoice.payment_failed — subscribe to those on the Plugipay side if you want to react.

Audit-log entries: billing.checkout_started, billing.cancel_requested, billing.plan_changed.

Next