Billing
The billing namespace lets a merchant inspect and mutate their Ripllo subscription — list available plans, see what they're on, view usage against the plan's caps, list past invoices, start a Plugipay-hosted checkout to upgrade, or cancel. This page covers ripllo.billing on the Node SDK. For the HTTP surface, see API → Billing.
Namespace
ripllo.billing.plans()
ripllo.billing.currentPlan()
ripllo.billing.subscription()
ripllo.billing.usage()
ripllo.billing.invoices(params?)
ripllo.billing.checkout(input)
ripllo.billing.cancel()
Seven methods, all merchant-scoped. Partner-billed merchants (those provisioned via Storlaunch's KNOWN_PARTNERS flow) don't use this namespace — their Ripllo bill rolls into the partner's invoice via Plugipay's Pattern 2 partner-billing. The billing methods here are for merchants who came to Ripllo directly.
Methods
billing.plans
Signature. ripllo.billing.plans(): Promise<Plan[]>
Returns every plan Ripllo offers as { id, name, price, currency, features }. There are four tiers — free, starter, growth, scale — and price is monthly IDR (0 / 299 000 / 799 000 / 2 499 000). There are no contactCap / monthlySendCap / seatCap fields; features is a human-readable bullet list, and the numeric caps live server-side in the plan-limits table.
const plans = await ripllo.billing.plans();
for (const p of plans) console.log(p.id, p.name, p.price, p.currency);
Case matters.
plans()returns lowercase ids (growth), butcheckoutrequires the uppercase enum (GROWTH). Upper-case the id before you pass it on, or the call 400s.
billing.currentPlan
Signature. ripllo.billing.currentPlan(): Promise<CurrentPlan>
Returns the tier the calling workspace is on plus its resolved limits — not the same shape as a plans() entry. Fields: plan (lowercase key), planName, isForjioInternal, contactsLimit, rateLimit, pixelsEnabled, feedsEnabled, blogPostsLimit, discountCodesLimit, referralProgramsLimit, billingCycleEnd. Unlimited is encoded as -1. If the merchant has never paid, this returns the free tier.
const plan = await ripllo.billing.currentPlan();
console.log(`On ${plan.planName}, ${plan.contactsLimit} contacts`); // -1 = unlimited
billing.subscription
Signature. ripllo.billing.subscription(): Promise<Subscription>
Returns a view of the subscription — { plan, planName, isForjioInternal, status, currentPeriodStart, currentPeriodEnd, cancelAt }, with plan and status lowercased. It does not expose the upstream Plugipay subscription ID. This is what the dashboard's billing page renders.
const sub = await ripllo.billing.subscription();
console.log(sub.status, sub.currentPeriodEnd, sub.cancelAt);
billing.usage
Signature. ripllo.billing.usage(): Promise<Usage>
Returns this calendar month's counters. The shape is flat — there are no {used, cap} pairs, no periodStart/periodEnd, and no seat metering anywhere in Ripllo:
{
plan: 'starter',
contacts: 4321, // contacts tracked this month
contactsLimit: 10000, // -1 when unlimited
discountsRedeemed: 87,
remindersSent: 412,
referralAttributions: 12,
blogPostsPublished: 3,
resetAt: '2026-06-01T00:00:00.000Z',
}
contacts is the only counter with a limit in this payload; the rest are informational. Use it for the dashboard's progress bar, and read the other ceilings from currentPlan().
billing.invoices
Signature. ripllo.billing.invoices(params?): Promise<{ data: Invoice[]; cursor: string | null; hasMore: boolean }>
Cursor-paginated invoice history, newest first, limit capped at 50 (default 20). Each row is { id, plan, amount, currency, status, paidAt, receiptUrl, createdAt }.
The SDK's declared return type is wrong here.
billing.invoicesis annotated{ invoices, nextCursor }, but the endpoint returns{ data, cursor, hasMore }— TypeScript won't stop you from readingpage.invoices, and it will beundefinedat runtime. Readpage.data/page.cursor.
const page = await ripllo.billing.invoices({ limit: 24 }) as unknown as {
data: Invoice[]; cursor: string | null; hasMore: boolean;
};
for (const inv of page.data) {
console.log(`${inv.id} — ${inv.amount} ${inv.currency} — ${inv.status}`);
}
billing.checkout
Starts a Plugipay-hosted checkout for a plan change. The body the API accepts is { plan, email?, name? }:
| Field | Notes |
|---|---|
plan |
Required. Uppercase enum: 'STARTER', 'GROWTH' or 'SCALE'. 'FREE' is rejected — downgrading is a cancel(). |
email |
Optional for portal-proxy callers (taken from the JWT claim). Required for SDK/API-key callers, which have no email claim — otherwise 400 VALIDATION. |
name |
Optional customer name for the Plugipay customer record. |
The response is { subscriptionId, invoiceId, checkoutSessionId, checkoutUrl }. Redirect the merchant to checkoutUrl; Ripllo's Plugipay webhook flips them onto the new plan when payment clears. Return URLs are fixed server-side to <APP_BASE_URL>/dashboard/billing?status=success|canceled — you cannot supply your own.
The SDK's typed signature does not match the API.
billing.checkoutis declared as{ planId, successUrl?, cancelUrl? }returning{ url, sessionId }. The backend requiresplanand ignores unknown keys, so a literalcheckout({ planId: 'growth' })400s withVALIDATION, andsuccessUrl/cancelUrlare silently stripped. Until the SDK is fixed, send the real body:
const result = await ripllo.billing.checkout({
plan: 'GROWTH',
email: merchant.email,
} as unknown as { planId: string }) as unknown as {
subscriptionId: string; invoiceId: string; checkoutSessionId: string; checkoutUrl: string;
};
return res.redirect(result.checkoutUrl);
Free-tier downgrades don't go through checkout — they're a cancel() call instead.
billing.cancel
Signature. ripllo.billing.cancel(): Promise<SubscriptionView>
Cancels the subscription at the end of the current period and returns the same view subscription() returns. The merchant keeps their plan benefits until currentPeriodEnd; then they fall back to free.
await ripllo.billing.cancel();
To un-cancel before the period ends, run a checkout for the same plan — that flips the cancelAt flag off.
Types
The billing methods are typed loosely in the SDK (Record<string, unknown> / unknown[]), so these are the runtime shapes rather than exported interfaces:
interface Plan { // an entry from plans()
id: string; // 'free' | 'starter' | 'growth' | 'scale'
name: string;
price: number; // monthly, minor-unit-free IDR (e.g. 799_000)
currency: string; // 'IDR'
features: string[]; // human-readable bullet list
}
interface SubscriptionView { // subscription() and cancel()
plan: string; // lowercase tier key
planName: string;
isForjioInternal: boolean;
status: string; // lowercase, e.g. 'active' | 'canceled'
currentPeriodStart: string | null;
currentPeriodEnd: string | null;
cancelAt: string | null; // when cancel-at-period-end is scheduled
}
interface Invoice { // a row from invoices().data
id: string;
plan: string; // lowercase tier key
amount: number;
currency: string;
status: string;
paidAt: string | null;
receiptUrl: string | null;
createdAt: string;
}
Common patterns
Render the dashboard billing page
const [plan, sub, usage, page] = await Promise.all([
ripllo.billing.currentPlan(),
ripllo.billing.subscription(),
ripllo.billing.usage(),
ripllo.billing.invoices({ limit: 6 }),
]);
return view.render({ plan, sub, usage, recentInvoices: page.data });
Warn when contacts approach the cap
const usage = await ripllo.billing.usage();
if (usage.contactsLimit !== -1 && usage.contacts >= usage.contactsLimit) {
return banner('You\'ve hit your contact limit. Upgrade to keep growing your audience.');
}
contactsLimit is the only cap returned by usage(), and -1 means unlimited. There is no send cap and no seat cap in Ripllo — don't gate on usage.sends or usage.seats, they don't exist.
Block UI for partner-billed workspaces
Partner-billed merchants (those provisioned through Storlaunch and friends) shouldn't see the upgrade/downgrade UI — their Ripllo fee rolls into the partner's invoice. Nothing in the billing responses marks them as partner-billed: subscription() returns no Plugipay identifiers, and a partner-provisioned workspace simply gets floored onto the starter tier. Track the provenance on your side (you made the provisioning call) and hide the UI from there. The only flag Ripllo does surface is isForjioInternal, which marks Forjio's own workspaces, not partner ones.
Errors
| Code | Status | Cause |
|---|---|---|
VALIDATION |
400 | plan missing, lowercase, or not one of STARTER / GROWTH / SCALE; or no email available for the checkout. |
NO_ACCOUNT |
403 | The principal carries no accountId. |
INSUFFICIENT_SCOPE |
403 | API key lacks read (GETs) or write (mutations). |
PLAN_NOT_CONFIGURED |
503 | No Plugipay plan is wired for that tier in this environment. |
CHECKOUT_FAILED |
500 | Plugipay rejected the subscription or checkout-session create. |
CANCEL_FAILED |
500 | Plugipay rejected the cancellation. |
There is no partner_managed error — nothing blocks checkout / cancel on a partner-provisioned workspace today, so keep partner-billed merchants away from this UI in your own product rather than relying on Ripllo to refuse. See Authentication for auth-layer codes.
Next
- API → Billing — HTTP-level reference.
- Plugipay docs — the underlying checkout + subscription engine.