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 to STARTER.
  • 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.