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 →

Changelog

All notable changes to Hardcount, called Velox before v0.9.0, are documented here.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Pre-1.0 versioning: we’re on 0.MINOR.PATCH. MINOR bumps for new features and can break things. PATCH is for fixes, and a fix can still change something you rely on, so read Before upgrading for every release. What can break, and how it is announced: Versioning before 1.0.

This page shows the 0.9.x and 0.8.x releases. Every earlier release is in the full changelog on GitHub.

Small fixes to what you see in your first hour: the dashboard shows the Hardcount logo, and the quickstart no longer guesses how long the demo takes.

Before upgrading: nothing to do. There are no migrations and no setting changes. On a Compose install, change the image tags in .env to v0.9.1, then run docker compose pull and docker compose up -d --no-build.

  • The quickstart and its docs no longer say how long the demo takes. The “about two minutes” guess was never measured (the demo runs in seconds). docs/first-invoice.md no longer says it “matches the newest code on main” when read at a release. (#1417)
  • The dashboard shows the Hardcount logo. The sidebar, sign-in pages and browser tab icon showed the old purple lightning bolt. They now show the tally mark used on gethardcount.com. Nothing else in the dashboard’s colours changes. (#1418)

The first release named Hardcount. Usage prices belong to the plan, a usage event says when it will not be billed, prepaid credit carries its currency, and migrations run with goose. It installs fresh only: it refuses a database made by Velox or by any earlier build.

Velox is now Hardcount (#1409). The old names are not read. Rename what you run and call:

  • Commands: velox, velox-bootstrap, velox-doctor are hardcount, hardcount-bootstrap, hardcount-doctor.
  • Settings: every VELOX_* variable is HARDCOUNT_*, such as HARDCOUNT_ENCRYPTION_KEY.
  • API keys start hc_ (hc_secret_live_…, hc_platform_…), and ids too (hc_cus_…). The default invoice prefix is HC.
  • Images: ghcr.io/gethardcount/hardcount and ghcr.io/gethardcount/hardcount-dashboard.
  • Compose: services hardcount-api and hardcount-dashboard; database and admin user hardcount.
  • Limited database user: hardcount_app.
  • Metrics: hardcount_*.
  • Headers: Hardcount-Signature, Hardcount-Event-Type, Hardcount-Request-Id, Hardcount-Version; Postmark metadata hc-outbox-id.
  • Cookies: hardcount_session, hardcount_device.
  • Stripe metadata keys and idempotency keys: hardcount_*, such as hardcount_invoice_id.
  • Repository and Go module path: github.com/gethardcount/hardcount. Go code that imported github.com/sagarsuperuser/velox imports github.com/gethardcount/hardcount now.

v0.9.0 needs a fresh database (#1354, #1409). A database made by Velox is refused, and nothing in it changes. From a build of main since the switch to goose (#1198), hardcount migrate and start-up stop with this database was made by Velox; from v0.8.x or an older build of main, with no migration record.

Pricing: prices belong to the plan (ADR-130).

  • A plan lists its own usage prices. POST /v1/plans takes prices, and /v1/plans/{id}/prices adds, changes and removes one. Two plans can charge different rates for the same meter.
  • A price names its meter, the events it takes (dimension_match, string values) and its rate: per_unit, graduated (the last tier has up_to: null) or package (a part package bills as a whole one; there is no overage field). Rates are in the currency’s smallest unit, such as cents, with up to 12 decimal places. A price takes the plan’s currency.
  • The matching price with the most labels takes an event. Two prices with the same number of labels that could both take one event are refused when you save the second (409 price_conflict). There is no priority.
  • A price change on a plan a subscription has used applies from each subscriber’s next billing period. A subscription that joins the plan after the change pays the new price from then, unless it was on the plan earlier in that billing period. On a plan no subscription has used, a change applies at once. Every price write answers effective: "now" or "next_period". Test clocks follow the same rule in simulated time.
  • A meter only counts. key is now event_name. A meter has one aggregation (sum, count, max or last), fixed once the meter has a usage event (409 meter_has_usage). last_ever is gone.
  • Price overrides name a price_id and take a price’s fields.
  • A usage event’s 201 says what Hardcount found when the event arrived, in billing_status: no_price when the plan prices the meter but no price takes the event’s dimensions, or not_on_plan, outside_subscription or after_close. No billing_status is not a promise that the event is billed.
  • Provider costs are in cents: unit_cost_cents on a provider cost, provider_cost_cents on a usage event, and cost_cents in the margin report.
  • Removed: /v1/rating-rules, /v1/meters/{id}/pricing-rules, /v1/meter-pricing-rules, Meter.rating_rule_version_id, Plan.meter_ids, rule_key, the meter’s default price and meter_in_use.
  • The base fee stays locked while a subscription that is not canceled uses the plan, in test or live mode, and the billing timing once a subscription has used it. To change either, make a new plan. So a used plan that bills its base fee in_advance keeps a fee above 0.
  • base_bill_timing is required when the base fee is above 0, and must be in_arrears when it is 0.
  • Yearly plans bill usage once a year, on the invoice at the end of the year. For monthly usage beside a yearly fee, use two subscriptions (how).
  • Recipes create meters and plans only. They no longer change your dunning policy or add a webhook endpoint.

Before upgrading: v0.9.0 cannot upgrade a database. It needs a fresh one, and refuses a database from any earlier build. The notes below say what changed since a build of main from the switch to goose on (#1198), which they call “the previous release”. Their migration steps no longer apply; the API and settings changes do.

  • hardcount migrate status prints a plan now. A script that reads its old version: N, dirty: … line must change.
  • API callers: publishable keys (hc_pub_…) are removed (#1415). A request with one gets the usual 401 for an unknown key. Use a secret key, from your server only.
  • hardcount migrate force is removed, and hardcount migrate rollback runs only with APP_ENV=local. To go back, restore the backup taken before the upgrade. The restore loses everything written after the backup (Going back).
  • Hardcount’s three extensions (pgcrypto, citext, btree_gist) must be in schema public. Migrations now run with search_path = public, so an extension kept in another schema is not found, and the migration stops without changing anything (Postgres requirements).
  • If you price LiteLLM traffic yourself, add cache_write_5m and cache_write_1h prices for each Anthropic model (#1349). Cache writes billed as input; they now bill under those token types, and with no price they bill $0 and show as unmatched usage. The anthropic_style recipe already has them.
  • Don’t release held LiteLLM usage while copies of the previous release are serving: during the rolling deploy, or after rolling back (#1349). A release run by the previous release’s code does not know a call’s cache writes may already be billed inside its input row, and can bill them twice.
  • API callers: the LiteLLM spend response now carries cache_hits, and the margin report no longer carries cache_write_excluded (#1349). Nothing in Hardcount read cache_write_excluded; it was always true.
  • LiteLLM: /v1/messages (Claude Code), /v1/responses, Gemini and pass-through usage starts billing with this release (#1352). It bills from the upgrade on, also on the invoice of the period already in progress. It bills only when the call names a known customer: Claude Code and Codex must send x-litellm-customer-id. A tokens price without model charges models you have not priced.
  • Claude Opus 5.5, Sonnet 5.5, Haiku 5.5, Fable 5.1 and Mythos 5.1 arrive under their own names (#1351). Hardcount billed Opus 5.5, Sonnet 5.5, Fable 5.1 and Mythos 5.1 at the older model’s prices, and did not price Haiku 5.5. A price for an older model does not cover them: give each its own price. The anthropic_style recipe has them.
  • A Claude model name Hardcount does not know is no longer priced as an older model (#1351). For example, claude-opus-4-9 billed at Claude Opus 4 prices, and claude-sonnet-4-5-thinking at Sonnet 4.5 prices. Such a name now keeps its own name, shows as not billed and is logged, until you add a price for it (how).
  • A price on a raw Claude model name stops matching some names (#1351). Haiku 5.5, Bedrock cross-region names such as us.anthropic.claude-sonnet-4-6, converse/ and azure_ai/ names, and some Google Cloud dated names now arrive as the model’s name, such as claude-sonnet-4.6. To price a raw name, put it in model_raw, with one more label than the price it refines: {model, model_raw, token_type} beside {model, token_type}.
  • Provider costs for Claude models are looked up under the models’ new names (#1351). Opus 5.5, Sonnet 5.5, Fable 5.1 and Mythos 5.1 now arrive as claude-opus-5.5, claude-sonnet-5.5, claude-fable-5.1 and claude-mythos-5.1, not as claude-opus-5 and the like. Google Cloud @default and @date names of Opus 4.1 to 4.8, Sonnet 4.5 and Sonnet 4.6 arrive as their own model, not claude-opus-4 or claude-sonnet-4. A name Hardcount does not know, such as claude-opus-4-9, keeps its own name. A cost saved on the older model’s name does not cover them, and a cost applies only to usage that arrives after you save it. While any of a customer’s usage has no cost, their margin shows no percentage, for every model (how).
  • Migration 181 adds two columns to invoice_charge_attempts (#1225). It is instant (a constant default, no table rewrite), and copies of the previous release ignore the columns, so they keep serving while it runs.
  • Migration 182 adds one table, login_throttle (#1254). It is new and empty, so it is instant, and copies of the previous release never read it: they keep serving while it runs.
  • Migrations 183 and 184 let a credit note’s refund be unknown (#1267). 183 adds a column with no default and widens the refund_status check for new rows; both are instant. 184 checks the existing rows without blocking writes. Copies of the previous release keep serving while they run.
  • API callers: a credit note’s refund_status can now be unknown (#1267). Stripe gave no clear answer, so the customer may already be refunded. Code that switches on refund_status needs the new value. POST /v1/credit-notes/{id}/retry-refund accepts it and answers 502 payment_outcome_unknown while the result stays unknown.
  • Migration 185 adds one table, invoice_disputes (#1287). It is new and empty, so it is instant, and copies of the previous release never read it: they keep serving while it runs.
  • To record card disputes, turn on three Stripe events for your Stripe webhook endpoint (#1287). Add charge.dispute.created, charge.dispute.updated and charge.dispute.closed to the endpoint in Stripe. Without them, Hardcount never hears of a dispute (Card disputes).
  • API callers: an invoice’s attention.reason can now be dispute_lost, and a payment-timeline row’s source can be dispute (#1287). Code that switches on either needs the new value.
  • Migrations 187 and 188 record why an email was not sent (#1310). 187 adds a column with no default to email_outbox and a check for new rows; both are instant. 188 checks the existing rows without blocking writes. Copies of the previous release keep serving while they run.
  • Migrations 189 and 190 record who caused each email and charge attempt (#1314). 189 adds two columns with no default to email_outbox and invoice_charge_attempts, checks for new rows, and the charge_now trigger; all are instant. 190 checks the existing rows without blocking writes. Copies of the previous release keep serving while they run. To go back to the previous release after this one has run, first run UPDATE invoice_charge_attempts SET trigger_source = 'auto_charge' WHERE trigger_source = 'charge_now'; the previous release does not know Charge now attempts and can misjudge a late card decline (ADR-102).
  • Migration 191 drops subscriptions.usage_cap_units and subscriptions.overage_action (#1327). Dropping a column is instant, but copies of the previous release read both columns, so stop every old copy before it runs (Upgrades). A subscription that had a block usage cap bills all its usage from its next period close (see Removed). To list them, run this before you upgrade: BEGIN; SET LOCAL app.bypass_rls = 'on'; SELECT tenant_id, id, code, usage_cap_units FROM subscriptions WHERE overage_action = 'block' AND usage_cap_units > 0; COMMIT;
  • API callers: payment-timeline rows are reworded, and a row’s status is now neutral, succeeded, warning or failed (#1297). Each row also carries line, the same row in the activity line standard’s words, with its actor. Code that matches a description or switches on status needs the new values.
  • API callers: subscription timeline rows are reworded, and a row’s status is now neutral, succeeded, warning or failed (#1300). Each row also carries line, the same row in the activity line standard’s words, with its actor, and error when it has a problem. Code that matches a description or switches on status needs the new values.
  • API callers: GET /v1/subscriptions answers 400 to a status it does not know (#1341). The statuses are draft, trialing, active, canceled and archived. Before, any other value returned an empty list. status=paused matched nothing, because paused collection is not a status: use collection=paused.
  • Migration 192 clears the cancel schedule and the pending plan change from canceled and archived subscriptions (#1337). It changes no table; it updates only rows earlier cancels left. A collection pause stays. Copies of the previous release keep serving while it runs.
  • API callers: an invoice a credit note leaves with nothing due is now paid, so POST /v1/invoices/{id}/void refuses it (#1306). invoice.paid is sent for it, with amount_paid_cents 0. A note issued while a charge is running leaves the invoice open at $0; if that charge fails, the next dunning retry marks it paid.
  • A password on the built-in leaked-password list is refused when it is set (#1222). Passwords already in use keep working. If a script bootstraps or resets with a fixed password, check that it still works; a refused password gets a 422.
  • In staging and production, sign in over HTTPS (#1226). The sign-in cookie is now always Secure. Over plain HTTP only Chrome and Firefox at http://localhost keep it; Safari does not. A quickstart deploy/compose/.env written before this has no APP_ENV, so it runs in production mode: add APP_ENV=local to it, or run ./quickstart.sh destroy and start again.
  • A rate limit or RUN_MIGRATIONS_ON_BOOT that Hardcount cannot read now stops start-up (#1260). Before, HARDCOUNT_RATE_LIMIT_GENERAL_PER_MIN or HARDCOUNT_RATE_LIMIT_INGEST_PER_SEC set to 0 or to a non-number quietly used the default, and an unreadable RUN_MIGRATIONS_ON_BOOT meant false. The log names the setting: fix it, or unset it.
  • Without Redis, sign-in is now rate-limited (#1255). Outside production with no Redis, /v1/auth now answers 429 after HARDCOUNT_RATE_LIMIT_GENERAL_PER_MIN requests a minute (default 100) from one address; before, it allowed every request. A test script that signs in more often from one address needs that setting raised.
  • API callers: an email address must be one bare address, such as bob@example.com (#1259). A name, quotes, angle brackets or a second address get a 422, as does a domain with no dot. Team invites and make bootstrap now apply the dot rule too, so tenant-b@local is refused. The dashboard forms already refused these.
  • A stored address that fails this check is kept, and email to it now fails at once. For a customer, Sent emails shows failed with email: recipient is not a single valid email address; fix the address and send again. A dev owner such as tenant-b@local gets no password-reset email. A failing company email blocks saving Settings until fixed.
  • On a Compose install, set the idle-transaction timeout before you start this release (#1258). New data volumes get it; an existing one does not, because Postgres runs its setup script only on an empty volume. From deploy/compose: docker compose exec postgres psql -U hardcount -d hardcount -c "ALTER ROLE hardcount_app SET idle_in_transaction_session_timeout = '30s'".
  • With CORS_ALLOWED_ORIGINS=*, a dashboard at another address than the API cannot sign in: list its address instead, such as https://app.example.com (#1262). * now allows any site without the sign-in cookie. An entry with a * inside, such as https://*.example.com, or * beside other addresses, stops start-up: list each address in full.
  • An OPTIONS request that is not a CORS preflight now gets the route’s own answer, such as 404 or 405, not 204. A load-balancer health check that sends OPTIONS, as HAProxy’s option httpchk does by default, must use GET /health/ready.
  • After the upgrade Hardcount asks Stripe about every parked invoice, and charges again an open one with no payment at Stripe for 24 hours (#1307). Write off first any the customer paid another way: SELECT tenant_id, id, invoice_number FROM invoices WHERE payment_status = 'unknown' AND COALESCE(stripe_payment_intent_id, '') = '';. Migration 186 matters only for an install that ran main between 2026-10-19 and this change: it gives back the charge hold that code cleared from a parked invoice. Elsewhere it changes nothing.
  • API callers: read routes now refuse what they used to drop (#1335). An unknown customer or meter id on the credit balance, ledger and grants, usage summary, sent emails and card status reads is 404. A from or to on GET /v1/usage-events or /aggregate that is not RFC 3339 or YYYY-MM-DD is 422, and so is a provider cost rate in a currency other than USD. Code that relied on the old answers must change. Cost and margin count every saved rate as dollars, so save again in USD any rate this finds: SELECT tenant_id, provider, model, token_type, currency FROM provider_cost_rates WHERE currency <> 'USD';.
  • Behind a proxy, Hardcount reads the client address from X-Forwarded-For only (#1315). X-Real-IP is no longer read. If your proxy sends only X-Real-IP, make it send X-Forwarded-For too (nginx: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;), or every request counts as the proxy’s address. The Compose nginx already sends both.
  • Migrations 193 and 194 record the invoice whose dunning paused a subscription’s collection (#1340). 193 adds an empty column to subscriptions and a check, both instant; every pause already set stays yours. 194 checks the existing rows without blocking writes. Once 193 has run, a copy of the previous release can no longer clear a pause that the new release’s dunning set: the check refuses its write with an error. A pause it sets over that one stays tied to the invoice. Upgrade every copy together.
  • API callers: pause_collection now carries set_by, operator or dunning, and subscription.collection_resumed can carry triggered_by: payment (#1340). Code that switches on triggered_by needs the new value.
  • Migrations 195 to 198 add invoices.below_minimum_at, the dunning resolution amount_below_minimum and the charge attempt outcome below_minimum (#1355). 195 adds an empty column and widens a check for new rows, and 197 widens another, all instant. 196 and 198 check the existing rows without blocking writes. Copies of the previous release keep serving while they run.
  • API callers: a failed payment the customer’s bank wants them to confirm has the attention code payment.authentication_required (it was payment.declined), with only the send_reminder action (#1360). Code that matches on payment.declined needs the new code.
  • While copies of the previous release still send email, a reminder to confirm a payment can go out as the ordinary retry warning, with a retry date (#1360). That retry charges nothing. Upgrade every copy together.
  • API callers: amount_below_minimum is a new value of the invoice attention reason, the dunning run resolution (also in dunning.resolved) and the hosted page’s pay_unavailable_reason, Collect payment can answer 409 with that code, and a payment timeline row’s event_type can be charge_attempt.below_minimum (#1355). Code that switches on these needs the new value.
  • Writing off a spend-threshold invoice no longer has the period’s invoice bill its charges again (#1353). If you relied on that, void the threshold invoice instead, before the period’s invoice is built.
  • Migrations 199 and 200 give every credit ledger row a currency (#1361). 199 fills it for existing rows with the currency each customer’s balance was shown in, then requires one on new rows; 200 checks the rows and makes the column NOT NULL. Once 199 has run, a copy of the previous release cannot write credit: stop the old copies before you migrate.
  • API callers: credit is per currency (#1361). POST /v1/credits/grant and /adjust require currency. GET /v1/credits/balance/{id} returns {data: [...]}, one balance per currency; the live balance returns balances; grants return remaining_by_currency; the cost dashboard’s credits is an array; analytics returns credit_balance_by_currency instead of credit_balance_total. Ledger rows and credit.* webhooks carry currency.
  • API callers: POST /v1/invoices refuses a currency that is not the customer’s, with 409 customer_currency_conflict (#1361). The billing profile’s and every live subscription’s plan currency must agree with it. With no currency sent, the invoice takes the profile’s, then the plan’s, then the workspace default.
  • Migrations 201 and 202 remove values and a table that no Hardcount code writes (#1365). 201 makes draft plans active (a draft plan already billed like an active one). No Hardcount version wrote the other removed values; if a row holds one, 201 stops without changing anything and lists the rows (runbook). Rows an earlier version wrote stay as they are. 202 checks the existing rows without blocking writes. Once 201 has run, a copy of the previous release cannot delete a test clock (it deletes from billed_entries): stop the old copies before you migrate.
  • API callers and webhook receivers: an invoice paid outside Hardcount has no stripe_payment_intent_id, and has paid_out_of_band: true (#1370). Before, Record payment and a recovery run resolved as payment_recovered wrote out_of_band:<time> there, which is not a Stripe id. Code that looked for that prefix should read paid_out_of_band. Every invoice now has the field. The same goes for recorded_payment_intent_id in payment.duplicate_charge, payment.amount_mismatch, payment.currency_mismatch and payment.received_on_voided_invoice: it is empty for such an invoice, and these events now carry paid_out_of_band.
  • Migrations 203 and 204 move offline payments off the PaymentIntent column (#1370). 203 adds invoices.paid_out_of_band, turns each out_of_band: value into paid_out_of_band with no PaymentIntent, deletes the charge-attempt rows stored under that value, and refuses the value from then on. Amounts, status and dates do not change. While 203 reads every invoice and charge attempt, invoice reads and writes wait. 204 checks the existing rows without blocking writes. Stop the old copies before you migrate: on one, Record payment and resolving a recovery run as payment_recovered fail.
  • API callers: reset_billing_cycle is removed from spend thresholds (#1371). PUT /v1/subscriptions/{id}/billing-thresholds refuses it, true or false, with 422: drop it from the body. billing_thresholds no longer carries it, and subscription.threshold_crossed no longer carries reset_cycle.
  • Migration 205 drops subscriptions.billing_threshold_reset_cycle (#1371). Dropping a column is instant, but copies of the previous release read it, so stop every old copy before it runs. A subscription that had it true keeps its thresholds and keeps its period from its next threshold invoice on. To list them first: BEGIN; SET LOCAL app.bypass_rls = 'on'; SELECT s.tenant_id, s.id FROM subscriptions s WHERE s.billing_threshold_reset_cycle AND (s.billing_threshold_amount_gte IS NOT NULL OR EXISTS (SELECT 1 FROM subscription_item_thresholds t WHERE t.subscription_id = s.id)); COMMIT; On one billed in advance on anniversary dates whose current period a threshold invoice started, the base from the old period’s end to the new one’s (May 1 to 20 after an April 20 threshold in April) is on no invoice: bill it with a one-off invoice if wanted.
  • A spend threshold now counts and bills usage only (#1371). Its invoice no longer carries the base fee; the period’s own invoice bills it, prorated on an immediate cancel. An amount_gte that counted the base is reached later, by the base fee: lower it by the base to keep the same point. running_subtotal in the subscription.threshold_crossed webhook and the subscription.threshold_deferred audit entry no longer counts the base either. A threshold invoice issued before the upgrade keeps its base, and it is not billed again.
  • API callers: before you upgrade, add "status": "draft" to every POST /v1/subscriptions that should wait for /activate (#1372). A create with no start_now and no trial made a draft. The same request now makes an active subscription, invoices a non-zero base fee billed in advance on day 1, and charges it to the customer’s saved card if they have one. A later /activate then answers 409. Remove start_now: it is now a 422.
  • API callers: send quantity on every usage event, with at most 12 decimal places, and send dimensions, not properties (#1374). A missing quantity was stored as 0, extra decimal places were rounded off, and properties was used as dimensions; each is now a 422 (a properties that is null or {} is ignored). A quantity that is not a number or a numeric string was a 400 for the whole body; it is now a 422 that names the event.
  • API callers: an invoice line’s quantity is now the exact decimal string, and quantity_decimal and unit_amount_cents are gone (#1374). Read quantity (it was the integer, truncated) and unit_amount_decimal. The hosted invoice data at GET /v1/public/invoices/{token} changes the same way.
  • API callers: send only the fields the API reference lists (#1399). Any other field in a request body, including one a GET returns but the server sets (id, tenant_id, created_at, updated_at), is now a 422 naming it. So is a name in another case (Email for email), and any field sent to a POST, PUT, PATCH or DELETE operation that takes no body, such as /cancel. Remove it from the body. In a usage batch, errors[] names each event that has one. A body that is not valid JSON is now a 400 on dunning policy create and PATCH and on POST /v1/invoices/create_preview (it was 422). Hardcount stores a 400 against its Idempotency-Key, so send the corrected body with a new key.
  • API callers: page every list with limit and offset, and read total (#1402). The after cursor and has_more/next_cursor are gone from the audit log, invoice and usage-event lists; a request that still sends after gets a 422. limit is 1 to 100 (default 50) on every list; a larger, negative or non-number limit or offset is a 422 naming it (it was clamped, or a 500 for a negative offset). The usage-event list took up to 1000. Lists that answered only data now page too, except GET /v1/credits/balance/{customer_id}, and GET /v1/customers/{id}/sent-emails answers data (was sent_emails). GET /v1/webhook-endpoints/endpoints/{id}/deliveries no longer carries endpoint: read it from the new GET /v1/webhook-endpoints/endpoints/{id}. Held usage, the two timelines and four other sets answer in one response and do not page; Lists names them.
  • API callers: read every error as {"error": {"type", "code", "message", "request_id", "param"}} (#1402). Idempotency-Key errors moved their reason from type to code (type is invalid_request_error; bad_request is now invalid_request). GET /v1/auth/password-reset/check answers a 422 error with code invalid_or_expired, not {"valid": false}. An unknown path, a wrong method, a panic, a timeout and /metrics 401 answer the envelope, not plain text or an empty body.
  • API callers: send a dunning policy’s schedule as retry_days (#1375). It lists the days to wait before each retry: grace_period_days: 3, retry_schedule: ["96h", "168h"], max_retry_attempts: 3 is retry_days: [3, 4, 7]. Create and PATCH refuse the three old fields with 422, and responses no longer carry them.
  • Migrations 206 and 207 rewrite each dunning policy’s schedule as retry_days (#1375). Runs retry on the same days. A policy whose retries use a wait that is not whole days from 1 to 30, such as "36h", stops 206 without changing anything (runbook). Copies of the previous release read the dropped columns: stop every old copy before you migrate.
  • Webhook receivers: every event’s data changed. It now holds the resource the event is about, as the API returns it, under the resource’s name (#1377, ADR-131). For example, invoice.paid sends the whole invoice as data.invoice. Change your receiver to read the resource; the catalog lists the few keys an event adds beside it. An event queued before you upgrade is always sent with its old data: on its retries (up to about 26.5 hours) and on any replay. Accept both shapes for that time, and on a replay of an older event. Old key → new key:
    • invoice.finalized, invoice.voided, invoice.marked_uncollectible, invoice.paid, payment.succeeded, payment.failed: each key moves under data.invoice with its name, except invoice_id → invoice.id, amount_cents (the invoice total) → invoice.total_amount_cents, payment_intent_id → invoice.stripe_payment_intent_id and failure_message → invoice.last_payment_error.
    • invoice.payment_recorded: invoice_id → invoice.id, amount_cents → invoice.amount_paid_cents, recorded_at → invoice.paid_at; invoice_number, customer_id and currency → under invoice; recovered_from stays.
    • payment.duplicate_charge, payment.amount_mismatch, payment.currency_mismatch, payment.received_on_voided_invoice: invoice_id → invoice.id, recorded_payment_intent_id → invoice.stripe_payment_intent_id; invoice_number, customer_id, currency, paid_out_of_band and amount_paid_cents → under invoice; incoming_payment_intent_id and captured_cents stay.
    • payment.dispute_opened, payment.dispute_closed: the dispute’s keys → under data.dispute, with dispute_id → dispute.id and payment_intent_id → dispute.stripe_payment_intent_id; invoice_id → invoice.id; invoice_number and customer_id → under data.invoice.
    • payment_method.attached: customer_id → payment_method.customer_id; stripe_payment_method_id stays. payment_method.updated: customer_id, card_brand and card_last4 → under payment_method; stripe_customer_id stays.
    • All subscription.* events: subscription_id → subscription.id; customer_id and status → under subscription; current_period_start and current_period_end → subscription.current_billing_period_start and _end; item_count → the length of subscription.items. canceled_at, cancel_at, cancel_at_period_end and cancel_effective_at → under subscription; trial_end → subscription.trial_end_at; behavior and resumes_at → subscription.pause_collection.
    • subscription.item.* and subscription.pending_change.*: item → item_id (an event that had item_id keeps it), with the item in subscription.items; new_plan_id and effective_at → the item’s pending_plan_id and pending_plan_effective_at, or its plan_id once applied. canceled_by, reason, ended_at, triggered_by, resumed_at, old_plan_id, applied_at and the subscription.threshold_crossed keys stay. subscription.collection_resumed from the API now also carries triggered_by: operator.
    • customer.email_bounced, customer.email_complained: customer_id → customer.id; reason stays.
    • dunning.started, dunning.escalated, dunning.resolved: run_id → dunning_run.id; invoice_id, customer_id and resolution → under dunning_run; attempts → dunning_run.attempt_count; final_subscription_action, final_invoice_action and applied stay.
    • credit.balance_low, credit.balance_depleted, credit.balance_recovered, credit.commit_retired: customer_id, currency and balance_cents → under credit_balance; threshold_cents and the other credit.commit_retired keys stay.
  • Migrations 208 and 209 give each meter one aggregation (#1376). 208 drops meter_pricing_rules.aggregation_mode, renames meters.key and usage_held_events.meter_key to event_name, and checks meters.aggregation for new rows; 209 checks the existing rows. Each is instant, but copies of the previous release read the old columns, so stop every old copy first. If a pricing rule’s mode is not its meter’s aggregation, or a meter’s aggregation is not sum, count, max or last, 208 stops before changing anything: install fresh.
  • API callers: a meter’s key is now event_name (#1376). Send event_name to POST /v1/meters; a meter, a held-usage group, customer usage and the public cost dashboard carry event_name. The held-usage discard body’s meter_key, analytics’ top-meter key and the recipe preview’s meters[].key, pricing_rules[].meter_key and plans[].meter_keys (now event_names) are renamed too. The discard answers an unknown field, meter_key included, with a 422 and discards nothing.
  • API callers: a PATCH /v1/meters/{id} that changes the aggregation of a meter with usage events is a 409 meter_has_usage (#1376). A price bills under its meter’s aggregation.
  • Migrations 210 and 211 give the price shapes their new names (#1381). 210 renames the rate columns of rating rules, price overrides and provider cost rates, and drops overage_unit_amount_cents; 211 checks the existing rows. If any of those tables has a row, or an invoice line is priced flat, 210 stops before changing anything: install fresh. Copies of the previous release read the old columns: stop every old copy first.
  • API callers: send prices and provider costs in the new words (#1381). Old → new: mode: "flat" → per_unit; flat_amount_cents → unit_amount_cents; graduated_tiers → tiers, last tier up_to: null (not 0); package_amount_cents is a decimal string; cost_per_token (dollars) → unit_cost_cents (cents). A request with an old word is a 422 naming it. Responses: provider_cost_micros and cost_micros → provider_cost_cents and cost_cents.
  • Migration 212 moves usage prices from meters to plans (#1386). It adds plan_prices and their versions, points price overrides and invoice lines at a price, and drops rating rules, meter pricing rules and the plan’s meter list. If any of those has a row, 212 stops before changing anything: install fresh. Stop every old copy first.
  • API callers: usage prices are set on the plan (#1386). /v1/rating-rules, /v1/meters/{id}/pricing-rules and /v1/meter-pricing-rules are gone: send prices to POST /v1/plans, or use /v1/plans/{id}/prices. A plan has prices, not meter_ids. A price override names a price_id. base_bill_timing is required with a base fee.
  • API callers: a recipe’s plans carry their prices (#1389). A recipe, its detail and its preview list meters and plans, and each plan lists its prices (meter, dimension_match and the rate). Gone: rating_rules, pricing_rules, dunning_policy and webhook on a recipe, event_names on its plans, and prices, dunning_policies and webhook_endpoints in the preview. creates counts meters, plans and prices.
  • Operators: hardcount_usage_after_close_total{origin} is now hardcount_usage_unbilled_total{reason="after_close", origin} (#1390). Change the alerts and dashboards that read it.
  • API callers: billing_status on a usage event’s 201 has three more values, no_price, not_on_plan and outside_subscription (#1390). Code that switches on it needs them. A trial event, and an event of a customer with no active or trialing subscription, now carry outside_subscription. An event in an invoiced period of an active or trialing subscription is after_close whatever the plan prices now, unless another subscription’s period not invoiced yet holds its time.
  • API: an item change says where its credit for unused time went (2026-10-28, #1406). proration has credited_to_balance_cents, reduced_open_invoices_cents and held_on_draft_invoices_cents. A change now to a plan with another billing interval, billed in advance, returns new_period_invoice: the invoice it created for the new period, with its number, status and total. Billed in arrears, it creates no invoice then and the field is absent.
  • A usage event’s answer says when Hardcount found a reason it is not billed (2026-10-27, #1390). Its 201 carries billing_status: after_close, not_on_plan, outside_subscription or no_price (no price of the plan takes its dimensions). It is what Hardcount found when the event arrived; no status is not a promise that the event will be billed. The batch answer gives each event’s result at its index in results, the batch and LiteLLM answers count each status, the held-usage release counts no_price, and hardcount_usage_unbilled_total{reason, origin} counts them.
  • Webhook receivers: payment.currency_mismatch carries charge_currency, the charge’s currency, beside the invoice (2026-10-25, #1384). Compare it with the invoice’s currency.
  • Webhook receivers: dunning’s pause now sends subscription.collection_paused with triggered_by: dunning, and an operator’s pause carries triggered_by: operator (2026-10-25, #1384). Before, a dunning pause sent no event. Dunning never resumes collection itself; paying the invoice does (triggered_by: payment), unless the subscription was canceled.
  • The anthropic_style recipe prices Claude Opus 5.5, Sonnet 5.5, Haiku 5.5, Fable 5.1 and Mythos 5.1 (2026-10-22). It is version 6.0.0, with Sonnet 5 at Anthropic’s $2 / $10. Haiku 5.5 has two prices: a prompt over 100,000 tokens (input, cache reads and cache writes) pays more, and its LiteLLM events carry context_tier. Provider costs have one cost per model and kind of token, so one Haiku 5.5 cost covers both prices, and the margin on prompts over 100,000 tokens is wrong (more). New installs only (#1351).
  • Usage that arrives after its period was invoiced is now flagged, on active and trialing subscriptions (2026-10-22). It is still stored and billed on no invoice. The event’s 201 carries billing_status: "after_close", batch and LiteLLM answers count after_close, and hardcount_usage_after_close_total counts them. Not flagged: usage on a canceled subscription, and usage before a spend-threshold invoice that is not paid yet (if that invoice is voided before the period closes, the period’s invoice bills it; a write-off does not do that). If the billing-period read fails, ingest answers 503 ingest_unavailable (late usage) (#1350).
  • API: the held-usage release answer has pending_threshold_invoice (2026-10-22). It counts released rows before the end of an unpaid spend-threshold invoice. Rows before a paid one’s end now count as outside_open_period; both were counted as in_open_period (#1350).
  • Credit note numbers can now start with your own prefix (2026-10-22). Set it under Settings → Invoicing, or send credit_note_prefix to PUT /v1/settings. The default stays CN, and it must differ from the invoice prefix, or an invoice and a note would share a number. New notes get the prefix; issued notes keep their numbers, and the count carries on (#1339).
  • API: GET /v1/credit-notes/stats counts every credit note by status and sums the issued ones per currency (2026-10-22). (#1339)
  • API: GET and PUT /v1/settings return next_invoice_sequence (2026-10-22). It is the number the next invoice in the request’s mode gets. Settings → Invoicing shows it in the prefix preview, which always said -000001 before. (#1323)
  • Each email and charge attempt now records who caused it, and Charge now is told apart from automatic charges (2026-10-20). The person, API key, customer or Hardcount itself, as in the audit log. Rows from before the upgrade record no one. Nothing on screen changes yet; the Activity cards name them in a later release. (#1314)
  • API: a dunning run’s events and a customer’s sent emails now carry line (2026-10-20). It is the row in the activity line standard’s words, the same as on the invoice’s payment timeline. GET /v1/dunning/runs/{id} also returns degraded, naming any source it could not read. The dashboard pages show the lines in a later release. (#1310)
  • New metrics show when emails or webhooks stop going out (2026-10-19). For operators: page on hardcount_email_outbox_failing_emails (an SMTP relay that keeps refusing mail), hardcount_email_outbox_oldest_due_age_seconds and hardcount_webhook_outbox_oldest_pending_age_seconds in live mode. Before, a refused sender was retried for 72 hours, then dropped with one log line (alerts). (#1304)
  • A webhook event that could not be queued is now counted (2026-10-27). hardcount_webhook_outbox_enqueue_failures_total, by event_type, counts each event the outbox could not store; no endpoint gets it. Every series starts at 0, so the first one alerts. (#1316)
  • Emails and webhook events given up on are now counted (2026-10-19). hardcount_email_outbox_dead_lettered_total and hardcount_webhook_outbox_dead_lettered_total, by cause and mode, count each row once, when it is marked failed. Every series starts at 0, so the first one alerts. (#1304)
  • CI now uploads screenshots of the activity cards on every run (2026-10-19). For contributors: the invoice and subscription Activity cards, a Dunning run and Sent emails, on the real clock and on a test clock, as the activity-shots artifact. The browser tests now send email to Mailpit (how to compare). (#1296)
  • Card disputes from Stripe are now recorded against the invoice (2026-10-19). For anyone collecting by card: the charge.dispute.created, .updated and .closed events update the record in any order, and payment.dispute_opened and payment.dispute_closed are sent. A lost dispute is flagged on the invoice, and names commit relief when the invoice bought prepaid credit. The invoice stays paid and no money moves (runbook). (#1287, fixes #979)
  • Repeated wrong passwords now slow down dashboard sign-in, instead of locking the account (2026-10-18). After 5 wrong tries from one address, or 10 for one email from many, each try waits 1 to 15 minutes, and the page says how long. A browser that signed in before is not slowed by other people’s guesses. A new browser waits while the guessing goes on: a password reset from it lets it in, which needs email set up; without email, DELETE FROM login_throttle; clears every wait. No setup and no Redis. (#1254)
  • make migration writes the next migration file, and -short tests check every file after 180 (2026-10-18). For contributors: the name and number, an Up and a Down, SET LOCAL only, the one index-build shape, and VALIDATE CONSTRAINT in a file of its own. Steps: Writing a database migration. (#1212)
  • The migration checks now refuse two ADD COLUMN forms that break an upgrade with data (2026-10-18). For contributors: NOT NULL with no DEFAULT, which fails on a table with rows, and a value computed for each row, such as DEFAULT gen_random_uuid(), which rewrites the table under a lock. Safe forms: Common changes. (#1257, fixes #1231)
  • API: an item change’s proration.type is credit whenever part of the credit goes to the customer’s balance (2026-10-28, #1406). That includes customer credits an open invoice hands back when its amount due drops below them. It was adjustment there.
  • API clients generated from the spec: fields the server always sends are now required (2026-10-27, #1405). The JSON is unchanged. Generated types make them non-optional: paid_out_of_band and is_simulated on invoices, and fields of customers, meters, plans, prices, price overrides, provider costs and the public cost dashboard, among others (the PR lists them). DELETE /v1/price-overrides/{id} is documented as returning the whole override. POST /v1/billing/run is documented as answering 206 when errors is not empty, with what each entry means. POST /v1/auth/password-reset/confirm is documented as the 200 with message it has always sent; the spec said 204. The public cost dashboard’s text no longer says every array is empty without a subscription: credits still lists the balance.
  • Breaking for API callers: every list pages one way, limit and offset in and {data, total} out (2026-10-27, #1402). Meters, plans, price overrides and test clocks stopped at 500 rows with no sign; a test clock’s customers and subscriptions at 1,000; a customer’s payment methods at 100; API keys, tenants and webhook events at 50; credit notes had no total. Every list route now reaches its last row, except the customer list with search or sorted by name or email, which looks at 5,000 customers at most. Held usage, the two timelines and four other sets do not page; Lists names them and their limits. The dashboard reads every page where it needs the whole list, and the audit log and an endpoint’s deliveries page with Previous and Next.
  • Breaking for API callers: every error answers the one JSON envelope (2026-10-27, #1402). An unknown path is a 404 route_not_found, a wrong method a 405 method_not_allowed with Allow, a panic a 500 internal_error and a time-out a 504 timeout; each was plain text or empty. Idempotency, usage batch, password-check, Postmark webhook and live-event stream errors carry code and request_id too.
  • Breaking for API callers: every request body refuses a field its operation does not list (2026-10-27, #1399). A misspelled or unknown field, a name in another case, or one the server sets such as id, is a 422 naming it, and nothing changes. A POST, PUT, PATCH or DELETE operation that takes no body refuses one with any field. Before, most routes dropped it: a PATCH with a wrong field name answered 200, and {"at_period_end": true} to /cancel canceled at once. The Stripe and Postmark webhooks and the LiteLLM callback take any field.
  • The audit row subscription.net_negative_usage names each meter by event_name (2026-10-27). Its meters entries had meter_key, the old name of the meter’s event_name. Rows written before the upgrade keep meter_key, and the activity line still names their meters.
  • API callers: customer usage over a window you choose (from and to) no longer adds an “Estimate” line to warnings (2026-10-27). period.source: "explicit" says the amounts are an estimate at each subscription’s current prices, and count only active and trialing subscriptions; warnings holds only problems. On the customer page, the Activity card’s 7, 30 and 90-day views and the Margin card say they are an estimate that leaves out canceled subscriptions, and the Activity card no longer shows “Usage may be incomplete” for them.
  • The price override form lists the prices of the customer’s own plans first (2026-10-27).
  • Breaking for webhook receivers: each event carries the resource it is about (2026-10-24, #1377). data holds the invoice, subscription, customer, payment method, dunning run or credit balance as the API returns it, plus the few facts the resource lacks. Every event of a family carries the same resource. The API’s payment-method rows now carry customer_id. A Stripe event that arrives after a card was removed in Hardcount no longer sends payment_method.attached or payment_method.updated. Resuming collection that is not paused, or clearing a cancel that is not scheduled, changes nothing and now sends no event; the clear also writes no audit log entry. See Before upgrading for each moved key.
  • Held usage no longer fixes a meter’s aggregation; only usage events do (2026-10-24, #1379). A meter made with the wrong aggregation while its usage was held can now be changed. has_usage is true only once the meter has usage events.
  • Breaking for API callers: request decimals are validated with one helper, and each field’s limits are documented (2026-10-24, #1378). Price and price-override rates: 0 or more, under 10^18 cents. A provider cost’s unit_cost_cents: required, 0 or more, under 10^10. usage_gte: more than 0, under 10^26. Each, like quantity, has at most 12 decimal places and 100 characters. A value outside these is a 422 naming the field. Valid values bill as before.
  • Breaking for API callers: a usage event’s quantity is checked as the API reference says (2026-10-23, #1374). It is required on all three ingest routes (backfill used 0 for a missing one): a JSON number, or a string holding one, with at most 12 decimal places and a size under 10^26. A negative value is still a correction. Anything else is a 422 naming quantity, and a batch names each failing event. Valid events bill as before.
  • Breaking for API callers: a dunning policy’s schedule is one list of days, retry_days (2026-10-23, #1375). The number of retries is its length, so the count, the first wait and the other waits can no longer disagree. Each wait is whole days from 1 to 30, at most 15 retries. The dashboard’s policy editor has one field, and the bundled recipes use retry_days (new major versions). A new policy in the dashboard now starts at day 3, 7 and 14, the seeded default’s days (it was day 3, 6 and 11).
  • Breaking: a meter has one aggregation, fixed once the meter has a usage event (2026-10-23, #1376). Every price of the meter bills under its sum, count, max or last. Before its first usage event it can change, even while subscriptions use the meter. A last meter’s preview and invoice now pick the same event when two share a timestamp (the higher id). The meter page shows the lock (has_usage).
  • The built-in recipes are now at 8.0.0 (anthropic_style), 7.0.0 (openai_style) and 5.0.0 (replicate_style) (2026-10-24, #1376). A recipe names its meter by event_name, and its prices have no aggregation_mode. Prices are unchanged.
  • Breaking: a package price bills a part package as a whole one (2026-10-25, #1381). It bills ceil(quantity ÷ package_size) packages, and its amount can be a fraction of a cent: $0.002 per 1,000 tokens is package_amount_cents: "0.2". Before, units past the last whole package billed at overage_unit_amount_cents, which is gone. Other prices bill as before.
  • Breaking for API callers: one unit rule for prices and provider costs (2026-10-25, #1381). Every *_cents field is in the currency’s smallest unit, and a price is per_unit, graduated or package (ADR-130). A graduated price’s last tier is up_to: null, and only the last. A price takes only its mode’s fields: tiers on a per_unit price, for example, is a 422 naming it. A provider cost rate is US cents per token. Invoice lines say pricing_mode: "per_unit".
  • The built-in recipes are now at 9.0.0 (anthropic_style), 8.0.0 (openai_style) and 6.0.0 (replicate_style) (2026-10-25, #1381). Their rates are mode: per_unit with unit_amount_cents. Prices are unchanged.
  • Breaking: a usage price belongs to a plan, so two plans can charge different rates for one meter (2026-10-25, #1386). The price with the most labels takes an event; two that could tie are refused when saved. A price change applies at once on a plan no one has used, else from the next period of each subscriber already on the plan, on a test clock too (ADR-130).
  • The built-in recipes are now at 10.0.0 (anthropic_style), 9.0.0 (openai_style) and 7.0.0 (replicate_style) (2026-10-26, #1389). A recipe file holds only meters and plans with their prices. Prices are unchanged.
  • A subscriber pays a plan as it stands when they join it (2026-10-26, #1388). A subscription that starts after a price change, an immediate move onto the plan and an item added on it are priced at the plan’s prices of that moment, from the first period. After a trial, the moment is the trial’s end. Before, they paid the prices of their period’s start until the next period. A subscription that leaves the plan and joins it again in the same period keeps the prices of its first join until its next period. An override created before the join, even that day, applies from the first period. On a test clock, a change or override takes effect just after the clock’s time, so a subscriber who joins at that same clock time gets the earlier prices. A subscriber already billing on the plan gets a change from its next period, on a test clock too (ADR-130 §4). An item added after its period ended, before that period was billed, no longer bills that period’s usage.
  • A price override names one plan price and follows the customer’s clock (2026-10-25, #1386). It keeps applying when the price’s rate changes. Removing the price ends it, and the answer lists the overrides it ended. On a test clock, a new or ended override follows the clock’s time.
  • A plan’s base fee can be set to 0 (2026-10-25, #1386). A plan with no base fee bills at period end, so in_advance with a base of 0 is refused. base_bill_timing is fixed once a subscription has used the plan.
  • Customer usage and the cost dashboard price usage the way the invoice does (2026-10-25, #1386). Customer usage lists prices per plan price, with a no_price row for usage no price of the plan takes. Each subscription’s whole current period is priced as its invoices price it, threshold invoices included, and the period shown spans all of them; a window you pick is an estimate at today’s prices.
  • The dashboard edits usage prices on the plan’s page (2026-10-25, #1386). The page says before you save whether a change applies at once or from each subscriber’s next period. A meter’s page lists the plans that price it. The Prices tab is gone.
  • The Provider costs page takes and shows dollars per 1M tokens (2026-10-25, #1381). It was dollars per token. The price forms show a graduated price’s last tier as “No limit”.
  • Breaking for API callers: a new subscription starts now unless you create it as a draft (2026-10-23, #1372). Before, POST /v1/subscriptions without start_now: true and without a trial made a draft that billed nothing until activated. Now it is active (or trialing) at once and a non-zero base fee billed in advance is invoiced on day 1. Old → new: start_now: true → leave it out; no flag → status: "draft". start_now, an empty or null status, and status: "draft" with a trial are a 422. The dashboard’s “Start immediately” box is now “Create as draft”, off by default.
  • A credit pays only invoices in its own currency (2026-10-23). Each grant, adjustment and credit-note credit is in one currency, and a balance in two currencies shows as two balances, never added together or converted. Before, USD credit paid a EUR invoice cent for cent. Grant and Deduct ask for the currency, the low-balance alert amount applies in each currency, and a credit note’s PDF names the currency its credit pays (ADR-129, #1361).
  • A manual invoice uses the customer’s currency (2026-10-23). One in another currency is refused, and an invoice with no currency takes the live subscription’s plan currency when the customer has no billing-profile currency; before, it took the workspace default. In the dashboard, New invoice then shows the currency fixed (#1361).
  • New page: what can break before 1.0, and where each break is announced (2026-10-23). See Versioning before 1.0. SECURITY.md now says only the latest release gets security fixes. (#1363)
  • The roadmap is now Now, Next, Later and Not building (2026-10-23). Usage prices set per plan are planned for v0.9.0; spend alerts and generated Go and TypeScript SDKs come next, in v0.10.0 (roadmap). (#1363)
  • Twenty claims about Lago, OpenMeter, Flexprice and Kill Bill are corrected against their code (2026-10-23). Each correction names the commit it was checked in, on How Hardcount compares or in the ADR. ADRs keep their text and gain a dated correction. (#1363)
  • Docs no longer overstate Hardcount (2026-10-23). Row-level security covers most API requests, not all (list); for contributors, a new TxBypass use fails a test until it is on that list. Grant entries in the credit ledger are updated in place. There is no volume pricing mode. The docs no longer suggest archiving old usage events, which would break dedup, and Scale says how dedup would survive a split by month. (#1363)
  • Docs: internal records, audits and design notes moved to docs/dev/; overlapping pages merged; two runbook steps corrected.
  • Docs: the self-host, ops, benchmark and runbook pages lead with the short path, with repeats and analogies cut. Claims corrected include the email rate, the shutdown worst case and the parked-invoice gauge.
  • Docs: the front-door pages and guides lead with the short path, with repeats, analogies and padding cut. Claims corrected include tenant isolation, which webhooks are never lost, and what the LiteLLM integration stores.
  • Breaking for API callers: the create routes for subscriptions, invoices and invoice lines, credit grants and adjustments, credit notes, customers, plans, prices and price overrides refuse a field they do not know, and so do invoice refunds, the customer billing profile, and the subscription routes that schedule a cancel, pause collection, set spend thresholds, and add or change an item (2026-10-22, #1327). The answer is a 422 with param set to the field, such as trial_period_days instead of trial_days, and nothing is written. Before, the field was dropped: a trial asked for that way became a subscription charged on day 1, and expiry instead of expires_at made a grant that never expires. A refund with amount instead of amount_cents refunded the whole invoice, flat_amount instead of flat_amount_cents made a free price, and tax_exempt on a billing profile left an exempt customer taxed. A 422 is not kept under the Idempotency-Key, so the corrected request can reuse the key. An item change that sent immediately instead of immediate was a 200 that moved the plan at period end. Also refused with a 422: trial_days below 0 or above 730, an extend-trial past 730 days from the trial’s start, a commit relief with refund_amount_cents, out_of_band_amount_cents or refund_type at the top level instead of inside commit_relief (it was ignored, and the relief refunded the card in full), a plan created with a status other than active, and a plan or customer PATCH with a status the database does not allow (that was a 500). dunning_policy_id is not a customer create field: assign the policy with PATCH /v1/customers/{id}. The dashboard sends only known fields. Other PATCH routes and the dunning policy routes are unchanged (ADR-112).
  • An operator’s Add payment method link now replaces a working default card (2026-10-22, #1343). Before, the card the customer saved through it was added beside the default. A card paid on the hosted page is still added beside a working default (ADR-021).
  • API callers: the finalize audit row of an invoice Hardcount created finalized carries born_finalized: true (2026-10-21, #1319). Renewals, first invoices, plan changes, spend thresholds and final invoices get it. A draft finalized by a person or by a tax retry does not.
  • The invoice’s Activity card now groups rows by day and names the timezone once (2026-10-21). Days and times follow the account’s timezone. Each row shows its time, the amount after the title, who did it, and on a test clock when it was recorded. Invoices, credit notes and subscriptions in a row are links. The dot is grey, green, amber or red. (#1318)
  • A failed-payment notice or payment setup email that Hardcount did not send now says why on the invoice’s Activity card (2026-10-20). It reads “Failed payment notice not sent · The invoice was already paid”, or voided, or marked uncollectible, not “already closed”. Emails skipped before this release keep the old words. (#1310)
  • Subscription activity rows now follow the same wording rules as invoice rows (2026-10-19). Spend thresholds show money, not cents. A failed proration says what was not billed, not a server error. A plan change for renewal reads “Plan change scheduled”. The dot is grey, amber or red, with one meaning each. (#1300)
  • Invoice activity rows now use one wording and name who did it (2026-10-19). Emails read “Invoice email delivered”, a credit note has one title with its total and where it went, a void or write-off shows its amount, and rows name the person or API key. The dot is grey, green, amber or red, with one meaning each. (#1297)
  • New design record for activity lines (2026-10-19). For contributors: ADR-128 sets one wording per event, a named actor on every line and one clock rule for the Activity cards, the Dunning page and Sent emails. The rules are in the writing-style guide. The screens change in later PRs. (#1294)
  • The upgrade guides now say to stop the API while a failed upgrade is fixed, and what going back loses (2026-10-19). With Compose, docker compose stop hardcount-api ends the restarts that retry it. A restore loses everything written after the backup, and Hardcount can charge some invoices again: Going back lists what to check. A stack built from source now upgrades to a release, after a backup. (#1292, fixes #1230)
  • The OpenAPI file now lists every route the server answers (2026-10-18). Thirteen were missing, among them the data behind the hosted invoice and payment-update pages, the Stripe and Postmark webhooks, /health/ready and /metrics. A Go test now fails when the router and api/openapi.yaml disagree (OpenAPI workflow).
  • New section: who can sign in to the dashboard (2026-10-18). It lists what protects sign-in with no setup and says Hardcount has no two-factor sign-in yet. To add one, put sign-in behind your own SSO or VPN; the section names the paths to keep open (Run Hardcount in production on one VM). Two-factor sign-in is built when users or a security review ask for it (ADR-095).
  • The OpenAPI file now describes every success response and the Idempotency-Key header (2026-10-18). For API users and generated clients: 37 routes had a missing or empty response schema, among them the list routes, POST /v1/credits/grant and the test clocks. Usage event quantity is a decimal string, as the server always sent it. Invoices, usage events and credit ledger entries gain fields the server already sent (API endpoints).
  • New page: card payment failure handling (2026-10-18). How Hardcount handles declines, ambiguous charge results, crashes mid-charge, duplicate or late Stripe webhooks and refunds, and the four gaps still open (Card payment failure handling).
  • Browser tests now set up pricing, recipes, price overrides and dunning policies through the dashboard (2026-10-18). Contributors: web-v2/e2e/account-setup.spec.ts has four journeys, each checked against the API, and the smoke test now opens the six public pages signed out, including links with no or a dead code.
  • Browser tests now change subscriptions through the dashboard (2026-10-18). Contributors: web-v2/e2e/subscriptions.spec.ts has four journeys: change plan now (proration invoice) and at renewal, cancel at period end, undo and cancel now, quantity and item changes against the upcoming invoice, and trial and collection controls. Each checks the page against the API.
  • A misspelled key in a bundled recipe now fails CI and stops Hardcount at startup (2026-10-18). For contributors: before, a key such as base_bill_timming was skipped without a word, so the plan quietly billed in arrears. The three bundled recipes load unchanged.
  • Browser tests now issue, settle, correct and close invoices through the dashboard (2026-10-18). Contributors: web-v2/e2e/invoices.spec.ts has four journeys: a one-off invoice from draft to finalized and Save & send, credit notes before and after an offline payment, void and write-off, and the credit notes page. Each checks the screen and the payment page against the API.
  • Migrations run with goose, from one v0.8.1 baseline (2026-10-18). A new database runs one generated file instead of 178. A failed migration is undone, so nothing needs repair: fix the cause and run again. Copies that start together take turns. ADR-125 supersedes ADR-124. (#1198)
  • hardcount migrate status shows the upgrade plan (2026-10-18). It prints the database’s version, the migrations to apply, index builds marked, and whether a migration is running now. It only reads, so run it with the new release before upgrading. (#1198)
  • Breaking for API callers: publishable API keys (2026-10-28, #1415). Hardcount keeps two key types, secret and platform. A request with an hc_pub_ key gets 401, and POST /v1/api-keys with key_type: publishable gets 422. The bootstrap response has no publishable_key_test. Stripe’s publishable key (pk_…) under Settings → Payments is not affected.
  • Breaking for API callers: values and tables kept only for data an earlier version wrote (2026-10-28, #1415). v0.9.0 installs fresh, so nothing holds them:
    • dunning run resolution manually_resolved;
    • invoice line line_type discount and tax;
    • an invoice’s stripe_invoice_id;
    • the coupons, coupon_redemptions and customer_discounts tables.
  • Breaking for API callers: duplicate fields and a second dedup key (2026-10-23, #1374). Old → new:
    • an invoice line’s quantity (the integer, truncated) and quantity_decimal → quantity, the exact decimal string; unit_amount_cents → unit_amount_decimal. The same on the hosted invoice data. Amounts, PDFs and the dashboard do not change. The hosted page now writes a whole quantity of 1,000 or more on a line that is not usage as 1,500, not 1500, as the dashboard does;
    • a usage event’s properties → dimensions; an event whose properties has a key is a 422, and null or {} is ignored;
    • the Idempotency-Key header on the usage routes (/v1/usage-events…, /v1/integrations/litellm/spend) → each event’s idempotency_key, already required. The header is ignored there, so nothing is replayed from it, and a batch over 1 MB is always a 413 batch_too_large.
  • The billing-interval rebuild at start-up (2026-10-23, #1374). It rebuilt intervals for items written before 2026-07-27. A 0.8.x database cannot be upgraded, so no database needs it. Two copies starting together no longer race on it, which deadlocked behind PgBouncer (#1364).
  • The hardcount migrate force stub, the HARDCOUNT_BILLING_INTERVALS_READER start-up warning, and HARDCOUNT_TEST_CLOCK_CATCHUP_DELAY_MS (2026-10-23, #1374). hardcount migrate force is now an unknown subcommand. The setting did nothing but log a warning, and the delay only slowed test-clock catch-up for a retired manual test.
  • Breaking: a recipe no longer creates a dunning policy or a webhook endpoint (2026-10-26, #1389). Installing one leaves your default dunning policy as it was. Set retries under Dunning policies and add an endpoint under Webhooks.
  • Breaking for API callers: rating rules, meter pricing rules and a meter’s default price (2026-10-25, #1386). Use plan prices. The 409 meter_in_use goes with them: a price change never reprices the period already open. A recipe install puts its prices on its new plan and never changes another plan’s prices.
  • Breaking for API callers: a pricing rule’s own aggregation, and last_ever (2026-10-23, #1376). A meter has one aggregation for all its prices (ADR-130), so aggregation_mode and its values last_during_period and last_ever are gone. Use a last meter for a latest-value price; nothing reads a value from before the period any more.
  • Breaking for API callers: 8 operations that another operation already does (2026-10-23, #1368). They now answer 404. Use the replacement; some response shapes differ, so check the API reference. The kept invoice, subscription and usage reads answer 200 with no rows for an unknown customer id, where the removed routes answered 404. Other differences are on each line. The replacements:
    • POST /v1/checkout/setup → POST /v1/customers/{id}/payment-methods/setup-session, which takes the name, email and address from the customer’s record;
    • GET /v1/checkout/status/{customerID} → GET /v1/customers/{id}/payment-methods;
    • GET /v1/webhook-endpoints/events/{id}/deliveries → GET /v1/webhook_events/{id}/deliveries, which answers 404 for an unknown event id, not 200 with no rows, and for a replay’s id lists the deliveries of the original event and of every replay;
    • POST /v1/webhook-endpoints/events/{id}/replay → POST /v1/webhook_events/{id}/replay, whose status is queued or no_matching_endpoints, not replayed;
    • GET /v1/customers/{id}/invoices → GET /v1/invoices?customer_id=;
    • GET /v1/customers/{id}/subscriptions → GET /v1/subscriptions?customer_id=;
    • GET /v1/usage-summary/{customer_id} → GET /v1/usage-events/aggregate?customer_id=, with from and to (there is no default window);
    • GET /v1/billing/preview/{subscription_id} → POST /v1/invoices/create_preview with customer_id and subscription_id.
  • Breaking for API callers: reset_billing_cycle on spend thresholds (2026-10-23, #1371). A spend threshold invoice always keeps the billing period, Stripe’s default; no open-source engine we compared has the option. On an anniversary subscription with an in_advance base, it left part of a period’s base on no invoice after each fire. The dashboard’s “Start a new billing period” box is gone.
  • Breaking for API callers: values no Hardcount code writes any more (2026-10-23, #1365). Code that lists these values can drop them:
    • subscription status archived; GET /v1/subscriptions?status=archived is a 400;
    • plan status draft; a PATCH that sends it is a 422, and a draft plan is now active (it already billed like one);
    • invoice tax_status failed (a failed calculation stays pending);
    • dunning run state paused (the paused flag stays); GET /v1/dunning/runs?state=paused, or any state but active, escalated and resolved, is a 400;
    • dunning event types retry_scheduled, retry_succeeded, retry_failed, paused and resumed.
  • The billed_entries table (2026-10-23, #1365). Nothing ever wrote it.
  • recovery_superseded from an invoice’s recovery_warning.code (2026-10-23). It warned that paying a written-off spend-threshold invoice double-charged; that no longer happens (#1353).
  • The v0.8.x upgrade path (2026-10-23, #1354). Hardcount no longer takes over a v0.8.0, v0.8.1 or v0.8.2 database, and hardcount migrate status no longer says ready to move or refused. The baseline is now ordinary source, and the migrations CI job checks a fresh install under each role setup.
  • The block usage cap: usage_cap_units and overage_action on subscriptions (2026-10-22, #1327; the usage-cap half of #973). With overage_action: block, the cap added up the units of every meter on an invoice and scaled each meter’s billed quantity down to fit; usage was still recorded. No billing engine we compared does this, the threshold invoice ignored the cap, and a capped close after a threshold invoice could bill more than the usage cost. Subscriptions that had a cap now bill all usage. API responses and webhooks no longer carry usage_cap_units or overage_action, and a create that sends either gets a 422. The dashboard’s Usage limits fields are gone. To invoice before a bill grows too large, use a spend threshold.
  • The Stripe Search sweep for parked invoices, and its metric hardcount_parked_search_errors_total (#1307). Parked invoices are checked through Stripe’s List API instead (see Fixed). Remove alerts on the metric.
  • hardcount migrate force and hardcount-migrate-safety (2026-10-18). goose leaves no half-applied state, so there is nothing to force. The safety tool replayed the old migration files, which no longer ship.
  • The Dunning page’s Resolve dialog starts with no choice made (2026-10-28, #1406). It opened with “Payment recovered” chosen, so Resolve with no other click recorded a payment and marked the invoice paid. Resolve now waits for a choice.
  • Finalize on a draft invoice asks first (2026-10-28, #1406). The dialog says the invoice can’t be edited afterwards, that a manual invoice’s tax is worked out then, and whether the customer’s card is charged or they are emailed a link to add one. It promises the email only when the server can send it (SMTP and PAYMENT_UPDATE_URL set). With Stripe not connected in the mode, it says no card can be charged or added online. While the subscription’s collection is paused, it says nothing is charged until collection resumes.
  • After a plan or quantity change, the toast names each part of a credit for unused time (2026-10-28, #1406). A credit that went partly to the balance and partly off an open invoice read as a balance credit for the whole amount. A credit on a draft invoice says it waits for the draft to be finalized.
  • Changing now to a plan with another billing interval says what happens (2026-10-28, #1406). The dialog said the price difference is charged or credited. It now says a new period starts. On a plan billed in advance, the new period is invoiced at once and the toast names that invoice, with its total unless it is a draft. On a plan billed in arrears, the new period is billed at its end.
  • Deleting a dunning policy that has run payment retries answers 409 with the reason, not 500 (2026-10-28, #1406). The runs keep pointing at it, so it stays. The dashboard shows the message.
  • The invoice PDF lists every issued credit note (2026-10-28, #1406). It read the newest 100 notes of any status, so past 100, or with drafts among them, rows were missing and stopped adding up to Amount Due. This covers the downloaded, emailed and hosted-page PDFs.
  • The Advance clock dialog says how to test payment retries that pause or cancel (2026-10-28, #1406). Move to the last retry first, then to the next billing date. One longer move bills a period a real clock would not (#1276).
  • A manual invoice for a subscription’s exact billing period no longer stops that period from being billed (2026-10-28, #1407). API: an invoice created with subscription_id and the next cycle’s period took the cycle invoice’s place, and the billing run skipped that cycle invoice with its base fee and usage. A period skipped this way before the fix is not billed again.
  • An in-arrears base fee with a change in the period bills exactly the period’s days (2026-10-28, #1407). Each segment was rounded to whole days on its own, so the segments could bill one day more or less than the period. This could happen with two mid-day changes, with one change at an exact half day from the period start, and with one change in a period that crosses a daylight-saving change in the tenant’s timezone. Each change time is now rounded to whole days from the period start. This covers the cycle invoice, its preview and an immediate cancel’s final invoice.
  • Voiding an invoice hands back all the customer credit applied to it (2026-10-28, #1407). It counted at most 100 credit applications, so an invoice paid by more returned too little.
  • An invoice with more than 50 credit notes: every note counts toward what is left to credit, refund and reverse (2026-10-27, #1404). Hardcount added up only the newest 50. A refund with no amount could be refused, a cancel or plan change crediting unused time could fail, with Stripe Tax voiding an unpaid invoice could reverse the older notes’ tax again, and a written-off invoice could miss its unapplied-credit warning.
  • A refund with no amount works on a paid invoice that already has a credit note that is not a refund (2026-10-27, #1404). It refunds what was paid by card less the refunds on the invoice’s credit notes that are not voided, up to what is left of the invoice total. It was refused with 422 when those other notes came to more than the part of the invoice not paid by card.
  • A card-setup link asked for with Stripe not connected answers 409 stripe_not_connected for every customer (2026-10-27, #1399). A customer not yet sent to Stripe got a 500. The dashboard shows the message. The customer’s update-payment page answers 409, not 502, and says adding a card online isn’t available, also for a customer Hardcount already sent to Stripe.
  • The first-invoice tutorial is checked in CI: every pull request runs its commands on its own build and compares each output with the page (2026-10-27, #1400). Its wording now matches Hardcount: the meter is named Tokens and counts tokens events, and the invoice’s ⋯ menu is next to Audit log. The quick start runs v0.8.2, so the page links its v0.8.2 version.
  • An installed recipe’s card says its settings were set at install (2026-10-27). It said “you can adjust”, while they are fixed once installed.
  • The Recipes page now says to edit an installed recipe’s plan and prices on the plan’s page (2026-10-27). It sent you to the Pricing page, which no longer lists prices.
  • The customer’s Daily usage chart shows each axis label whole, and large numbers in short form, such as 180K (2026-10-27). Long labels were cut off: 180,000 read “80000” and 0.00008 read “00008”.
  • The Edit plan dialog shows a used plan’s own billing timing, and says before you save that a used plan billed in advance needs a base fee above 0 (2026-10-27). It showed “At end of period” and “0 for no base fee”, and Save failed. The API’s refusal no longer tells you to change the timing, which is fixed.
  • Two installs of the same recipe at the same time: the second now answers 409, not 500 (2026-10-26, #1389). It installs nothing, and a retry returns the first install.
  • When an item leaves a meter on the first day of a period and an item on another plan that prices the meter is added later that day, each event is billed once (2026-10-26, #1388). The added item bills from the period start, and the events before the first item left were billed by both items. This covers the invoices, the previews, customer usage and the live balance.
  • A row that does not say test or live is refused, instead of being stored as live (2026-10-25, #1385). The database stamps each new row’s mode from the transaction’s app.livemode setting, and read an unset setting as live. An insert with it unset now fails with needs app.livemode set to on or off (migration 00213_livemode_trigger_requires_mode.sql). Hardcount sets it on every write. SQL you run by hand must set it first: Manual fixes shows how.
  • Work that does not say test or live mode is refused, instead of being treated as live (2026-10-25, #1385). Hardcount reads the mode on every request and job, and a missing one now fails with livemode was never set on ctx (an API answers 500) rather than using live data or live Stripe keys. In the database, an insert into a table with a livemode column fails without app.livemode set to on or off, and a read shows no rows (migrations 00213_livemode_trigger_requires_mode.sql and 00214_livemode_policies_require_mode.sql). SQL you run by hand must set it first: Manual fixes shows how.
  • Stripe keys that cannot be read are reported as an error, not as “Stripe not connected” (2026-10-25, #1385). This covers refunds, the hosted invoice page, payment-method setup and Stripe Tax. A tax calculation that cannot read the keys waits with the new reason credentials_unreadable on hardcount_tax_outcome_total (Tax calculation).
  • Every collection pause that ends sends one subscription.collection_resumed, with its own trigger (2026-10-25, #1384). A pause can end before Hardcount clears it: its date comes, or its invoice is paid. A new pause or a resume call that reached it first sent no resume, or sent triggered_by: operator. And the scheduled resume could clear a pause dunning set a moment earlier, sending triggered_by: payment for an invoice nobody paid. The scheduled resume’s event and audit row are now saved with the change.
  • On a test clock, a cancel by dunning says canceled_by: dunning in subscription.canceled and in its audit row (2026-10-25, #1384). It said operator, taken from the API key of the clock advance that ran dunning.
  • An event’s after_close flag, its counter and its WARN follow the aggregation the event is counted under, even when the meter’s aggregation changes during the ingest (2026-10-25, #1382). Before, they could follow the old one. Billed amounts were not affected.
  • An amount too large for 64-bit cents refuses the invoice or fails the read, instead of billing or showing a wrapped number (2026-10-25, #1383). This covers base fees, prorations, tax splits, previews, the live balance, threshold checks, credit notes, the credit ledger and dashboards. Amounts that fit (up to about $92 quadrillion) are unchanged, except a rounding that was one cent off when it divided by more than about 4.6 × 10^18 cents. A spend threshold counts usage only, so a base fee this large does not stop its fire.
  • Manual tax is exact on any invoice amount, and an invoice too large for 64-bit cents fails instead of carrying wrong amounts (2026-10-24, #1378). At 20%, a 1e14-cent invoice was taxed 1553255926290 cents; it is now taxed 2e13. An invoice whose sum or total with tax does not fit fails to bill with an overflow error.
  • A usage event whose provider cost is too large to record is stored and billed with no cost (2026-10-24, #1378). Before, the ingest, a LiteLLM call or a held-usage release failed with a database error. The margin report counts the event in unresolved_events.
  • A spend threshold followed by an immediate cancel no longer bills the whole period’s base fee (#1371). A 3000c monthly base canceled after 24 days billed 3000c; it now bills 2400c, as without a threshold.
  • An invoice paid outside Hardcount no longer shows a Stripe payment ID (2026-10-23). Its Payment details showed out_of_band:<time> as the Stripe payment ID. A second payment by card on such an invoice now says the first was recorded as paid outside Hardcount (#1370).
  • A plan change, a new subscription or an activation no longer fails with a deadlock error when a meter’s prices or aggregation are edited at the same time (2026-10-23). When two plans that bill an edited meter were involved, Postgres could stop one of the requests (40P01). Subscription writes now lock plans in the same order as the meter edit (#1367).
  • A webhook endpoint can subscribe to payment.currency_mismatch (2026-10-23). Hardcount sent it, but an endpoint that named it was refused as an unknown event (only * and payment.* got it), and the dashboard’s event picker did not offer it, nor payment.dispute_opened and payment.dispute_closed. A test now fails when an event Hardcount can send is missing from the list endpoints accept, the picker or the webhooks page (#1365).
  • The OpenAPI file lists the values the database accepts (2026-10-23). tax_error_code is an enum and includes provider_not_configured, and the removed values above are gone. A test now fails when a stored enum’s database check and its OpenAPI enum differ (#1365).
  • Editing a price no longer reprices the billing period already open (2026-10-23, #1362; on the plan since 2026-10-25, #1386). A price change on a plan a subscription has used applies from each subscriber’s next billing period, on a test clock too, and a meter’s aggregation is fixed once the meter has a usage event. Before, a price edit, a new price or an aggregation change repriced usage already recorded.
  • A card payment the customer’s bank wants them to confirm (3-D Secure) is no longer shown or emailed as a declined card (2026-10-23). The invoice notice says the customer must confirm and offers Email payment link, not Retry payment. The customer is asked to confirm on the hosted invoice page. Payment retries count but don’t charge that card, as in Stripe Billing (runbook) (#1360).
  • The payment-failed email now gives Stripe’s reason for a card decline, such as “Your card has insufficient funds.” (2026-10-23). It gave no reason: the reason was saved under a field the email sender didn’t read. For a card refused for good it asks for a different card. A failure Stripe gives no card-decline message for still has no reason line (#1360).
  • A written-off invoice that billed a subscription’s current period in advance now counts as unpaid when the subscription changes (2026-10-23). While it has an amount due, a plan change, added item or seat increase that would charge for the rest of the period is refused with unpaid_invoice_blocks_change; it used to charge nothing for the rest of the period (#1359).
  • A cancel, downgrade, removed item or plan swap now credits the unused time on that written-off invoice, as it does on an unpaid one (2026-10-23). It lowers what the invoice asks for, so a customer who pays it later pays only for the time they had; it used to give nothing. A cancel on the period’s first day voids it instead, if nothing is paid on it, no charge on it is being confirmed with Stripe and it bills nothing else (#1359).
  • A written-off invoice still waiting on an unclear charge is checked again once Stripe can be reached (2026-10-23). When Stripe refused the check (not connected, key rejected, customer missing), it used to stay waiting for good (#1359).
  • Writing off an invoice now asks Stripe to close a Checkout page still open for it (2026-10-23). The customer could pay through it for up to an hour after it opened, and still can if Stripe refuses the request (#1359).
  • An invoice below Stripe’s minimum charge, such as $0.30 USD, is no longer treated as a declined card (2026-10-23). It was marked failed, started payment recovery, emailed the customer that it was declined and, when recovery ran out, ran the final action (by default, pausing the subscription). Now, once Stripe refuses a charge or a hosted-page Pay click for it, nothing is marked failed or emailed, any recovery ends, and the invoice stays open with a notice to void it, record a payment or pay it from credit with Collect payment, which also asks Stripe again (runbook) (#1355).
  • Payment retry emails no longer call every failed attempt a decline, or show “payment failed: “ before Stripe’s reason (2026-10-23). A retry with no card on file, or one Stripe refused for another reason, read “was declined” (#1355).
  • A Pay click Stripe refuses on the hosted invoice page no longer holds the invoice’s charges for an hour (2026-10-23). The refused page counted as open for 65 minutes, so no charge or payment retry ran (#1355).
  • The finalize-time collect log no longer says the auto-charge sweep will retry an invoice Stripe refused or answered unclearly (2026-10-23). The sweep charges only queued invoices; the line now says what happens next (#1355).
  • A written-off spend-threshold invoice is no longer billed again at period end (2026-10-23). The period’s invoice billed again what the threshold invoice already billed. Paid late, or partly paid by credit, the customer paid twice. Now only a void made before the period’s invoice is built gives that window back. The spend-threshold scan also stops recomputing tax every tick after a void (#1353).
  • A void of a spend-threshold invoice that finishes before the period’s invoice is saved is now always honored (2026-10-23). A void that finished while the period’s invoice, an immediate cancel’s final invoice or a plan change’s invoice was being saved could leave that invoice billing only what came after the threshold invoice, so the voided charges were billed nowhere. Now that invoice sees the void and bills them, or the void waits and lands after it (#1353).
  • Anthropic cache writes sent by LiteLLM now bill at the cache-write price (2026-10-22). They billed as input, 1.0x instead of 1.25x (5-minute) or 2x (1-hour): a call Anthropic charges $0.52 billed $0.31. A call first saved before this keeps its old billing when LiteLLM resends it or a held row of it is released, so its writes do not bill twice (#1349).
  • Claude on Google Cloud with an @default or @date name bills at its own model’s price, for usage sent after you upgrade (2026-10-22). Opus 4.5 to 4.8 billed at Claude Opus 4 prices, 3 times too much. Usage stored before the upgrade keeps the old price (#1351).
  • LiteLLM’s Bedrock cross-region names, converse/ and invoke/ routes, Foundry azure_ai/ names, and Google Cloud @date names of older Claude models now find their recipe prices, for usage sent after you upgrade (2026-10-22). Before, a name such as us.anthropic.claude-sonnet-4-6 or vertex_ai/claude-haiku-4-5@20251001 kept its raw name, matched no recipe price and was not billed. Now it is billed. OpenAI names on Foundry, such as azure_ai/gpt-4o, find theirs too. Usage stored before the upgrade stays unbilled until you price it (#1351).
  • LiteLLM response-cache hits and failed calls are no longer billed (2026-10-22). A hit billed as a fresh call, with its would-have-been cost as COGS; the provider charged nothing. The response counts hits in cache_hits. Hits saved before this stay billed (#1349).
  • LiteLLM /v1/messages (Claude Code), /v1/responses, Gemini and pass-through usage is now billed (2026-10-22). Hardcount billed only chat, text completion and embedding calls and answered 200 to the rest, so this usage was lost with no log. /v1/messages is affected on Anthropic, Bedrock, Vertex AI, Azure AI and MiniMax models. Usage sent before this release is not recovered (#1352).
  • A LiteLLM call Hardcount does not bill but that carries tokens is now logged (2026-10-22). Image, audio, realtime, batch and A2A calls log a WARN with the call type and count in hardcount_litellm_skipped_with_tokens_total (#1352).
  • Dunning’s final pause or cancel now waits for the invoice grace (2026-10-22). When retries ran out just after a period ended, the pause or cancel built that period’s invoice at once, and usage arriving inside INVOICE_GRACE_PERIOD was billed on no invoice. The dunning run now waits until the grace ends. Your own subscription changes still close the period at once (#1348).
  • A dunning run with every retry used no longer reads as retrying (2026-10-22). The invoice page said “Next retry” and the invoice notices said retries continue while the final pause, cancel or write-off was still to run, so no charge was coming. The page now says “Final step”, the Dunning list says “Final step in …”, and the notices promise no retry. GET /v1/dunning/runs returns the policy’s max_retry_attempts (#1348).
  • A held-usage release inside the invoice grace counts rows stamped after the period end as in_open_period (2026-10-22). The next period bills them; they were counted as outside. A subscription that ends at that period end still counts them as outside (#1350).
  • The margin docs say what margin is (2026-10-22): usage rated in the window, invoiced or not, not “what you billed” (#1350).
  • Issue credit note on the Credit Notes page now opens the invoice page’s dialog (2026-10-22). Its own form offered only the newest 50 open and 50 paid invoices, showed amounts in the default currency, and allowed more than was left to credit. Its refund option always failed. Now you pick the customer, then the invoice. Written-off invoices no longer offer Issue credit note, which the server refused (#1339).
  • A lowercase invoice prefix set through the API no longer blocks saving Settings (2026-10-22). The form refused the value it loaded. Both prefixes are now stored in capitals, as numbers always printed them; PUT /v1/settings answers ACME for acme (#1339).
  • The Credit Notes page’s totals and tab counts now cover every note (2026-10-22). They counted only the 100 notes on screen, and a tab hid the other tabs’ counts. They now come from the server, and amounts in two currencies are shown side by side, not added (#1339).
  • The Subscriptions page’s Paused tab now lists subscriptions whose collection is paused (2026-10-22). It sent a status no subscription has, so it was always empty. The tab is now called Collection paused, and lists show a “Collection paused” chip. A timed pause leaves the tab at its resume date, when collection starts again. API: GET /v1/subscriptions?collection=paused (#1341).
  • API: invoices carry test_clock_id, the test clock their dates follow (2026-10-22). It is the customer’s clock, on the list and get routes. The Invoices page reads “Due in N days” from it and no longer loads subscriptions to work it out (#1341).
  • A voided or written-off invoice no longer reads as owed on the dashboard (2026-10-22). The subscription page, Home, the command palette, the Invoices and Dunning lists and the customer’s “Last invoice” showed “pending” and “$29.00 due” on a void. Home, and the API’s failed_payments, no longer count it as a failed payment. The Invoices and Dunning lists’ “Amount due” column is now “Amount”: a void shows the amount it was voided at, a paid invoice what paid it (it read $0.00), and the Invoices sort orders rows by the figure shown. On a phone, Home’s recent invoices no longer draw the amount over the badges. (#1328)
  • Dunning “Recovered” counts only runs a payment closed, and “At risk” only open invoices (2026-10-22). A void, a write-off, or a credit note or customer credit that left nothing due counted as recovered, and a void’s or write-off’s amount stayed at risk. The “Recovered” tab is now “Resolved”. API: GET /v1/dunning/stats adds recovered_count; dunning_recovery_rate counts payments only. (#1328)
  • The Home revenue chart’s axis reads $1.2K for $1,200, in the workspace currency (2026-10-22). It read “$12.0K” and always printed “$”. (#1328)
  • A subscription whose trial ended no longer shows a red “Expired” badge in the list (2026-10-22). The badge shows only during a trial, as on the subscription page. (#1328)
  • A payment, void or credit made on the dashboard now refreshes the sidebar counts, the Dunning page and “Last invoice” at once (2026-10-22). They kept the old state until the page changed. “Last invoice” skips drafts and shows what was paid, customer credit included. (#1328)
  • A cancel or plan swap that voids an unpaid prebill now closes its dunning run and stops its payment page (2026-10-22). The void reversed the tax and sent invoice.voided, but the run stayed active until its next retry, days later, and a hosted-page Checkout session stayed payable for up to an hour. Every void now does all of it. When dunning’s final cancel voids the run’s own invoice, the run ends resolved, with no escalation email (ADR-118). (#1337)
  • A canceled subscription no longer shows a scheduled cancel or a pending plan change (2026-10-22). A cancel left both on the subscription, so the dashboard offered Undo on it. A collection pause stays after a cancel: it still holds the subscription’s unpaid invoices, its final invoice among them, until you resume collection. (#1337)
  • Credits, credit notes, Home and the audit log show each amount in its own currency (2026-10-22). A EUR customer’s credit read “$20.00”, and a EUR credit note was added into the dollar totals. Credit balances from GET /v1/credits/balances and /balance/{customer_id} now carry the customer’s currency. Home’s MRR and Revenue say “in USD only”, and the chart axis uses the same currency. The credit-note and credit-ledger CSV exports have a Currency column (ADR-100). (#1336)
  • The invoice footer and the support URL now print where Settings says (2026-10-22, #1333). The footer was printed nowhere. It now prints on invoice PDFs and the payment page, and each invoice keeps the footer it was issued with. Invoices issued before this release show no footer. A footer longer than the page breaks onto the next page. The support URL now prints on invoice and credit-note PDFs too.
  • Emails no longer name “Velox Billing” as the seller (2026-10-22, #1333). With no business name set, or when the settings read failed, emails came from “Velox Billing”. They now show no seller name. Set a business name in Settings → General.
  • A credit expiry date picked in the dashboard is valid through the end of that day (2026-10-22). A grant “through Nov 30” was saved as 23:59:59.999, 1 ms before the November invoice is issued, so it paid nothing and the card was charged in full. A picked day is now saved as the next midnight, and reads “Valid through Nov 30”. A grant that ends at any other time, such as an API grant at 17:00, reads “Expires” with its time. Grants created in the dashboard before this release keep the gap and read “Expires Nov 30, 11:59 PM”; grant them again if needed. The commit line in New invoice now accepts today as its last day (ADR-071). (#1332)
  • An operator invoice line is an add-on, base fee or usage line (2026-10-22). A discount or tax line was accepted and added to the total, and a tax line was then taxed again at finalize. Both now answer 422 on create and on add line, and the “Add line item” dialog no longer offers Discount. Both line pickers now name the type “Add-on”. A refused line no longer uses up an invoice number. (#1324)
  • An invoice’s subscription must belong to its customer (2026-10-22). POST /v1/invoices accepted another customer’s subscription_id, so dunning, collection pause and test clocks acted on the wrong customer. It now answers 422 with param: subscription_id. (#1324)
  • The paused-collection invoice notices now say when payment retries continue (2026-10-22). Pausing collection keeps new invoices as drafts, but retries already under way keep charging, as in Stripe. The notices said adding a card would not charge the invoice, and the dunning email, the dunning policy outcome and the write-off dialog said a pause stops all charging. The pause dialog now says retries already running continue. Void the invoice or mark it uncollectible to stop the retries. (#1323)
  • Settings say what they do (2026-10-22). The business email hint no longer claims a Reply-To; the address is printed on invoices and credit notes. In test mode, the General, Invoicing and Tax tabs say that test and live share these settings. (#1323)
  • A card saved through a setup or update link becomes the default, so the next retry charges it (2026-10-22, #1343). After a decline it sat beside the old card, and every dunning retry, auto-charge and Collect payment charged the old card again, though the page said the new one would be used. Stripe’s default now follows Hardcount’s, and the audit log records the default change (ADR-021).
  • A late Stripe webhook no longer brings back a card that was removed (2026-10-22, #1343). A redelivered setup_intent.succeeded re-added a card the operator had removed, or an older copy of a card saved again, and an older copy could take the default back. Stripe cannot charge a removed card, so the card now stays removed.
  • The hosted invoice page charges in the invoice’s currency (2026-10-22). On a Stripe account with Adaptive Pricing on, Checkout could offer the buyer a converted local-currency price with a conversion fee, so a USD 10.29 invoice was charged as INR 1,010.68. Hardcount now turns Adaptive Pricing off on the session (#1344).
  • Credit for unused prepaid time goes to the invoice that charged it, newest first (2026-10-22). A seat decrease, downgrade, removal, cancel or swap split it by subtotal or put it on one invoice, so an unpaid upgrade invoice could stay due while the credit went to the balance. The timeline now shows credit that does not fit, and splits a credit that went partly to an open invoice (#1345).
  • The upcoming-invoice preview of an in-advance subscription with a plan change scheduled for the period end shows the new plan’s price and period (2026-10-22). It showed the old plan, while the invoice that period end issued billed the new one. The preview’s plan_name now lists the old plan too, because the period that is ending still bills on it (#1342).
  • Paying the overdue invoice now resumes collection that dunning paused (2026-10-22). Before, a subscription that dunning paused stayed paused after the customer paid, by card, offline or through a credit note: new invoices stayed drafts and nothing charged them. Now paying the invoice whose dunning paused collection resumes it, even while another invoice is still owed, and the subscription’s activity shows “Collection resumed · The overdue invoice was paid”. Invoices from the paused periods stay drafts. A pause you set stays until you resume it, and dunning no longer replaces your pause or its resume date with its own. (#1340)
  • The API spec lists every request field the server reads (2026-10-22). A generated client could not attach a customer to a test clock (test_clock_id), send a credit note’s refund_type, lines[].invoice_line_item_id or relief invoice_id/reason, or send properties on a batch usage event. A test now fails when a request body’s fields differ from the JSON fields of the Go type its handler decodes, or when one is an array or object and the other is not. balance_after is documented as cents (#1338).
  • A wrong id or date on a read route is now an error, not an empty answer (2026-10-22). A typo’d customer id read as “no credit, no card”, and a plain date such as from=2030-01-01 returned all-time usage totals. Now the id gets 404 and the date filters. A rule with no tiers sends graduated_tiers: [], not null; item metadata is an object, left out when empty, not "e30=". A test clock’s subscription list now carries names, billing time and items (#1335).
  • Attaching a meter to a plan, or any plan edit, no longer clears the plan’s tax code and description (2026-10-22). PATCH /v1/plans/{id} wrote both on every call, so a tax code set through the API was lost. The base fee then fell back to the tenant’s default tax code, or to no code when the tenant has none. A field left out now keeps its value; to clear one, send an empty string (#1334).
  • Usage sent during a free trial is no longer billed, and usage a subscription canceled earlier that day already billed is not billed again (2026-10-22). A period starts at midnight. So the first paid invoice after a mid-day trial end billed that day’s trial usage. A subscription recreated the day its predecessor ended billed that day’s usage a second time. Usage now starts at the trial end, or where the other subscription’s ended. The base fee still counts the whole day (ADR-012). (#1329)
  • An immediate monthly-to-yearly (or yearly-to-monthly) swap on an in_advance plan now bills the usage before the swap (2026-10-22). The swap’s invoice carried only the new period’s base, so the old period’s usage up to the swap was never billed. It now carries that usage, once, from any spend-threshold invoice’s end (ADR-056). (#1326)
  • On a test clock, a trial that runs out is dated at its end, and a dunning run’s final actions at its escalation (2026-10-21, #1319). An advance past either dated “Trial ended”, the first invoice’s issue and due dates, the pause, cancel and write-off at the advance instead, so the first invoice sorted after the renewal. When the advance also crossed a renewal, the pause or cancel is dated at that renewal, so the cancel still voids or credits the renewal’s unused invoice.
  • A payment on the hosted invoice page is dated on the invoice’s test clock (2026-10-21, #1319). It used to carry only its real time, so on a simulated invoice it sorted before the invoice’s own rows. On the real clock nothing changes.
  • The invoice’s Activity card names every source it could not load (2026-10-21). A failed read of payment attempts or disputes printed its code; it now reads “payment attempts” or “disputes”. The never-shown “Payment attempts” count under Payment details is removed. (#1318)
  • A cancel, plan swap or item change on a partly credited invoice now gives back what fits (2026-10-20). An operator credit note, or a partial waive (credits paid part, a note waived the rest), leaves an invoice less room. When the unused part was larger than that room, a cancel or swap gave nothing and an item change answered 500. Now it credits the room left and logs the rest. (#1312)
  • A charge Stripe refuses for the API key no longer counts as a decline (2026-10-20, #1313). After a key is rolled, or a restricted key lacks a permission, charges pause instead: the invoice is not marked failed, no dunning retry is used, and the customer gets no email. Settings → Payments shows the refused key until a charge goes through again or you reconnect. Charges resume once the key works, including a restricted key you fix in Stripe by adding the permission.
  • A restart during a tax retry no longer leaves the invoice an unbilled draft (2026-10-20). With Stripe Tax, a deploy or a lost leader lease between saving the tax and finalizing left a draft that nothing retried, so the period was never billed. The finalize now completes. If the finalize itself fails, Retry tax shows an error, and the invoice stays a draft with its tax saved: finalize it. (#1308)
  • A parked invoice now ends without a person (2026-10-20, #1307). A charge whose unclear answer named no payment, or that outlasted the caller’s time limit, is parked. Hardcount now lists the customer’s payments in Stripe every tick: a payment that appears is recorded, and with none after 24 hours the invoice is charged again as usual. Before, it waited for a Stripe search or a write-off. The notice is now a Warning. hardcount_parked_invoices now counts only parks Hardcount has not ended on schedule (still parked 26 hours after the charge’s hold, or with no hold); alert on disposition="open" above 0.
  • Revenue and the revenue chart count what paid each invoice, not its total (2026-10-20). A credit note on an unpaid invoice lowers what is due but not the total, so the waived part counted as revenue; an invoice waived in full added its whole total. revenue and revenue_cents in the analytics API change the same way. (#1306)
  • A credit note that leaves an unpaid invoice with nothing due now marks it paid (2026-10-20). Before, the invoice stayed open at $0: dunning closed as recovered with no invoice.paid, or its final action canceled the customer. Now invoice.paid is sent with amount_paid_cents 0, dunning closes at once, and the invoice can no longer be voided, as in Stripe. (#1306)
  • The test-clock docs now say how to rehearse a dunning policy that cancels or pauses (2026-10-20). An advance bills every skipped period before it retries payments, so one long move bills a period a real clock would not. Move to the time of the last retry, then to the next billing date: the cancel or pause is dated at the time you move to (first invoice, #1276). The 1-year advance limit no longer claims Stripe parity. (#1311)
  • On a test clock, a credit that was live at a payment retry now pays it (2026-10-20). Test mode only. An advance expired credit before it ran the payment retries it skipped, so a retry dated before the credit’s expiry charged the card in full. Credit now expires after the retries (ADR-029). (#1311)
  • A late period close no longer loses credit that was live when the period ended (2026-10-19). The hourly expiry run retired a grant an hour after expires_at, even while that period’s close was retrying or its invoice waited for Stripe Tax, so the card was charged in full. Now the run waits for them, and a grant live at the period end pays that period’s invoice, at a dunning retry too. An expired grant can stay in the balance longer (ADR-071). (#1305)
  • An SMTP relay that refuses the sender no longer marks customers bounced (2026-10-19). SendGrid’s 550 The from address does not match a verified Sender Identity read as a bounce: the email was given up on at once, later emails to that customer were dropped, and nothing paged. Only a refusal of the recipient address is a bounce now; other refusals are retried (bounces). (#1304)
  • The outbox and Stripe circuit-breaker runbooks now say what the code does (2026-10-19). A failing webhook endpoint has its own page: it never grows the outbox. Both outbox pages give SQL to send failed rows again, guarded to failed rows. The breaker is one per process, not per tenant. Roles that run every few seconds page after 5 minutes. (#1304)
  • A database error during a dunning retry no longer uses up an attempt (2026-10-19). Before, it counted as a declined payment: the customer’s warning email gave the error as the reason, and on the last attempt dunning cancelled, paused or wrote off with no final charge. Now the retry is skipped, a WARN line names the invoice and the error, and the next pass usually retries it within the hour. If the database is still down when Hardcount undoes the attempt, as in a restart or failover, the attempt still counts (Not handled yet). (#1302)
  • A panic on one row no longer stops billing or dunning for every tenant (2026-10-19). The rest of the tick runs, and the panic still pages. A panicking dunning retry no longer spends an attempt. A panicked tick reruns after one interval, not 60 s. A failed payment-method read is now a counted error. (#1303)
  • A replayed webhook now carries the original event’s id and created_at (2026-10-19). It carried a new id, so a receiver that skips ids it has seen, as the webhook guide says to, ran the event again: a replayed invoice.paid could be booked twice. Now a receiver that already processed the event skips the replay. Store an event’s id only once you have handled it (rule 1): an id stored on arrival makes an event that failed skip its retries and replays. The dashboard still lists each replay as its own row. (#1301)
  • A webhook endpoint that answers with a redirect is no longer marked delivered (2026-10-19). Hardcount followed the redirect, and the 200 from the page it led to counted as delivered, although the endpoint never took the event. A 3xx is now a failure and is retried; register the URL that answers directly (Retries). (#1301)
  • A trial that runs out now reads “Trial ended” on the subscription’s Activity card, not “Trial ended early” (2026-10-19). Only an operator’s End trial reads “early”. A trial that ends with a cancellation due reads “Subscription canceled · At trial end”. (#1300, fixes #1278)
  • A charge whose result was lost can no longer take a second payment (2026-10-19). After a crash, a shutdown or a lost answer, nothing charges, voids or records an offline payment on the invoice until Hardcount has asked Stripe, usually within about an hour (a time limit parks the invoice instead, see the parked-invoice entry above). A payment found is recorded; otherwise charging resumes. Write-off stays allowed (ADR-127, runbook). (#1299, fixes #976)
  • A prepaid commit is now taxed once, on its usage (2026-10-19). For accounts with a tax provider on. The commit purchase was taxed, then the usage its credit paid was taxed again. A commit line now carries no tax, with the reason prepaid_credit_purchase. Issued invoices are unchanged; a draft whose tax was worked out before the upgrade is taxed this way when it is finalized. If your country taxes prepayments when bought, read Tax on prepaid credit first. (#1288, fixes #1271)
  • invoice.finalized now carries credits_applied_cents, and the docs say when credit is used (2026-10-19). Hardcount uses a customer’s credit when it collects an invoice, after this event, so the event’s amount_due_cents is the amount before credit. To mirror what is owed, read the invoice (webhooks, ADR-088). (#1295, fixes #1281)
  • The invoice Activity card no longer states false things (2026-10-19). Retries that ran out say what they did, such as “Collection paused”, not “Escalated for manual review”. A declined retry is red and shows the decline. Credit-note and decline reasons are in plain words. (#1297)
  • A mid-period cancel’s credit for unused prepaid time now reduces the final invoice (2026-10-19). For in-advance plans that also bill usage. With a card on file, the card paid the whole final invoice and the credit sat unused on the balance. With no card, the email asking for a card quoted the whole amount. (#1286, fixes #1272)
  • scripts/restore.sh now gives each restored table the access rights it has in the backup (2026-10-19). Restored into a database that gives new tables more, as a new Compose install does, audit_log came back with more access for hardcount_app than the backup gave. psql now runs the restore, so it must be at least as recent as pg_restore. A plain SQL dump restores only into a database without default privileges. (#1287)
  • Upgrade, seat-increase and add-item invoices now send invoice.finalized and get a finalize row in the audit log (2026-10-19). Before, an app that syncs revenue on invoice.finalized never saw them; the first event was invoice.paid. The event has the same fields as a period-end invoice’s. One that starts as a draft sends it when finalized. (#1293)
  • Clicking “Issue credit note” again after an error no longer makes a second note or refund (2026-10-19). If Stripe was slow, the dashboard could show an error although the note was made, and a second click made another. Each open of the dialog now sends one Idempotency-Key, so a retry gets the first note back. (#1290, fixes #1273)
  • A refund with no clear answer from Stripe now shows unknown, not failed (2026-10-18). After a timeout, a Stripe 5xx or a rate limit the customer may already be refunded, so failed invited a second refund by hand in Stripe. A refund Stripe refused stays failed. Retry refund looks in Stripe first, then resends the same request. The dashboard still shows unknown as “refund pending”: retry it through the API for now. (#1267, fixes #965)
  • Two changes to one subscription’s items at the same moment no longer bill twice (2026-10-19). For seat or plan pricing billed in advance: a seat sync, a second operator or a retry overlapping a change was priced from the old state too. Now it is priced from what the first change left, or answered 409 item_changed. Two adds or plan changes onto plans that bill one meter can no longer both go through and bill that meter twice every period, and two removals can no longer leave a subscription with no items. (#1283, fixes #1270)
  • A block usage cap no longer bills above the cap (2026-10-18). For subscriptions with overage_action=block. With three or more meters, or a meter billed for part of the period, a cap could bill every unit, and an item removed mid-period could take the invoice past the cap. All usage on an invoice now scales by one factor and stays within the cap. The cap counts each meter only over the part of the period it is billed for, so usage from before an item was added, or after it was removed, no longer scales the invoice down too far. After a spend-threshold invoice, the next-invoice preview now keeps tiered prices on when a cap is set but not passed, as the invoice does. Issued invoices are unchanged. (#1261, fixes #1192)
  • The period close now bills the usage of an item removed after the period ended (2026-10-18). If the item was removed before a late close ran, some of its meters billed 0 for that period. (#1261)
  • PDFs print long line descriptions whole, and large totals no longer overlap their labels (2026-10-18). On invoices and credit notes, a description over 50 characters was cut to 47 and ...; it now wraps inside its column. Totals now line up with the Amount column, so amounts of $10,000 or more and long tax names fit. (#1263, fixes #1228, #1178)
  • A Stripe 409 no longer counts as a declined charge (2026-10-18). After a dropped connection, Stripe can answer the resent charge with 409 while the first one still runs. Hardcount then started dunning, and its next retry could charge the card a second time. Now the invoice is parked as unknown (runbook, part of #976, #1264).
  • Dunning no longer cancels or pauses a subscription while the invoice’s payment is processing or unknown (2026-10-19). If the payment reaches that state just before dunning’s final action, dunning waits and checks again. A payment that then fails gets the final action. If you write off a parked invoice whose dunning used every retry, apply that action by hand (runbook, part of #976, #1284).
  • Usage below zero after corrections no longer stops the invoice (2026-10-18). For some meters, the period close and an immediate cancel failed and built no invoice, base fee included. Now any meter below zero bills 0, the rest is invoiced, and the subscription timeline names it. A block usage cap totals what the invoice bills, not a meter’s net total. Failed billing recovers on its next retry; a parked subscription needs retry-billing (runbook, #1219).
  • The Compose stack now sets idle_in_transaction_session_timeout = '30s' on hardcount_app (2026-10-18). The Postgres requirements call it required: without it, a Hardcount process frozen mid-transaction keeps a subscription locked until something ends its session. Existing volumes need the step under “Before upgrading”. (#1258, fixes #1229)
  • Dunning no longer charges a card the bank refused for good (2026-10-18). After a lost, stolen or similar decline, retries keep their schedule but charge only once another card is the customer’s default. The customer is asked for a different card, and the invoice says so. A card the customer then saves replaces the refused one as the default, and the next retry charges it. (#1225, fixes #966)
  • The payment-failed email’s plain-text part labels the pay link “Pay invoice” (2026-10-18). It said “Update payment method” for the link to the hosted invoice page, which takes payment. (#1225)
  • Adding an item to a subscription now refuses an archived plan (2026-10-18). Creating a subscription and changing an item’s plan already refused one; POST /v1/subscriptions/{id}/items did not, so new business could land on a retired plan. It now answers 409 invalid_state with the same message as create. Items already on an archived plan keep billing. (#1218, fixes #1194)
  • Writing off an invoice with “Also pause collection” ticked now shows the paused subscription (2026-10-18). The pause happened, but the invoice page still said “The subscription is still billing.” and the subscription page still offered Pause collection, until it was reloaded about 30 seconds later. Both pages now refresh the subscription. (#1208)
  • Setting a password now refuses common leaked passwords (2026-10-18). Bootstrap, password reset and invite accept refuse about 30,000 passwords of 12 or more characters, such as Password1234, in any letter case. Passwords already set still work. (#1222)
  • Go 1.26.10 (2026-10-21). Clears GO-2026-6809 in crypto/x509, which govulncheck reaches from Hardcount code; CI lint failed on every branch until this lands. No application code change.
  • The client address comes from chi’s trusted-proxy middleware (2026-10-20). X-Forwarded-For counts only from a TRUST_PROXY address, every header line is read, and an entry that is not a bare IP stops at the proxy. Rate limits, audit and session rows, logs and traces all use it (ADR-095, #1315).
  • Traces no longer carry a link’s token (2026-10-20). A request span is renamed to {method} {route} once routed, such as GET /v1/public/invoices/{token} (before, :id replaced ID-like parts). Its url.path holds the route or a redacted path, and client.address the trusted client address. Redaction covers hosted invoice, payment-update, cost-dashboard and invite links, in traces and the access log (#1315).
  • CORS_ALLOWED_ORIGINS=* now allows any site without the sign-in cookie (2026-10-18). The API answers * with no credentials, so another site’s page can call it with an API key only. Listed addresses still may send the cookie. CORS now runs on github.com/rs/cors (Self-host Hardcount, #1262).
  • Every email address Hardcount accepts or sends to gets the same check (2026-10-18). Customer emails and CC lists, the email of an invoice or credit-note send (before, only checked for being present), team invites, the company email and the bootstrap owner email must be one bare address. The sender checks To and Cc again before sending (ADR-082, #1259).
  • Sign-in stays rate-limited while Redis is down (2026-10-18). The per-address limit on /v1/auth used to allow every request when a Redis call failed, so an outage made password guessing unlimited. Each copy now counts in memory with the same limit until Redis answers again (ADR-095, #1255).
  • The sign-in cookie is always Secure in staging and production (2026-10-18). It dropped Secure when the browser was on plain http at localhost, for Safari on the quickstart. The quickstart now runs with APP_ENV=local instead. An http:// address setting, such as DASHBOARD_BASE_URL, logs a start-up warning (ADR-095 Phase 0(b), #1226).

Fixes before the first partner installs. Long invoice PDFs go onto more pages, and every email has a Date and a Message-ID. Webhook URLs with a private IPv6 address are refused when saved, and sign-ins record the real client address. No database migrations.

Before upgrading:

  • OTEL_TRACES_SAMPLER now takes effect. If you set it, Velox now keeps only the traces it selects; before, every trace was exported. Unset, nothing changes.
  • A webhook endpoint saved earlier with a private IPv6 URL still fails every delivery. Change its URL to a public address.
  • make test no longer reads .env (2026-10-18). For contributors: only make run, dev, migrate, migrate-status and bootstrap load it, so tests see the same settings as CI. make test-unit runs packages in parallel (about 30 s, was 60 s). The unused test-integration and test-verbose targets are gone. (#1207)
  • ADRs 001–061 all open with a short Summary (2026-10-18). Forty-six records gained one, giving the current rule and status, including what later ADRs changed. The records below each Summary are unchanged. Fourteen status lines now name their dated amendment or partial supersession. See the ADR index.
  • Go tests now walk five untested backend flows over HTTP on a real database (2026-10-18). Contributors: internal/api has new journeys for API keys (create, use, revoke, rotate), offline payments and signed Stripe webhooks, the update-payment link, dunning-policy delete, and whoami, the mode switch and the reset-link check. (#1204)
  • Decision recorded: migrations move to goose in v0.9.0 (2026-10-18). Nothing changes yet. v0.9.0 takes over a v0.8.0, v0.8.1 or v0.8.2 database without changing a table; older ones upgrade to v0.8.2 first. A failed migration leaves nothing to repair, so velox migrate force goes. ADR-125 supersedes ADR-124; ADR-073’s deadlock correction stands.
  • Dashboard lint now fails on unused code (2026-10-18). For contributors: npm run lint in web-v2 runs knip after eslint, so an unused file, export or dependency fails CI. Unused dashboard files, exports and three unused dependencies are deleted. web-v2/knip.jsonc says what is ignored and why. (#1196)
  • The README shows before it tells (2026-10-17). The pictures come first, then a small diagram of how Velox works and one example usage event, then the quick start. The gaps table is shorter, and the volume line now uses the €6.49-a-month server test.
  • OTEL_TRACES_SAMPLER now decides which traces Velox keeps (2026-10-18). Velox set its own sampler, which replaced the variable, so every trace was exported whatever you set. Unset, all traces are still kept. The Compose stack now passes the sampler settings, and its example endpoint uses the OTLP/HTTP port 4318 (self-host settings).
  • A failed release of an Idempotency-Key is now logged and counted (2026-10-18). When a 409, 422 or usage-ingest 5xx could not free its key, nothing was recorded, and retries with that key got 409. It now logs an error and counts in velox_idempotency_cache_errors_total{stage="release"}. The finalize log and metric help no longer claim a retry runs the request again (API errors).
  • Long invoice and credit-note PDFs now go onto more pages (2026-10-17). Before, rows after about the 30th and the totals were drawn below the page edge, out of sight. Later pages repeat the table header and are numbered “Page N of M”, and the totals stay together on the last page. Amounts are unchanged.
  • Thai baht amounts on PDFs show their currency (2026-10-17). The PDF font has no ฿ sign, so THB amounts printed with no currency mark. They now print as THB 1,234.00, like AED and SAR.
  • Every email now has a Date and a Message-ID, and its text is encoded the standard way (2026-10-18). Mail servers and spam filters expect both headers. Accented names and long lines, such as a long note on a setup link, now travel as quoted-printable in short lines. The Message-ID uses your SMTP_FROM domain. The wording is unchanged (#1187).
  • Webhook endpoints with a private IPv6 address are refused when you save them (2026-10-18). Before, a URL such as https://[fd00::1]/ was saved, then every delivery to it failed. Saving now uses the same address check as sending. An endpoint saved earlier with such a URL still fails; change its URL (webhooks, #1199).
  • Dashboard sign-ins record the real client address (2026-10-18). A login or invite accept stored the first X-Forwarded-For entry, which any client can write, in dashboard_sessions.ip. It now stores the address TRUST_PROXY resolves, the same one the audit log and rate limits use. Nothing reads this column yet. (#1203)

Safer upgrades: a repair command for a database upgrade that stopped midway, and a 5-second limit on how long a migration waits for a table lock. Also a faster invoice list, and the Compose stack passes the Postmark webhook login and VELOX_API_BASE_URL. No database migrations.

Before upgrading:

  • A migration that cannot get a table lock within 5 seconds now stops (#987). Velox exits with an error that says how to retry or repair. Set MIGRATION_LOCK_TIMEOUT (for example 30s, or 0 for no limit) to change the wait. See the runbook.
  • A dirty schema_migrations now has a named error and a repair command (#967). Velox still refuses to start, and the error names the migration and the velox migrate force <version> commands to run. See the runbook.
  • The Compose stack now passes POSTMARK_WEBHOOK_USER, POSTMARK_WEBHOOK_PASS, VELOX_API_BASE_URL and MIGRATION_LOCK_TIMEOUT from .env (#1150). If you added them with an override file or by editing docker-compose.yml, move them to .env.
  • velox migrate force <version> repairs an upgrade that stopped midway (2026-10-17, #967). Velox still refuses to start while schema_migrations is dirty, but the error now names the migration and the exact commands. Force refuses while a migration runs. Steps: runbook.
  • Sizing for one small server, measured (2026-10-17). A €6.49/month Hetzner CX23 took about 1 million events a day with 0 errors, and its disk fills in about two months (scale). The one-VM guide gains a Docker install link, a waiting health check, a start-up warnings table and a private backup folder. (#1163)
  • The README’s “What it looks like” is two labelled pictures (2026-10-17). The invoice your customer gets, then your margin per model, each with one line saying what it shows. The text copy of the same numbers is gone.
  • Database upgrades no longer wait on a table lock without limit (2026-10-17, #987). A migration that cannot get its lock within 5 seconds stops, leaves nothing behind and says how to retry. MIGRATION_LOCK_TIMEOUT changes the limit. CI fails new migrations that build an index or constraint the write-blocking way (runbook). (#1167)
  • The invoice list costs the same few database reads whatever the page size (2026-10-17). Each row read its own banner facts, so 25 rows took about 110 ms. A page now reads each fact (card on file, Stripe connection, dunning, pause, payment page) once. Banners are unchanged (#1142). (#1166)
  • The Compose stack passes the Postmark webhook login and VELOX_API_BASE_URL to Velox (2026-10-17). Set POSTMARK_WEBHOOK_USER, POSTMARK_WEBHOOK_PASS and VELOX_API_BASE_URL in .env; editing docker-compose.yml or adding an override file is no longer needed (#1150).

LiteLLM usage for an unknown customer is held instead of lost, new accounts retry failed payments, Settings → Email with a test send, and dates that follow the billing timezone. Three database migrations (0178, 0179, 0180).

Before upgrading:

  • New accounts start with a “Default retries” dunning policy; existing accounts do not get one (#1152). An account with no default policy still retries nothing after the upgrade. Create a policy if you want retries.
  • LiteLLM usage for a customer or meter that does not exist yet is held, not dropped (#1044). The callback still answers 200, with a new held count. Create the customer, then release the held usage from the Usage page.
  • Email actions answer 409 email_not_configured when SMTP_HOST is not set. This covers sending an invoice or credit note, sending a payment-method setup link and inviting a teammate.
  • A charge Stripe never answered returns payment_outcome_unknown, not provider_error. Add the new code if you branch on it.
  • Many messages were reworded; codes are unchanged. Match on reason and code, not on the text.
  • Set your company name if you have not (Settings → Business details). With none set, PDFs no longer print “Velox” as the seller, and that includes already-issued invoices, because PDFs are rendered when opened (#1093). Totals and lines do not change.
  • LiteLLM usage for an unknown customer is held, not lost (2026-10-17, #1044). Usage for an unknown user or meter was answered 200 and dropped. Now it counts in held and waits on the Usage page. After you create the customer, Release records each row once. Discarded rows stay unbilled. See LiteLLM. (#1151)
  • Settings → Email shows whether this server can send email (2026-10-17). It lists the SMTP settings and email-link variables, says what fails while one is missing, and sends you a test email with the mail server’s real answer. API: GET /v1/settings/email, POST /v1/settings/email/test (dashboard sign-in only); whoami gains smtp_configured. (#1121)
  • Browser tests for the dashboard (2026-10-16). Contributors: npm run test:e2e in web-v2 runs Playwright against a real API. It opens every page at desktop and phone width, fails on console errors, failed API calls or sideways scroll, and runs five key journeys. CI job: e2e. See Browser tests (Playwright). (#1101)
  • The demo bills two models, so the margin report shows margin per model (2026-10-17). scripts/demo.sh sends Claude Sonnet 4.5 and Claude Haiku 4.5 usage: five invoice lines ($5.73), margins 67.1% and 20.0%, and 51.9% overall. The Margin card and the margin API (by_model[].margin_bps) show each model’s margin. See Quick start. (#1159)
  • New accounts retry failed payments from day one (2026-10-17, #1152). Each new account gets a “Default retries” policy in both modes: retries on days 3, 7 and 14, customer emails if set up, then no action on the subscription or invoice. Your first enabled policy replaces it. Existing accounts are unchanged. ADR-123 (#1155)
  • The README is shorter and says each thing once (2026-10-17). It runs: what Velox is, why (the gaps table), what it looks like, quick start, whether it fits you, and what’s inside. Quick-start detail moved to Quick start: run Hardcount and its demo. About 1,950 words became about 1,200, and no checked fact was dropped. (#1157)
  • Contributor pages start with a working setup (2026-10-17). Contributors: CONTRIBUTING goes from clone to a billed test customer in five steps, and splits CI into seven required checks and three optional ones. Architecture adds the life of a usage event, the background jobs and every invoice state. (#1146)
  • Features and “Why Velox” are tables (2026-10-17). What’s in the box lists each feature, what it does for you and where to read more. Why Velox exists opens with who it is for and a four-row table of needs. (#1147)
  • The operations runbook is one page per problem (2026-10-17). Operators: docs/ops/runbook/ opens with a “what do you see?” table, and each page runs Symptom, Why, Check, Fix, Verify. The leader-lease runbook is now A background job is stuck. Alerts, health checks and log searches are on one page. (#1148)
  • “How much traffic Velox handles” opens with the short answer (2026-10-17). How much traffic Hardcount handles starts with the figures to plan on (1,000 events/s on db.m7g.2xlarge, 12,000 on db.m7g.4xlarge), what runs out first and what to do next. Self-host’s load numbers moved there. Both benchmark records carry a dated status banner. (#1144)
  • Design docs and dated records move out of the reader’s path (2026-10-17). The nine docs/design-*.md files are now in docs/design/, each naming its current ADR. Audits, reviews and other dated records move from docs/dev/ to docs/records/. The docs index opens with “New here?” and five reader paths. (#1141)
  • API keys, API and webhooks pages start with a working call (2026-10-17). Integrators: API keys covers getting, sending, rotating and revoking a key. The Velox API opens with one curl and links the served /openapi.yaml; there is no SDK yet. Webhooks starts with five steps to a first verified event. (#1143)
  • A new account’s Home says what Velox does and what to do first (2026-10-17). With no subscriptions, Home shows a welcome card and opens the “Bill your first customer” checklist once. The checklist shows the mode and adds “Send a usage event”. Pricing, Dunning, Test clocks and MRR terms are explained. (#1138)
  • Each dashboard page says what it is for, in plain words (2026-10-17). List pages state their purpose in one line; credit notes, credits and reverse charge are explained in place. Titles, dialogs and buttons use sentence case, matching the menu. The publishable key reads “safe to use in a browser”. (#1145)
  • Operator pages are task-first (2026-10-17). Operators: email setup, backups (backup.sh, restore.sh, restore-drill.sh), Postgres requirements and tax calculation are now steps. Tax internals moved to architecture. Both .env.example files are grouped Required → Required in production → Email → URLs → Optional. (#1140)
  • A “Production on one VM” guide (2026-10-17). Operators: Run Hardcount in production on one VM installs a pinned release, then adds HTTPS with Caddy, web addresses, email, Stripe and its webhook, nightly backups with a restore test, a go-live checklist and upgrade steps. The Stripe end-to-end test and release checklist moved to docs/dev/. (#1136)
  • A step-by-step page from your own API calls to a first invoice (2026-10-17). Send usage and get your first invoice creates a test-clock customer, sends token usage (curl or LiteLLM-style), advances a month and opens the invoice, showing real output. ai-pricing.md is now a reference page, and litellm.md says to create the customer first. (#1137)
  • The roadmap lists the API reference as shipped (2026-10-17). It shipped in v0.7.0, but the roadmap still listed it under Next.
  • Test clocks say they hold customers, and the customer pages name your company (2026-10-17). A customer goes on a clock when created; the pages said you pin subscriptions. The update-payment and hosted-invoice pages say “Contact <your company>”, linked to your support URL or email. API: the payment-update link gains branding.company_email. (#1135)
  • A “How Velox works” page, and a glossary in plain words (2026-10-17). How Hardcount works follows a usage event to a paid invoice in one picture and six steps, each naming what the dashboard shows. Each glossary entry starts with a plain sentence; code and database words moved to “For contributors”. (#1134)
  • A quick-start page walks through each demo step (2026-10-17). Quick start: run Hardcount and its demo covers each of the nine steps: what it does and prints, where to click, and why Home reads $0. The demo ends with links to open. If Velox is down, it names the quick-start command, not only make dev. (#1125)
  • deploy/README.md is a three-row “which one do I want” table (2026-10-17). Operators: load balancing, health checks, shutdown and rolling upgrades for two or more copies moved to High availability, and upgrade steps to Self-host. Contributors: the from-source and Docker-image recipes moved to CONTRIBUTING. (#1132)
  • The README says why Velox exists and who it fits (2026-10-17). “Why it exists” is a sourced table of the gaps small AI teams hit, detailed in Why Velox exists. “Who it fits” gives the measured volume of one Postgres. comparison.md: Kill Bill’s Aviate adds raw-event metering, not metering as a whole. (#1133)
  • Docs have a first-time reader bar (2026-10-17). Writers: Writing style for Hardcount docs adds five rules for reader-facing pages, “You see:” after each task step, a list of words to keep out of UI text, and a before/after example.
  • The README says who Velox is for and what each quick-start step shows (2026-10-17). A “Who it is for” line and a short “Why it exists” list come first. Each quick-start step ends with “You see:” and real output. It covers WSL on Windows and why Home reads $0. (#1124)
  • Operator docs match the code (2026-10-17). One VM suits a small team’s production; two or more copies is the high-availability setup. Docs cover the Postmark bounce webhook, PAYMENT_UPDATE_URL (only the “add a payment method” email), the replay route POST /v1/webhook_events/{id}/replay, and Postgres 16 tested, 14 oldest. The unread STRIPE_WEBHOOK_SECRET is gone. (#1122)
  • The README explains Velox and the quick start in plain words (2026-10-17). A short “In plain words” summary sits at the top. The quick start is now three steps, and it says what each command does and what you see after it, including each step of the demo.
  • Activity and Margin open on the billing period (2026-10-16). On the customer page, Activity opens on Cycle and Margin on “Current period”, the window the invoice uses; Margin adds “Last invoiced period”. A rolling 30 days never matched a period. An empty window names the latest usage date (ADR-121). (#1089)
  • Dashboard unit tests run on Vitest (2026-10-16). Contributors: cd web-v2 && npm test now runs vitest run, and tsc -b (part of npm run build) type-checks web-v2/tests/, so a type error in a test fails CI. The custom node --test alias loader is gone.
  • Velox code rules run in golangci-lint and go test (2026-10-16). Contributors: make lint-clock, lint-funding-set and lint-tz are gone. Clock and funding-set rules are forbidigo rules in .golangci.yml; waive with //nolint:forbidigo // wall-clock: <reason> or //nolint:forbidigo // single-invoice-ok: <reason>. A bare //nolint or a reasonless waiver fails. The timezone rule is an internal/arch test. (#1099)
  • One page on how we test; MANUAL_TEST.md is retired (2026-10-16). Contributors: write a test at the layer How we test names for your change. Dashboard changes get a Playwright spec. Stripe, email and PDF are checked by hand before each release with the release checklist.
  • The sidebar is grouped by task and Setup moved into it (2026-10-16). Groups: Billing, Revenue, Catalog, Developers. Dashboard is now Home; Settings is last. Setup progress is a sidebar row, not a floating pill over buttons. Ctrl+K lists every sidebar page (it missed four) with invoice states and amounts due. (#1091)
  • Dialogs keep their buttons in view, and sign-in follows dark mode (2026-10-16). A tall dialog scrolls inside with Save and Cancel pinned; New invoice, recipe and Change Plan cut them off before. Sign-in and reset use the saved dark theme. Invoice states read Open, Past due, Paid or Void everywhere. (#1090)
  • Invoice notices are one short sentence (2026-10-15). API: every attention.message was reworded to at most three newline-separated lines: the cause, then the setup-link email and the prepaid credit at risk when they apply. The explanation and payment ids moved to attention.detail. Match on reason and code, not the text. (#1064)
  • Notices say what will really happen (2026-10-15). The no-payment-method and recovery-ended notices say when collection is paused, so a new card charges nothing yet. They also say when no setup link can be emailed (no customer email, or no SMTP_HOST or PAYMENT_UPDATE_URL) and when a test clock applies. GET /v1/whoami gains can_email_setup_links. (#1064)
  • One notice above the invoice (2026-10-15). The invoice page shows only the most urgent notice. “Diagnostic detail” is now a collapsed “Payment details” section below the invoice. The test-clock banner is one line: “Simulated time: … Dates on this page follow the test clock.” (ADR-099 amended).
  • Subscription and customer pages show one notice box (2026-10-15). The most urgent notice (billing failure, then no payment method, then scheduled cancel, then paused collection) is a box; the rest are one-line rows. “No payment method” reads “Renewals can’t be charged until the customer adds a card.” (#1065)
  • Subscription errors read plainly (2026-10-15). API: refusals for activate, item changes, trial end and extend, scheduled cancel, pause, spend thresholds and over-balance credit deductions no longer name fields. “Billing made no progress” now reads “Billing didn’t finish…”. Codes are unchanged; match on code, not the text. (#1065)
  • Usage with no price shows a plain notice (2026-10-15). The customer Activity card showed the server’s raw sentence (“meter … has usage matching no pricing rule”). It now reads “Some usage has no price”, lists the unbilled quantity per meter and links to the meter.
  • The plan page shows when its price is locked (2026-10-15). With live subscriptions on a plan, the base price, billing timing and meter controls are off, with “N subscriptions use this plan…”. API: GET /v1/plans/{id} gains live_subscription_count.
  • “Price” everywhere a price was called a rule (2026-10-15). The Pricing tab “Rules” is now “Prices”; the meter page says “Default price” and “Prices for specific values”. Its trash icon asks “Remove … from this meter?”: it unlinks the price, which stays on the Prices tab. (#1066)
  • Pricing, recipe and archive refusals read plainly (2026-10-15). API: the plan-locked, meter-has-no-price, last-price, recipe-conflict and customer-archive messages were reworded; they name subscriptions and meters, not API paths, fields or ids. Error codes are unchanged; match on code, not the text.
  • Dashboard and Usage notes match the data (2026-10-15). In test mode with test clocks, the Dashboard says its totals leave them out. Usage shows “Set up a meter” only when no meter exists, and quantities read “12,500 tokens”. The account menu can reopen a hidden setup checklist. The unused /onboarding page is gone. (#1066)
  • Stripe key errors say what happened (2026-10-15). The Payments card says “Stripe didn’t accept these keys” or “Couldn’t check keys with Stripe” (amber: saved but not tested), not Stripe’s raw JSON. API: last_verified_error holds Stripe’s sentence; new last_verified_error_kind (rejected, unavailable). Migration 0178. (#1067)
  • Settings and sign-in messages match what Velox does (2026-10-15). The webhook signing secret shows “Saved”, not “Verified”. The rotate, replace-secret and disconnect dialogs no longer describe changes at Stripe that Velox doesn’t make. Sign-in pages say “Couldn’t reach the server…”, and Forgot password states the 3-per-hour limit.
  • Team invites say when they can’t be sent (2026-10-15). With email or DASHBOARD_BASE_URL unset, the Team tab shows “Invites are off” and disables the form. API: GET /v1/whoami gains can_send_invites; the 409 message no longer names the setting.
  • Plainer refusal messages (2026-10-15). API: reworded without jargon: webhook URL checks (“must start with https://”), passwords (“is too long. Use at most 72 characters…”), and tax rate (“must be from 0 to 100…”). Codes and fields are unchanged. A paused webhook endpoint gets a Resume button.
  • A parked invoice offers Mark uncollectible, and a payment anomaly offers Open in Stripe (2026-10-15). API: new action code mark_uncollectible; reconcile_payment is now offered on payment_anomaly.
  • Subscription dates agree, and the upcoming invoice says what it is (2026-10-17). A canceled subscription reads “Ended <date>”, not “Renews”, on its page and in “Next bill”. Every page says “access through Nov 15 · doesn’t renew Nov 16”. The Upcoming invoice card shows its period and “estimate as of”. (#1119)
  • Usage Export CSV holds every filtered row (2026-10-17). The export stopped at 50,000 rows. It now streams from the server with the page’s filters, event ids and no row limit, up to 365 days. A longer window exports the last 365 and says so. API: GET /v1/exports/usage-events.csv takes customer_id, meter_id and dimensions. (#1123)
  • Usage totals follow each meter’s aggregation (2026-10-16). Usage summed every reading: a Maximum meter at 140, 210 and 120 GB showed 470 GB. Each meter now shows its own value, unit and daily line; “Total Units” is gone. API: GET /v1/usage-events/aggregate rows gain value, aggregation (or mixed), events, customers, peak and daily; it takes tz. (#1097)
  • The customer payment page no longer offers a Pay button that can’t work (2026-10-17). Without Stripe keys it showed Pay, then “failed to create checkout session”. It now says payment isn’t set up and whom to contact. Errors and bad links use customer wording. API: the public invoice adds pay_unavailable_reason. (#1118)
  • Customer and plan pages show their own subscriptions (2026-10-16). Both filtered the tenant’s first 50 subscriptions, so with more than 50 a customer could show “Subscriptions (0)”. They now query by customer or plan, show the full count and list the first 100, saying when there are more. (#1101)
  • Pages fit a phone screen (2026-10-16). On a 390px-wide phone, Customers, Subscriptions, Invoices, Settings and customer credits were wider than the screen, so buttons such as Add Subscription sat off to the side. Headers and filters now wrap, and Settings tabs drop their icons on a phone. (#1101)
  • A date-only to filter now includes the last second of that day (2026-10-16). to=2026-10-31 ended at 23:59:59.000 UTC, so CSV exports, the invoice list and the audit log left out rows stamped 23:59:59.000001–.999999. It now ends at 23:59:59.999999 UTC.
  • Customer usage accepts a full leap year (2026-10-16). GET /v1/customers/{id}/usage?from=2028-01-01&to=2028-12-31 answered 422 “window must be ≤ 1 year”, because the cap was 365 days. It is now one calendar year, so a 366-day yearly cycle also works; /v1/customers/{id}/margin gets the same cap.
  • A margin request without dates follows the customer’s test clock (2026-10-16). GET /v1/customers/{id}/margin with no from/to covered the wall-clock last 30 days, so a customer on an advanced test clock read revenue and cost 0. It now covers the 30 days ending at the clock’s time. Others are unchanged. (#1082)
  • A new trial no longer hides the running cycle’s usage (2026-10-16). Adding a trial beside an active subscription moved the default GET /v1/customers/{id}/usage period to the trial’s first paid period. Usage and the public cost dashboard read $0 until the trial ended. The default now stays on the current cycle. (#1073)
  • Dashboard charts bucket by UTC day on any Postgres (2026-10-16). Revenue, usage and MRR movement charts grouped rows by the database server’s time zone, so on a server not set to UTC a 02:00 UTC payment could show on the previous day. Buckets are now always UTC, as documented. (#1070)
  • “Net N” due dates no longer print a day early across a clock change (2026-10-16). Due dates added N×24 hours, so a Net 30 invoice issued Nov 1 00:00 in New York printed “Due November 30”. Due dates now count calendar days in the billing timezone: due December 1. (#1077)
  • Trials end on the right day across a clock change (2026-10-16). trial_days added N×24 hours, so a 14-day trial started Mar 1 23:30 in New York ended Mar 16 00:30, not Mar 15 23:30. Trials now count calendar days in the billing timezone. (#1087)
  • Manual invoice PDFs show dates in the tenant timezone (2026-10-16). One-off invoices had no billing timezone, so the PDF and hosted page printed dates in UTC: a 6 PM Los Angeles invoice read “Issued October 17”. New manual invoices store the tenant zone; older ones are unchanged. (#1081)
  • The duplicate-period refusal names the period as invoices show it (2026-10-16). API: invoice_billing_period_taken printed the stored end, “2026-11-01..2026-12-01”, for a period shown elsewhere as “Nov 1, 2026 – Nov 30, 2026”. It now uses that inclusive form in the invoice’s billing timezone. The code is unchanged. (#1078)
  • Activity’s Cycle dates end on the period’s last day (2026-10-16). Cycle read “Oct 1, 2026 → Nov 1, 2026” while Upcoming read “Oct 1, 2026 – Oct 31, 2026” for the same period. Both now use the second form. The 7d, 30d and 90d dates are unchanged. (#1069)
  • Chart labels no longer show the previous day west of UTC (2026-10-16). The revenue and “Daily usage” charts shifted each day into the tenant timezone: in New York, the Oct 15 bar read “Oct 14” and October read “Sep 30”. Each bar now shows its own day; months read “Oct 2026”. (#1071)
  • A prepaid-credit expiry of tomorrow is accepted in the evening (2026-10-16). The new-invoice dialog took “today” as the UTC date, so west of UTC in the evening, picking tomorrow read “Credit expiry must be a future date”. Today is now the date in the workspace timezone. (#1072)
  • Usage and invoice date filters use the tenant timezone on first load (2026-10-16). In a new tab, Usage events and dated Invoices links could keep UTC day bounds, drawn before settings loaded. A New York tenant’s “last 30 days” started four hours late. The window now recomputes when the timezone loads. (#1074)
  • The invoice due badge agrees with the due date (2026-10-16). It counted 24-hour steps from the issue time, so on the morning of “Due Oct 16” it read “Due in 1d”, and “Due today” lasted into Oct 17. It now counts calendar days in the workspace timezone. (#1076)
  • The invoice page shows the same Issued, Due, Paid and Voided dates as the PDF (2026-10-16). It used the current org timezone, not the invoice’s own, so after a timezone change a date could be a day off. Invoices without a stored zone still use the org timezone. (#1079)
  • A cancel invoice’s “view usage” link includes the cancel day (2026-10-16). The link ended a day before the period end, but a cancel invoice ends at the cancel time, so that day’s usage was missing and a same-day cancel showed “No events match”. Cycle invoices are unchanged. (#1080)
  • Pause collection no longer offers today as the auto-resume date (2026-10-16). Collection resumes at the start of the picked day, so the server refused today with “must be in the future”. The earliest date is now tomorrow in the workspace timezone, or by the test clock for a clock-pinned subscription. (#1083)
  • The Dashboard revenue chart shows days without revenue (2026-10-16). It plotted only days with paid invoices, so payments on Sep 20 and Oct 15 were joined as if revenue continued between them. Every day (or month, for 12 months) now has a point, at zero when nothing was paid. (#1084)
  • The test clock “+1mo” preset lands on the right day (2026-10-16). It used the browser’s timezone: for a UTC tenant viewed from Los Angeles, Jan 31 became Mar 1 and Oct 1 became Oct 31, missing the Nov 1 cycle close. It now uses the tenant timezone. (#1085)
  • The demo can be rerun on any day (2026-10-16). scripts/demo.sh (and ./quickstart.sh demo) named its customer by time of day only, so a rerun at the same time on a later day stopped with “HTTP 409” on POST /v1/customers. Ids now include the date and a random suffix.
  • Docs: the runbook’s late-usage check queries the invoice’s period (2026-10-16). Runbook §15 and §16 called GET /v1/customers/{id}/usage without from and to, which returns the current cycle. They now pass the invoice’s period, or use /v1/usage-events for a canceled subscription. The compose demo command prints dashboard links on port 80. (#1068)
  • Draft invoices no longer look issued, and no PDF invents a seller (2026-10-16). A draft PDF carries a DRAFT mark and says it is not issued, instead of an issue date. Without a company name, PDFs print no seller (they printed “Velox”); the invoice page links to Settings. The page now shows the memo. (#1093)
  • Home trends compare like with like (2026-10-16). Failed payments compared against 0 and Active customers against a negative baseline. “Customers” now means customers with an active or trialing subscription; the MRR sparkline is gone. API: overview adds live_customers, live_customers_prev, failed_payments_prev; trialing_subscriptions now counts (it was always 0). (#1094)
  • The next-invoice preview prorates like the invoice (2026-10-16). A calendar first period, a mid-period item or a quantity change shows prorated base lines; usage counts only while its plan was on. A trial previews its first paid invoice. This covers the subscription page and create_preview without period. An empty preview says why. (#1096)
  • Used-up credit stays visible on the customer page (2026-10-16). A fully applied grant left $0.00 and no row. Credits now show granted, used, expired, canceled and deducted amounts, and every grant. API: GET /v1/credits/grants/{customer_id} rows gain expired_cents, retired_cents and deducted_cents; GET /v1/credits/balance/{customer_id} and GET /v1/credits/balances gain total_retired and total_deducted. (#1098)
  • Margin no longer reads 100% when no provider cost is known (2026-10-16). The Margin card shows a dash, says whether rates are missing or unmatched, and links to Provider costs. Partly costed usage shows no margin either. API: margin_bps is omitted in both cases; new costed_events and cost_rates_configured. (#1095)
  • A customer’s figures use the customer’s currency (2026-10-16). Credit balance, usage total, margin and New invoice used the workspace currency, so a EUR customer read $0.00. They now use the next invoice’s currency (billing profile, live plan, then latest invoice). Until it is known, they show a dash and New invoice waits. (#1092)
  • Usage event log values have thousands separators (2026-10-15). “+35000” now reads “+35,000”, matching the totals above it; the value is formatted as text, so no digit is lost.
  • Setting a plan’s base price to 0 no longer reads “Plan updated” when nothing changed (2026-10-15). The server keeps the old price when sent 0, so Edit plan now says so and disables Save. The meter page lists each plan’s base price in its own currency. (#1066)
  • The customer Activity card’s total no longer adds up different currencies or calls quantities “events” (2026-10-15). It summed every currency into the first one and counted billed quantity in mixed units. It now shows billed usage per currency, and each meter row gives its own quantity and unit. (#1065)
  • A meter’s row in Activity no longer reads “0 tokens · 0 rules” when its usage has no price (2026-10-15). It now says both numbers, as in “0 tokens billed · 12,400 tokens not billed · no price matched”, matching the notice above it.
  • Activity’s 7d, 30d and 90d windows include the latest usage (2026-10-15). The window ended exactly at “now” and left out events stamped at that instant: every event sent without a timestamp to a test-clock customer. 30d showed $0.00 while Cycle showed usage. The Margin card had the same gap. (#1065)
  • Long emails and IDs no longer run over their labels (2026-10-15). On a phone, the customer Details card and the subscription Properties card put each value under its label and wrap long values.
  • The customer page’s credit balance updates after Save & send (2026-10-15). Finalizing a new invoice spends credit, but the balance kept its old value until a reload.
  • Customer and subscription stats fit a phone (2026-10-15). At 390px wide, Billing cycle, Trial period and Outstanding were clipped; the cells now stack.
  • Cancel immediately says it bills a final invoice (2026-10-15). It said “Stops billing right now”. The archived-customer notice drops “All data is read-only”; unpaid invoices are still collected. Pause collection says drafts stay drafts after resuming. Plan, quantity and item changes say “charged or credited now” only on advance-billed plans. (#1065)
  • An invoice link that matches nothing says “Invoice not found” (2026-10-15). It showed the server’s raw text with a Retry button that could never work.
  • Charge now on an invoice paid entirely from credit now ends its dunning run (2026-10-15). The invoice was marked paid, but its run stayed active at $0.00 until the next retry date and counted on the Dashboard and sidebar badge. It now closes as payment recovered. (#1061)
  • After logging in, the dashboard opens the page you were trying to reach (2026-10-14). Opening a link to an invoice or customer while logged out went to login, then to the home page. It now returns to the linked page. Only paths inside the dashboard are followed.
  • The “No payment method” notice no longer promises an email that cannot be sent (2026-10-14). It said Velox emails the customer a setup link, but without email set up (as in the quickstart) nothing is sent. It now says so and offers the customer page instead of Resend. (#1059)
  • Amounts show in the invoice’s own currency (2026-10-14). Record offline payment, the invoice page’s credit-note and line-item dialogs, and dunning runs showed a non-default-currency invoice with the workspace currency symbol. The credit grant confirm no longer hard-codes “$”.
  • The Dashboard’s outstanding total names every currency (2026-10-14). It summed only the default currency next to a count of all open invoices. It now lists each currency, for example “$200.00 + €300.00 outstanding”. The API adds outstanding_by_currency to the analytics overview.
  • Charge now says what happened (2026-10-14). “Payment initiated” showed even when credit paid the invoice with no charge. The message now reads paid, payment started, or failed, from the returned invoice.
  • A write-off whose pause fails no longer says collection was paused (2026-10-14). One warning, “Collection not paused”, now says the invoice was written off and the subscription is still billing.
  • Save & send on a new invoice shows one message (2026-10-14). When the email could not be sent, it showed a warning and then “Draft saved” for an invoice that was finalized.
  • Email actions no longer say “sent” without email set up (2026-10-15). With no SMTP_HOST, the dashboard said “Invoice sent” though the email failed. Sending an invoice, credit note or setup link, and inviting a teammate, now answer 409 email_not_configured and create nothing. Save & send still finalizes the invoice; password reset is unchanged. (#1062)
  • The Dunning “At risk” card shows every currency (2026-10-15). It summed only the default currency, so after the default changed to CAD it read “CA$0.00” while USD runs were active. It now reads, for example, “$739.00 + €5.00”. The API adds at_risk_by_currency to dunning stats.
  • The customer and subscription pages show amounts in their own currency (2026-10-15). After the default currency changed, USD invoices read “CA$”. Outstanding now lists each currency; invoices, plan prices and proration messages use their own. Credits use the customer’s billing currency. API: the customer overview adds outstanding_balance.by_currency. (#1062)
  • The payment-method link error names the action (2026-10-15). A failed “Email a payment-method link” was titled “Failed to resend setup link”; it now reads “Couldn’t email the payment-method link”.
  • The Margin card no longer spins forever (2026-10-14). A failed load shows a retry, and a custom range asks for both dates.
  • A session that ends while you work sends you to sign in (2026-10-14). Pages showed “missing credentials … Authorization: Bearer …”. The dashboard now says “Your session ended. Sign in to continue.” and returns you to the page after sign-in.
  • Error messages name what failed (2026-10-14). An error toast keeps its title (“Failed to attach meter”) with one sentence below. Copy ID shows only for server errors.
  • Charge now no longer says a payment failed when it may have gone through (2026-10-14). A refusal reads “Couldn’t charge”. With no answer from the server or Stripe, toasts read “Result unknown” or “Payment result unknown” and say to check before retrying. API: an unanswered charge returns payment_outcome_unknown, not provider_error (the decline code). (#1063)
  • A failed load no longer looks like an empty page (2026-10-14). Recipes, Provider costs and meter prices said “none yet” on a failed request. Every list and detail page now shows “Couldn’t load …” with Retry, not raw server text. A rate limit reads “Too many requests. Wait a moment, then try again.” (#1063)
  • Usage events say when filters hide events (2026-10-14). An empty 30-day window with older events read “No usage events yet”; it now reads “No events match these filters” with Clear filters. An export over 50,000 events says the file holds the newest 50,000. (#1063)
  • Settings Save explains an invalid field on another tab (2026-10-14). It did nothing. It now opens that tab and says what to fix.
  • Unknown addresses show “Page not found” (2026-10-14). The screen was blank. The crash screen keeps the raw error inside Details.
  • No raw codes on screen (2026-10-14). The invoice notice’s support code moved into Details. “payment_anomaly”, the invoice list hover, the “payment: unknown” filter and Dunning reasons now read as words, for example “Why recovery started: No payment method on file”.
  • Dashboard layout fixes (2026-10-15). Toasts follow dark mode and sit above the Settings save bar, which the setup pill no longer covers on a phone. “Result unknown” is an amber warning, not a red error. Detail-page header buttons wrap on a phone without cutting a test clock’s name short. (#1063)
  • Amber is kept for problems (2026-10-14). The test-mode strip, the Dashboard open-invoices row, an unlinked price, the tax example and a finished clock advance are no longer amber. The sidebar Dunning count is amber, as on the Dashboard. Notices share one shape, with dark-mode colours.

Earlier releases: the full changelog on GitHub.