Billing & Usage

Stripe integration & invoicing

The money-movement layer — the always-on billing summary that points at the Stripe portal, plus the experimental recipient-wallet plugin and metered-usage invoicing worker.

Payment plumbing is experimental

Only the thin billing summary below is always available and stable. The recipient-wallet plugin and the metered-usage invoicing worker are experimental and gated off by default — they do not move money unless an operator explicitly configures and enables them. Treat everything under The Stripe plugin and Metered-usage invoicing as not-yet-production.

This is the money-movement edge of the settled stage: where a Settlement actually reaches a payment provider. This page has two clearly separated parts. The billing summary is a read-only pointer that tells a dashboard where a tenant manages billing — Ductor does not operate a billing system of its own. The Stripe plugin is a separate, experimental subsystem for recipient-scoped prepaid wallets and per-decision charges. They share the word "billing" and almost nothing else.

Three things called a ledger

The Stripe wallet/charge ledger on this page moves real money for recipients. It is distinct from (a) the auction settlement pricing ledger (Commerce & pricing, Markets) and (b) the durable usage-metering plane (Usage metering). Keep them separate.

Billing summary

The billing summary is a single always-on endpoint. It reports that billing is managed externally and hands back a portal link — it is a pointer, not a billing store.

OperationMethod & pathAuthz
Get billing summaryGET /api/v2/billing/summaryresource type tenant, action read

The tenant is taken from the authenticated request context. The response is intentionally minimal:

FieldValue
managed_externallyAlways true
customer_portal_urlLink to the customer portal
currencyThe deployment's configured default billing currency (ISO 4217, lowercased; may be empty)
balance_minor_unitsAlways 0
noteHuman-readable explanation
curl -s "$BASE/api/v2/billing/summary" \
  -H "X-Tenant-ID: $DUCTOR_TENANT_ID"

Two configuration keys feed it:

  • stripe.customer_portal_url — the deployment's portal-login link. When empty, a generic portal-login link is used.
  • stripe.default_currency — the default billing currency, defaulting to usd.

The billing module is always mounted, but it only reads deployment configuration. It has no store, and there is no subscription or invoice read API — Ductor does not store tenant plans, subscriptions, or invoices. The dashboard's billing view is a link into the external customer portal, not a system Ductor operates.

The Stripe plugin

Experimental — recipient wallets, not tenant subscriptions

This subsystem charges recipients for routing decisions via prepaid wallets. It is separate from the tenant-level billing summary above and is gated behind explicit plugin configuration. There is no read API for subscriptions or invoices.

The plugin implements recipient-scoped prepaid wallets and per-routing-decision charges. It runs in one of two modes:

ModeBehavior
PrepayRecipients pre-fund a wallet; routing charges deduct from the balance; auto-top-up refills when the balance runs low
PostpayCharges accumulate and are billed on a periodic invoice

Models

ModelNotes
WalletBalanceCurrent balance and lifetime totals, in cents
PaymentModeprepay or postpay
PaymentConfigPer-recipient policy; InvoicePeriodDays defaults to 30
ChargeEvent / ChargeStatusA charge and its lifecycle state
CustomerMappingMaps a tenant/recipient to an external customer
UsageInvoiceLineA single billable line, in cents

Inbound webhook

The plugin receives external billing events and reconciles local state:

payment_intent.succeeded payment_intent.payment_failed invoice.paid invoice.payment_failed customer.subscription.deleted Webhook event event type credit wallet or update charge mark charge failed re-enable payment config handle failed invoice disable auto-top-up

The customer.subscription.deleted event disables auto-top-up for the mapped recipient, because the subscription that funded it was cancelled. This is the same webhook path that plan and subscription cancellation flows through — see Plans & subscriptions.

Metered-usage invoicing

Opt-in, off by default

The metered-usage invoicing worker is experimental and disabled by default. It only runs when explicitly enabled and fully wired — otherwise it is inert.

The worker sweeps billable usage — unit='usd' cost rows from the durable usage-metering plane — and turns each tenant's accrued spend into an invoice. Its claim/sum/mark/release surface over the usage store is:

ClaimUninvoicedUsage SumClaimedUsageMicros create invoice for tenant MarkClaimedUsageInvoiced ReleaseUsageClaim on failure Invoicing worker Usage store Stripe
  • ClaimUninvoicedUsage — stamps a claim ID on eligible billable rows.
  • SumClaimedUsageMicros — totals the claimed cost.
  • MarkClaimedUsageInvoiced — records the invoice against the claim.
  • ReleaseUsageClaim — releases a claim so the rows can be swept again.

Gating

The worker is gated off unless all three conditions hold:

  1. stripe.usage_invoicing_enabled is true (default false).
  2. A Stripe plugin is configured with a store.
  3. A usage store is wired.

The usage-invoicing module is registered unconditionally, but with the gate off or either dependency missing the worker is never constructed or stays inert — zero external traffic. Cadence is controlled by stripe.usage_invoicing_interval (default 1h) and stripe.usage_invoicing_min_age (default 1m, so in-flight bursts are not half-billed).

Configuration

All Stripe settings live under the stripe.* prefix: api_key, webhook_secret, default_currency, customer_portal_url, and the three usage_invoicing_* keys. The API key is also accepted as the plain STRIPE_API_KEY environment variable.

These are documented with types and defaults in Reference → Configuration and, for cluster deployments, in Deployment → Kubernetes / Helm — this page does not re-tabulate them.

Durability & backup

The plugin keeps four Postgres tables that hold state the external provider cannot reconstruct — local wallet balances and payment policy:

  • stripe_wallet_balance
  • stripe_charge_event
  • stripe_customer_mapping
  • stripe_payment_config

Their point-in-time-recovery and backup procedure is covered in Operations → Backup & restore.