5.9 KiB
Stripe Meters API — Operator Runbook
Audience: operators wiring usage-based billing after the Stripe SDK 13 → 19 migration (B-001, Task #28).
Why: Stripe 19 removed the legacy Usage-Records API
(subscriptionItems.createUsageRecord /
subscriptionItems.listUsageRecordSummaries) and Recurring.aggregate_usage
in favor of the Billing-Meters API
(billing.meterEvents.create / billing.meters.listEventSummaries).
The boilerplate worker (ai-cost-stripe-report) still drives overage
reporting from AICostEvent rows; only the Stripe wire format changed.
Operators must configure a Billing-Meter once and migrate existing
subscriptions to the new meter-backed price.
Step 1 — Create the Billing-Meter
In the Stripe Dashboard (or via the API):
- Billing → Meters → Create meter.
- Configure:
- Display name:
AI overage cents - Event name:
ai-overage(the boilerplate hardcodes this id; change it only if you also overrideaiOverageStripeMeterId/aiOverageMeterEventNameper plan). - Aggregation:
Sum - Customer mapping: map the event payload key
stripe_customer_idto the Stripe customer id (default — leave as-is). - Value settings: event payload key
value, typenumeric. The reporter submits the rounded overage block-count in cents.
- Display name:
- Copy the resulting Meter id (
mtr_…). You will store this inTenant.aiQuotaOverrideJson.aiOverageStripeMeterId(per-tenant) or in the plan default (per-plan, future admin UI).
CLI alternative:
stripe billing meters create \
--display-name="AI overage cents" \
--event-name="ai-overage" \
--default-aggregation[formula]="sum" \
--customer-mapping[event_payload_key]="stripe_customer_id" \
--customer-mapping[type]="by_id" \
--value-settings[event_payload_key]="value"
Step 2 — Create the metered product + price
- Products → Create product → "AI Overage".
- Add price:
- Pricing model:
Usage-based - Meter: select the meter created in Step 1 (
mtr_…). - Per-unit cost: the per-cent charge (typically
0.01USD because the meter aggregates cents). - Billing period:
Monthly(match the tenant's plan).
- Pricing model:
- Copy the resulting Price id (
price_…).
Step 3 — Migrate active subscriptions
For every active subscription that had a metered usage-record line in SDK 13.x:
-
Add a new subscription item referencing the new price from Step 2. Stripe API:
stripe subscription_items create \ --subscription="sub_..." \ --price="price_..." \ --quantity=0 -
Remove the old subscription item that referenced the pre-migration metered price (or archive its price so no new subscriptions can attach it).
-
Confirm:
stripe subscriptions retrieve sub_... --expand=items.data.priceshows the new price and the meter id inprice.recurring.meter.
The boilerplate's overage configuration uses aiOverageStripeMeterId
as the sole identifier (the legacy aiOverageStripeSubscriptionItemId
field was retired with the Stripe-19 cleanup). Existing tenant JSON
overrides that still carry the legacy key are silently ignored by the
parser.
Step 4 — Wire the boilerplate
-
Per-tenant override (admin route
/admin/settings/pricing/ai-overage):{ "aiOverageStripeMeterId": "mtr_test_abc...", "aiMarkupMultiplier": 2.0, "aiOverageBlockCents": 10 } -
Per-plan default — edit
packages/entitlements/src/ai-overage.tsstandardAIOverageDefaultsand bake the meter id into the plan blueprint (operator-friendly admin UI is a future task). -
Worker bootstrap — ensure the Stripe-19 client is constructed with
apiVersion: "2025-10-29.clover"and the worker injects aStripeMeterEventSubmitterwhosesubmit()callsstripe.billing.meterEvents.create( { event_name: "ai-overage", payload: { stripe_customer_id: tenant.stripeCustomerId, value: String(roundedBlocks), }, identifier: idempotencyKey, timestamp, }, { idempotencyKey }, );See
apps/worker/src/jobs/ai-cost-stripe-report.tsfor the pure-by-deps interface (StripeMeterEventSubmitter).
Step 5 — Verify with a test event
-
Toggle
AI_OVERAGE_SUBMISSION_ENABLED=true. -
Manually emit a test event:
stripe billing meter_events create \ --event-name="ai-overage" \ --payload[stripe_customer_id]="cus_test123" \ --payload[value]="100" \ --identifier="manual-test-$(date +%s)" -
Confirm aggregation:
stripe billing meters list_event_summaries mtr_... \ --customer="cus_test123" \ --start-time=$(date -v-1d +%s) \ --end-time=$(date +%s)You should see a non-zero
aggregated_valuefor the meter event you just submitted. -
End-to-end: run
pnpm --filter @toolctrl/worker test ai-cost-stripe-reportto confirm the worker keeps passing its 13 unit tests against the new wire.
Rollback
The Meters API is not trivially reversible — once events are
submitted, the legacy Usage-Records API cannot ingest them. The safe
rollback path is to leave already-submitted events in place and revert
the SDK to 13.x via Git revert (commit
4b2350f0c5a098d3c2dd0778f087cac5910ec2f9 and its follow-ups). The
operator-side meter + price can stay in place — Stripe ignores them
when no subscription item references them.
Open items
- Admin UI per-plan meter-id editor (currently only per-tenant via
aiQuotaOverrideJson). - Auto-creation of the meter on first operator-bootstrap (currently manual via Dashboard / CLI).
- CLI helper
toolctrl-cli ai-cost stripe-initto scaffold the meter + product + price idempotently — listed indocs/improvement-plan/05-business-features/ai-cost-phase-3.mdfollow-ups.