# Stripe integration & invoicing (/docs/billing/stripe)



<Callout title="Payment plumbing is experimental" type="warn">
  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](#the-stripe-plugin) and
  [Metered-usage invoicing](#metered-usage-invoicing) as not-yet-production.
</Callout>

This is the money-movement edge of the **settled** stage: where a
<Term name="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.

<Callout title="Three things called a ledger" type="info">
  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](/docs/billing/commerce),
  [Markets](/docs/strategies/markets)) and (b) the durable **usage-metering plane**
  ([Usage metering](/docs/billing/usage-metering)). Keep them separate.
</Callout>

## Billing summary [#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                                                                |

```bash
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 [#the-stripe-plugin]

<Callout title="Experimental — recipient wallets, not tenant subscriptions" type="warn">
  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.
</Callout>

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 [#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 [#inbound-webhook]

The plugin receives external billing events and reconciles local state:

```mermaid
flowchart TD
  A[Webhook event] --> B{event type}
  B -->|payment_intent.succeeded| C[credit wallet or update charge]
  B -->|payment_intent.payment_failed| D[mark charge failed]
  B -->|invoice.paid| E[re-enable payment config]
  B -->|invoice.payment_failed| F[handle failed invoice]
  B -->|customer.subscription.deleted| G[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](/docs/billing/plans).

## Metered-usage invoicing [#metered-usage-invoicing]

<Callout title="Opt-in, off by default" type="warn">
  The metered-usage invoicing worker is experimental and **disabled by default**.
  It only runs when explicitly enabled *and* fully wired — otherwise it is inert.
</Callout>

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

```mermaid
sequenceDiagram
  participant W as Invoicing worker
  participant U as Usage store
  participant S as Stripe
  W->>U: ClaimUninvoicedUsage
  W->>U: SumClaimedUsageMicros
  W->>S: create invoice for tenant
  W->>U: MarkClaimedUsageInvoiced
  Note over W,U: ReleaseUsageClaim on failure
```

* `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 [#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 [#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](/docs/reference/configuration#billing-entitlements--rate-limits)
and, for cluster deployments, in
[Deployment → Kubernetes / Helm](/docs/deployment/kubernetes-helm) — this page
does not re-tabulate them.

## Durability & backup [#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](/docs/operations/backup-restore).

## Related [#related]

<Cards>
  <Card title="Plans & subscriptions" href="/docs/billing/plans" description="Subscription and plan lifecycle, including cancellation." />

  <Card title="Commerce & pricing" href="/docs/billing/commerce" description="Per-recipient pricing, budgets, returns, and the settlement ledger." />

  <Card title="Usage metering" href="/docs/billing/usage-metering" description="The durable usage plane that records billable cost rows." />
</Cards>
