Skip to content
Hardcount
Run it locally
Pre-1.0 · one maintainer · Stripe and cards only · no SDKs (client libraries) yet · no production users yet. Docs describe v0.9.1 (29 Oct 2026) · What’s new →

ADR-001: PaymentIntent-Only Stripe Integration

Date: 2026-04-15 Status: Accepted

Accepted

2026-04-14

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.

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.

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.

  • 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
  • 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
  • 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
  • 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) and package; the database allows only these three (rating_rule_versions_mode_check in internal/platform/migrate/baseline/00180_baseline.sql:1259, and internal/domain/pricing.go:15-19).
  • 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.