Billing
The billing namespace is the merchant's subscription to Fulkruma itself — not the merchant's billing of their own end customers. Read plans, the current plan and subscription, usage, and invoices; start a hosted checkout to upgrade; cancel. Under the hood this is all powered by Plugipay (Pattern 2 partner-billing), but the SDK exposes a flat surface. For HTTP shapes, see API → Billing.
Namespace
fulkruma.billing # BillingResources
Seven methods: five reads, a checkout and a cancel. There's no "upgrade in place" — every plan change goes through checkout, which returns a hosted Plugipay URL.
Methods
plans
fulkruma.billing.plans(*, on_behalf_of: str | None = None) -> list
Every plan Fulkruma offers: free, starter, growth, scale. Each has an id, name, price (in IDR), currency ("IDR") and features (display strings). No credentials needed.
for p in fulkruma.billing.plans():
print(f"{p['name']} — Rp{p['price']:,}/month")
current_plan
fulkruma.billing.current_plan(*, on_behalf_of: str | None = None) -> dict
The workspace's plan and its limits: plan, planName, ordersLimit, warehousesLimit, licenseKeysLimit, apiKeysLimit, webhookEndpointsLimit, rateLimit, biteshipShipmentsLimit (each -1 for unlimited) and billingCycleEnd.
current = fulkruma.billing.current_plan()
print(current["planName"], current["ordersLimit"])
subscription
fulkruma.billing.subscription(*, on_behalf_of: str | None = None) -> dict
The subscription's state: plan, planName, status (lowercase: active, canceling, …), currentPeriodStart, currentPeriodEnd, cancelAt.
sub = fulkruma.billing.subscription()
if sub["status"] == "canceling":
show_renew_banner(sub["currentPeriodEnd"])
usage
fulkruma.billing.usage(*, on_behalf_of: str | None = None) -> dict
This month's counters against the plan: ordersFulfilled / ordersLimit, shipmentsCreated, licensesIssued, and resetAt (the start of next month).
usage = fulkruma.billing.usage()
print(f"{usage['ordersFulfilled']} / {usage['ordersLimit']} orders this month")
invoices
fulkruma.billing.invoices(
*,
limit: int | None = None,
cursor: str | None = None,
on_behalf_of: str | None = None,
) -> dict
Invoices for Fulkruma's own subscription, newest first: {"data": [...], "cursor": ..., "hasMore": ...}. limit defaults to 20, max 50. Pass the returned cursor to get the next page while hasMore is true. Each invoice has id, plan, amount, currency, status, paidAt, receiptUrl, createdAt.
cursor = None
while True:
page = fulkruma.billing.invoices(limit=50, cursor=cursor)
for inv in page["data"]:
print(inv["id"], inv["amount"], inv["status"])
if not page["hasMore"]:
break
cursor = page["cursor"]
checkout
fulkruma.billing.checkout(body: dict, *, on_behalf_of: str | None = None) -> dict
Starts a Plugipay subscription for the plan and returns the hosted page where the first payment is made. body:
| Key | Required | Meaning |
|---|---|---|
plan |
yes | "STARTER", "GROWTH" or "SCALE". |
email |
with an API key | The billing contact. A key has no email of its own; the portal fills it from the signed-in user. |
name |
no | The billing contact's name. |
currency |
no | "IDR" or "USD"; defaults by the caller's country (IDR in Indonesia, USD elsewhere). |
The result has subscriptionId, invoiceId, checkoutSessionId and checkoutUrl.
result = fulkruma.billing.checkout({"plan": "GROWTH", "email": "owner@your-store.example"})
# Redirect the merchant's browser to result["checkoutUrl"]
The hosted page handles card capture, 3DS and the partner-billing routing back to Fulkruma. When the invoice is paid, Plugipay notifies Fulkruma and the plan changes.
cancel
fulkruma.billing.cancel(*, on_behalf_of: str | None = None) -> dict
Cancels the subscription at period end — the merchant keeps the plan until the current period closes. The request carries no body; the result is the updated subscription (as subscription() returns it, with status "canceling"). A workspace with no paid subscription gets its current state back unchanged.
Common patterns
Render a billing dashboard.
def billing_dashboard(fulkruma):
return {
"current": fulkruma.billing.current_plan(),
"subscription": fulkruma.billing.subscription(),
"usage": fulkruma.billing.usage(),
"plans": fulkruma.billing.plans(),
}
Upgrade flow.
def upgrade_to(fulkruma, plan: str, owner_email: str) -> str:
result = fulkruma.billing.checkout({"plan": plan, "email": owner_email})
return result["checkoutUrl"] # your route handler returns a 302 to this
Fulkruma sends no webhook event for plan changes; re-read subscription() when the merchant comes back from the hosted page.
Errors
err.status |
err.code |
Cause |
|---|---|---|
400 |
VALIDATION |
plan not one of STARTER / GROWTH / SCALE, a malformed email, or no email at all for an API-key caller. |
403 |
NO_ACCOUNT |
The credentials resolve to no workspace. |
500 |
CHECKOUT_FAILED |
Plugipay refused to start the subscription. |
500 |
CANCEL_FAILED |
Plugipay refused the cancellation. |
503 |
PLAN_NOT_CONFIGURED |
The plan has no Plugipay price set up yet. |
Next
- Integrations — check the Plugipay link status if billing isn't working.
- API → Billing — HTTP reference.