Currency

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), but checkout requires 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.invoices is annotated { invoices, nextCursor }, but the endpoint returns { data, cursor, hasMore } — TypeScript won't stop you from reading page.invoices, and it will be undefined at runtime. Read page.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.checkout is declared as { planId, successUrl?, cancelUrl? } returning { url, sessionId }. The backend requires plan and ignores unknown keys, so a literal checkout({ planId: 'growth' }) 400s with VALIDATION, and successUrl / cancelUrl are 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

CurrencyRupiah is paid by transfer or QRIS; US dollars settle through PayPal.