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.
| Operation | Method & path | Authz |
|---|---|---|
| Get billing summary | GET /api/v2/billing/summary | resource type tenant, action read |
The tenant is taken from the authenticated request context. The response is intentionally minimal:
| Field | Value |
|---|---|
managed_externally | Always true |
customer_portal_url | Link to the customer portal |
currency | The deployment's configured default billing currency (ISO 4217, lowercased; may be empty) |
balance_minor_units | Always 0 |
note | Human-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 tousd.
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:
| Mode | Behavior |
|---|---|
| Prepay | Recipients pre-fund a wallet; routing charges deduct from the balance; auto-top-up refills when the balance runs low |
| Postpay | Charges accumulate and are billed on a periodic invoice |
Models
| Model | Notes |
|---|---|
WalletBalance | Current balance and lifetime totals, in cents |
PaymentMode | prepay or postpay |
PaymentConfig | Per-recipient policy; InvoicePeriodDays defaults to 30 |
ChargeEvent / ChargeStatus | A charge and its lifecycle state |
CustomerMapping | Maps a tenant/recipient to an external customer |
UsageInvoiceLine | A single billable line, in cents |
Inbound webhook
The plugin receives external billing events and reconciles local state:
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— 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:
stripe.usage_invoicing_enabledistrue(defaultfalse).- A Stripe plugin is configured with a store.
- 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_balancestripe_charge_eventstripe_customer_mappingstripe_payment_config
Their point-in-time-recovery and backup procedure is covered in Operations → Backup & restore.