ADR-001: PaymentIntent-Only Stripe Integration
Date: 2026-04-15 Status: Accepted
Status
Section titled “Status”Accepted
2026-04-14
Summary
Section titled “Summary”Hardcount charges cards through Stripe PaymentIntents only, never through Stripe Billing or Stripe Invoicing. Hardcount owns the invoice lifecycle, dunning (retrying failed payments) and payment status, which avoids Stripe Billing’s percentage fee (0.5% when this was written, 0.7% on pay-as-you-go today) and keeps one record of each invoice. Each charge is an off-session PaymentIntent on the card Hardcount chose (ADR-053), and one settle step records its outcome (ADR-049). The idempotency key is now hardcount_inv_{id}_{attempt}, seeded from a per-invoice attempt counter (ADR-105), not the bare hardcount_inv_{id} described below. Status: active.
Context
Section titled “Context”Hardcount is a usage-based billing engine that needs to collect payments from customers via Stripe. Stripe offers two paths: (1) use Stripe Billing/Invoicing, which handles invoice creation, payment collection, and dunning automatically, or (2) use the PaymentIntent API directly, managing invoices, dunning, and payment lifecycle ourselves.
Stripe Billing charges 0.5% on top of the standard processing fee. For a platform processing $10M/year in billing volume, that is $50K/year in additional fees passed through to our customers or absorbed. Since Hardcount is itself a billing engine, delegating invoice management to Stripe Billing would also create an awkward redundancy — two systems tracking the same invoices.
Decision
Section titled “Decision”Hardcount uses the Stripe PaymentIntent API exclusively. We create PaymentIntents with Confirm: true and OffSession: true against saved payment methods. Invoice lifecycle (draft, finalized, paid, voided), dunning (retry schedules, escalation), and payment status tracking are all managed by Hardcount.
The payment.Stripe adapter creates PaymentIntents with idempotency keys derived from invoice IDs (hardcount_inv_{id}), preventing duplicate charges on retries. Webhook handlers for payment_intent.succeeded and payment_intent.payment_failed close the loop — updating invoice payment status and triggering dunning on failures.
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Eliminates the 0.5% Stripe Billing fee for all Hardcount tenants
- Full control over invoice presentation, numbering, PDF generation, and payment terms
- No split-brain between Stripe’s invoice state and ours — Hardcount is the single source of truth
- Idempotent charge creation prevents duplicate PaymentIntents on retry
Negative
Section titled “Negative”- We own the entire dunning implementation: retry scheduling, escalation policies, email notifications
- Must handle edge cases Stripe Billing abstracts away: partial payments, webhook ordering, PI state transitions
- More surface area for payment-related bugs in a financially sensitive context
Trade-offs
Section titled “Trade-offs”- More code to maintain in exchange for zero dependency on Stripe’s billing opinions and pricing changes
- We accept the maintenance burden because billing logic is our core product, not a peripheral feature
Alternatives Considered
Section titled “Alternatives Considered”- Stripe Billing/Invoicing: Rejected due to the 0.5% fee and redundant invoice management. Also creates tight coupling to Stripe’s invoice model, limiting our ability to support alternative payment processors later.
- Stripe Subscriptions API: Rejected because Hardcount handles its own subscription lifecycle with usage-based metering. Stripe Subscriptions would fight our pricing models (tiered, graduated, volume, package).
- Correction (2026-10-23): Hardcount has no volume mode. Its usage pricing modes are
flat(a price per unit),graduated(tiers) andpackage; the database allows only these three (rating_rule_versions_mode_checkininternal/platform/migrate/baseline/00180_baseline.sql:1259, andinternal/domain/pricing.go:15-19).
- Correction (2026-10-23): Hardcount has no volume mode. Its usage pricing modes are
- Abstract payment provider interface: Rejected for v1. Hardcount is Stripe-native today. Premature abstraction would add complexity without a concrete second provider to validate the interface against.