Billing
Billing in Catentio is mediated by Plugipay's partner-billing pattern. The pricing catalogue lives in pricing/catalog.json on the control plane, not on Plugipay; Plugipay only holds the live subscription/plan objects once a workspace actually subscribes. Most self-hosted deployments run in internal_noop billing mode, where summary() and subscribe() return placeholder/no-op shapes instead of touching Plugipay at all.
For Catentio's wire-level reference, see API → Billing.
Auth. Billing is one of the few places in this SDK where Bearer auth actually works against the control plane:
/v1/billing/*usesRequireSessionOrAgent, which honours a real Huudis-issued JWT inAuthorization: Bearer. It does not honour this SDK's staticapiKey(cat_pk_...) — that's a runtime-only credential the control plane never checks. See SDKs → Auth: what actually works for the base-URL caveat that still applies on top of this.
Namespace
catentio.billing — three methods:
catentio.billing.catalog()
catentio.billing.summary()
catentio.billing.subscribe(input)
The control plane also has POST /v1/billing/topup, GET /v1/billing/topup/options, GET /v1/billing/wallet, GET /v1/billing/addons, and POST /v1/billing/addons/openrouter — none of them are wrapped by this SDK. Call them via catentio.api.* directly, or the REST API.
There's also a separate catentio.cost namespace (cost.summary(query?) → GET /v1/cost/summary, params days 1–180 default 30 and top_n 1–50 default 10) for the runtime's cost rollup — a different thing from billing summary, and not covered by a dedicated page in this section.
Methods
billing.catalog
Signature. catentio.billing.catalog(token?): Promise<CatalogResponse>
Returns the real pricing catalogue — pricing/catalog.json, not a Plugipay-side listing.
import { CatentioSaasClient } from '@forjio/catentio-saas-node';
const catentio = new CatentioSaasClient({ session });
const cat = await catentio.billing.catalog();
for (const t of cat.tiers) {
console.log(t.key, t.name, t.monthly_usd_cents, t.caps);
}
Real shape:
interface CatalogResponse {
version: 2;
tiers: Array<{
key: string; // 'free' | 'starter' | 'pro' | 'scale'
name: string;
tagline: string;
monthly_idr_cents: number | null;
monthly_usd_cents: number | null;
plugipay_plan_id: string | null;
can_provision_managed: boolean;
caps: string[];
support: string;
highlight?: true;
contact_only?: true;
plugipay_plan_live?: boolean | null; // only present when mode === 'live'
}>;
modifiers: { infra: object; anthropic: object; openrouter: object };
topup: {
min_initial_usd_cents_with_managed_infra: number;
min_initial_usd_cents_models_only: number;
min_usd_cents: number;
max_usd_cents: number;
presets_usd_cents: number[];
};
mode: 'internal_noop' | 'live';
error?: string; // present if live mode couldn't reach Plugipay
}
There is no displayName, priceCents, currency, includedAgentHours, overageCentsPerHour, or features field — those don't exist. The real field names are name, monthly_usd_cents/monthly_idr_cents, and caps (a flat list of human-readable strings, not a structured quota).
billing.summary
Signature. catentio.billing.summary(token?): Promise<SummaryResponse>
Returns the current-period billing snapshot. Shape depends on billing mode:
// internal_noop (the default for most deployments):
{
mode: 'internal_noop',
current_tier: 'internal',
current_period: { start: '2026-05-01', end: '2026-06-01' },
subscription_idr: 0,
addons_idr: 0,
managed_anthropic_idr: 0,
total_idr: 0,
lines: [],
note: "Billing is in internal_noop mode — the catentio team owns this deployment and isn't billed. ...",
}
// live:
{
mode: 'live',
current_tier: 'unknown',
current_plan_id: string | null,
current_period: { start, end },
subscription_idr: 0,
addons_idr: 0,
managed_anthropic_idr: 0,
total_idr: number, // settled partner usage for the period
lines: Array<{ merchant_id, period_start, period_end, plugipay_revenue_idr, partner_share_idr, settlement_idr }>,
}
There is no tierSlug, currentPeriodUsd, includedRemaining, paymentMethodStatus, nextInvoiceAt, or plugipayCustomerId field — none of those exist on the real response.
const s = await catentio.billing.summary();
console.log(s.mode, s.current_tier, s.total_idr);
billing.subscribe
Signature. catentio.billing.subscribe(input, token?): Promise<SubscribeResponse>
The SDK's declared input type is wrong. It types
inputas{ tier: string }, but the real body field is{ plugipay_plan_id: string; currency?: "IDR" | "USD" }. Sending{ tier: "pro" }gets you400 { "detail": "plugipay_plan_id required" }. Use the tier'splugipay_plan_idfromcatalog(), not itskey:const cat = await catentio.billing.catalog(); const pro = cat.tiers.find(t => t.key === 'pro')!; const res = await catentio.billing.subscribe({ plugipay_plan_id: pro.plugipay_plan_id! });
Response shape varies:
internal_noopmode:{ ok: true, noop: true, message: "billing_mode=internal_noop — subscribe is a no-op. ..." }- Live, success:
{ ok: true, change: "created" | "updated", subscription: <Plugipay subscription object>, plan_id: string } - Live, no payment method:
{ ok: false, payment_method_required: true, checkout_url: string }— note that in the current implementationcheckout_urlis populated straight from the upstream Plugipay error message, so verify it's actually a URL before redirecting. 404 { detail: "no_deployment_for_customer" }if the caller has no workspace deployment row yet.
There is no { subscribed: true, tierSlug } shape and no checkoutUrl (camelCase) field.
Errors
Real error bodies are FastAPI's {"detail": ...}; detail is sometimes a plain string, sometimes a dict.
| Status | detail |
Cause |
|---|---|---|
| 400 | "plugipay_plan_id required" |
subscribe() body missing the field (see the warning above). |
| 400 | {"code": "invalid_amount", ...} |
Only relevant to topup, not wrapped by this SDK. |
| 404 | "no_deployment_for_customer" |
subscribe() before a workspace deployment exists. |
| 404 | "plan {id} not found" |
subscribe() with a plugipay_plan_id Plugipay doesn't have. |
| 409 | "plan {id} has no active {currency} price" |
Plan exists but no price row for the resolved currency. |
| 502 | — | Catentio couldn't reach Plugipay; the detail string includes the upstream status/code/message. |
Next
- API → Billing — HTTP-level reference, including the routes this SDK doesn't wrap (
topup,wallet,addons, ...). - SDKs — the base-URL and auth reality that applies on top of everything above.