Nodea — logo

Billing — API v1 and MCP | Nodea Documentation

The Billing module is read-only over the API. It exposes what the web UI shows the customer: wallet balances, the wallet transaction ledger and invoices with their line items — and nothing more. This document describes the REST API v1 layer (/api/v1/billing/*) and the module's MCP tools.

Authentication and scopes

API v1 authenticates with a Passport personal access token via the Authorization: Bearer <token> header. All billing endpoints are guarded by the account:read scope (there is no write path). The whole module is additionally gated by the Pennant flag features:hosting-suite (mirroring the web /app/account/* groups). Rate limit: throttle:api-v1 = 240/min per token user.

Read-only — what is NOT exposed

Deliberately web-only (interactive and/or Stripe-bound) and absent from the API:

  • top-ups (Stripe Checkout),
  • payment-method management (SetupIntents + Stripe Elements),
  • auto-recharge configuration (a mutation — in the API the auto-recharge fields are read-only),
  • invoice PDF download (binary rendering, a web-only route).

Responses contain no Stripe identifiers (customer / payment method / intent) and no internals: whmcs_id, wfirma_id, pdf_path. This module never resolves a Stripe client.

Ownership and visibility

Neither Wallet nor Invoice has a policy class — row-level access is owner-scoped inline. A foreign or unknown id consistently yields 404 (never 403), so the API doesn't confirm that another tenant's row exists.

API v1 — endpoints

MethodPathScopeDescription
GET/api/v1/billing/walletsaccount:readThe user's wallets (one row per currency). Strictly read-only: does NOT lazily create the PLN/EUR pair, so without history the list may be empty.
GET/api/v1/billing/transactionsaccount:readWallet transaction ledger, newest first. Filters wallet_id and/or currency. Simple pagination.
GET/api/v1/billing/invoicesaccount:readThe user's invoices (issued_at DESC, id DESC). status filter. Simple pagination.
GET/api/v1/billing/invoices/{invoice}account:readOne invoice with its line items (items[]). Inline ownership (foreign → 404).

GET /billing/wallets — response

A WalletResource collection — fields: id, currency (PLN|EUR), balance_cents, balance_formatted (string, e.g. "123.45 EUR"), auto_recharge_enabled, auto_recharge_threshold_cents, auto_recharge_amount_cents, is_below_threshold, created_at, updated_at. The auto-recharge fields are read-only (changing them is web-only).

GET /billing/transactions — parameters and filter semantics

  • wallet_id (int, optional) — names a concrete resource, so a foreign/unknown id is 404.
  • currency (enum PLN|EUR, optional) — a plain filter: a value outside the enum is 422, a valid currency with no wallet yields an empty page.
  • per_page (int 1–100, default 20), page (int ≥1).

The two filters intersect (e.g. a PLN wallet_id with currency=EUR returns an empty page, not an error). Response: a WalletTransactionResource collection — fields: id, wallet_id, type, amount_cents (signed), balance_after_cents, currency (when the wallet relation is loaded), note, created_at. Internal fields (idempotency key, polymorphic reference, admin author, legacy WHMCS id) stay server-side.

GET /billing/invoices — parameters and fields

  • status (enum, optional) — a value outside the enum is 422.
  • per_page (int 1–100, default 20), page (int ≥1).

A InvoiceResource collection — fields: id, number, status, currency, subtotal_cents (net), vat_rate, vat_cents, total_cents (gross), issued_at, due_at (date), paid_at, period_start, period_end. In the list the items key is absent; in the invoice detail items[] is InvoiceItemResource: id, description, quantity (decimal, serialized as a string, e.g. "2.5000"), unit_price_cents, subtotal_cents, vat_cents, total_cents. Omitted: whmcs_id, wfirma_id, pdf_path and any PDF URLs.

Error codes

  • 401 — missing/invalid token.
  • 403 — missing account:read scope or inactive hosting-suite flag.
  • 404 — foreign/unknown wallet_id or invoice.
  • 422 — filter validation (e.g. currency/status outside the enum).

Examples — API

Wallets

curl -s https://app.nodea.io/api/v1/billing/wallets \
  -H "Authorization: Bearer $NODEA_TOKEN" \
  -H "Accept: application/json"

Transactions filtered by currency

curl -s "https://app.nodea.io/api/v1/billing/transactions?currency=PLN&per_page=25" \
  -H "Authorization: Bearer $NODEA_TOKEN" \
  -H "Accept: application/json"

Paid invoices

curl -s "https://app.nodea.io/api/v1/billing/invoices?status=paid" \
  -H "Authorization: Bearer $NODEA_TOKEN" \
  -H "Accept: application/json"

One invoice with line items

curl -s https://app.nodea.io/api/v1/billing/invoices/123 \
  -H "Authorization: Bearer $NODEA_TOKEN" \
  -H "Accept: application/json"

MCP — tools

MCP tools share the auth and scope model with API v1 (Bearer + scope checked in ensureScope) and the features:hosting-suite flag — a tool on an inactive flag disappears from tools/list. All are read-only (IsReadOnly/IsIdempotent annotations) and never touch Stripe. Invoice ids are integers; a missing and a foreign id raise the same error (AuthorizationException: "Invoice not found or not accessible with your permissions.") — no cross-tenant enumeration.

ToolScopeTypeDescription
billing-walletsaccount:readread-onlyWallet balances (one row per currency): balance_cents, balance_formatted, auto-recharge config. No parameters. A fresh account may return an empty list.
billing-transactionsaccount:readread-onlyTransaction ledger, newest first. Filters type and/or currency, page, per_page (max 50, default 15).
billing-invoicesaccount:readread-onlyInvoices, newest first (issued_at DESC, id DESC). status filter, page, per_page (max 50, default 15).
billing-invoice-getaccount:readread-onlyOne invoice with line items by id. Owner-scoped.

The type filter in billing-transactions accepts WalletTransactionType values (incl. topup_stripe, topup_admin, refund, debit_meter, debit_invoice, adjustment). The status filter on invoices accepts InvoiceStatus values (incl. draft, issued, paid, void, migrated_historical). Note: the MCP shapes are slightly richer than the API — billing-invoice-get returns extra per-item vat_rate, period_start, period_end (absent from InvoiceItemResource), while walletArray omits the wallet id and is_below_threshold. Internal fields (polymorphic reference, idempotency key, admin attribution, wfirma_id/whmcs_id, pdf_path) are never returned.

Example — MCP (JSON-RPC)

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "billing-transactions",
    "arguments": { "currency": "PLN", "type": "debit_meter", "per_page": 25 }
  }
}
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "billing-invoice-get",
    "arguments": { "id": 123 }
  }
}