Billing
The billing resource exposes the merchant's subscription state, current plan, recent invoices, and a checkout/cancel pair for upgrading and downgrading. Under the hood Ripllo bills via Plugipay's Pattern 2 partner billing — the merchant pays Plugipay, Plugipay forwards a share to Ripllo's connected account, and Ripllo reflects state through this API. From the merchant's perspective it's one bill.
This page documents the merchant-facing surface. The Plugipay-side webhook handlers and reconciliation logic are internal — you don't need to know about them to read or change a plan.
Endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/api/v1/billing/plans |
none | Public plan catalog |
GET |
/api/v1/billing/plan |
required | Current plan |
GET |
/api/v1/billing/subscription |
required | Current subscription |
GET |
/api/v1/billing/usage |
required | Usage against plan limits |
GET |
/api/v1/billing/invoices |
required | Invoice history |
POST |
/api/v1/billing/checkout |
required | Start a checkout to change plan |
POST |
/api/v1/billing/cancel |
required | Cancel the subscription |
Public plan catalog
GET /api/v1/billing/plans
No auth. Returns the public catalog of plans, suitable for rendering the landing-page pricing section and the dashboard's plan picker. The exact field set varies; here's the typical shape:
{
"data": [
{
"key": "STARTER",
"displayName": "Starter",
"priceIdr": 0,
"billingCycle": "monthly",
"tagline": "For testing the platform",
"features": ["100 contacts", "1 active campaign", "Discount codes"],
"limits": {
"maxContacts": 100,
"maxActiveCampaigns": 1,
"maxBlogPosts": 10,
"creatorMarketplaceEnabled": false
}
},
{
"key": "GROWTH",
"displayName": "Growth",
"priceIdr": 290000,
"billingCycle": "monthly",
"tagline": "For growing storefronts",
"features": ["5,000 contacts", "10 active campaigns", "All marketing primitives", "Creator marketplace"],
"limits": { /* ... */ }
},
{
"key": "SCALE",
"displayName": "Scale",
"priceIdr": 1490000,
"billingCycle": "monthly",
"tagline": "For multi-channel teams",
"features": ["50,000 contacts", "Unlimited campaigns", "Priority support"],
"limits": { /* ... */ }
}
],
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
Current plan
GET /api/v1/billing/plan
Returns the effective plan for the workspace — the one limit enforcement reads from. This may differ from the subscribed plan in a few situations:
- The subscription is in
past_due; the effective plan downgrades toSTARTER. - A trial is active; the effective plan is the trialled tier.
- Comp/partner overrides are set.
{
"data": {
"key": "GROWTH",
"displayName": "Growth",
"limits": { /* same shape as in the public catalog */ },
"source": "subscription"
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
source is one of subscription, trial, override, downgrade_past_due.
Current subscription
GET /api/v1/billing/subscription
Returns the subscription record — status, current-period dates, the underlying Plugipay subscription ID.
{
"data": {
"id": "sub_01HX...",
"accountId": "acc_01HX...",
"plan": "GROWTH",
"status": "active",
"currentPeriodStart": "2026-05-01T00:00:00.000Z",
"currentPeriodEnd": "2026-06-01T00:00:00.000Z",
"cancelAtPeriodEnd": false,
"trialEndsAt": null,
"plugipaySubscriptionId": "sub_plugipay_01HX...",
"createdAt": "2026-04-01T00:00:00.000Z",
"updatedAt": "2026-05-01T00:00:00.000Z"
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
status values: trialing, active, past_due, canceled, incomplete. The cancel/past-due distinctions matter for limit enforcement (past_due triggers the downgrade_past_due effective plan); the others are mostly informational.
Usage against plan limits
GET /api/v1/billing/usage
Returns current consumption counters compared to plan limits. Useful for dashboard "you're using X of Y" widgets and pre-flight checks before bulk operations.
{
"data": {
"plan": "GROWTH",
"limits": {
"maxContacts": 5000,
"maxActiveCampaigns": 10,
"maxBlogPosts": 100,
"creatorMarketplaceEnabled": true
},
"usage": {
"contacts": 1284,
"activeCampaigns": 3,
"blogPosts": 12
},
"asOf": "2026-05-13T10:42:00.000Z"
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
The numbers are computed eventually-consistently — they're accurate to within a minute or so. For "right-now" checks, the per-resource limit enforcement at create time is authoritative.
Invoice history
GET /api/v1/billing/invoices
Cursor-paginated. Returns invoices Ripllo has emitted for this workspace.
Query parameters
| Param | Default | Notes |
|---|---|---|
limit |
20 |
Clamped to [1, 50]. |
cursor |
— | Opaque cursor from the previous response. |
Response
{
"data": {
"items": [
{
"id": "inv_01HX...",
"number": "RPLO-2026-0512",
"issuedAt": "2026-05-12T00:00:00.000Z",
"amountIdr": 290000,
"status": "paid",
"paidAt": "2026-05-12T00:01:23.000Z",
"pdfUrl": "https://invoices.plugipay.com/inv_…/pdf",
"plugipayInvoiceId": "inv_plugipay_01HX..."
}
],
"nextCursor": null,
"hasMore": false
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
pdfUrl is a signed Plugipay-hosted URL that expires after a short window; refetch this endpoint to get a fresh URL rather than caching the PDF.
Start checkout
POST /api/v1/billing/checkout
Begins a plan-change flow. Returns a Plugipay checkout session URL the merchant follows to complete the payment. Once Plugipay confirms, Ripllo's webhook handler flips the subscription to the new plan.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
plan |
enum | yes | One of STARTER, GROWTH, SCALE. |
email |
string | no | Where Plugipay should send the receipt. Defaults to the email in the auth token. |
name |
string | no | Display name on the receipt. |
Response
{
"data": {
"checkoutSessionId": "cs_plugipay_01HX...",
"checkoutUrl": "https://pay.plugipay.com/c/cs_plugipay_01HX...",
"plan": "GROWTH"
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
Errors
| Status | error.code |
When |
|---|---|---|
400 |
VALIDATION |
Unknown plan, email required (and not derivable from the auth token). |
503 |
PLAN_NOT_CONFIGURED |
The target plan exists in the catalog but no underlying Plugipay product is wired up yet. This shouldn't happen in production; contact support if it does. |
const { checkoutUrl } = await ripllo.billing.checkout({ plan: 'GROWTH' });
window.location.href = checkoutUrl;
Cancel subscription
POST /api/v1/billing/cancel
Cancels at period end. The subscription stays active until currentPeriodEnd, then transitions to canceled and the effective plan drops to STARTER.
Response
The updated subscription object (same shape as Current subscription) with cancelAtPeriodEnd: true.
await ripllo.billing.cancel();
To uncancel before period end, start a fresh checkout for the same plan — the webhook handler re-activates the subscription and clears cancelAtPeriodEnd.
Events
The billing resource emits events that bridge Ripllo's local state with the upstream Plugipay billing model. These are useful for downstream automations (e.g., notify ops when a workspace hits past_due).
| Event type | Fires on | Status |
|---|---|---|
ripllo.billing.subscription_activated.v1 |
First successful charge after a checkout. | Reserved — not currently emitted. |
ripllo.billing.subscription_updated.v1 |
Plan change. | Reserved. |
ripllo.billing.subscription_past_due.v1 |
Payment failed, subscription enters past_due. |
Reserved. |
ripllo.billing.subscription_canceled.v1 |
Subscription transitions to canceled at period end. |
Reserved. |
ripllo.billing.invoice_paid.v1 |
A new invoice settles. | Reserved. |
For now, poll GET /billing/subscription and GET /billing/invoices for state changes.
Next
- API keys — mint a programmatic key to query billing from your own dashboard.
- Portal → Marketing — dashboard walkthrough of plan management.