Troubleshooting

Symptom first, for the things Pro adds. Anything about counters, quotas, plans or the credit ledger itself is in the free core’s troubleshooting page.

If an operation’s outcome is genuinely unknown rather than wrong, go straight to Recovery. This page is for the cases where something is visibly not happening.

No webhook is arriving at all

In order:

  1. The endpoint is mounted after Plug.Parsers, or with a router forward. Signature verification needs the raw body, and a parser that has consumed it makes verification impossible: every delivery answers 400. Mount plug AuroraMeter.Pro.Webhook, path: "/webhooks/stripe" in the endpoint, above Plug.Parsers. Getting started shows the placement.
  2. Every page of the app answers 400 “invalid signature”. The plug is in the endpoint without path:, so it treats every request as a Stripe event. Add the path.
  3. Deliveries answer 200 and nothing is applied. The endpoint was created with the Thin payload style, whose events carry no object. Recreate it with the Snapshot style (a classic webhook endpoint always is).
  4. The signing secret is the wrong one. Stripe issues a different secret per endpoint, and the CLI’s stripe listen issues another. A mismatch answers 400 and logs it.
  5. Stripe is retrying and you are answering non-2xx. Check the endpoint’s delivery attempts in the Stripe dashboard. A redelivery is not an edge case: Stripe retries any non-2xx for days, which is why every handler is idempotent.

A webhook arrives and nothing happens

Some events are received and deliberately ignored, which is not a failure. The required table in Getting started lists exactly the events that do something; anything else falls through to :ignore on purpose.

Period rollover in particular is not driven by an invoice event. It comes from the configured period source, so a missing invoice.paid is not why a period did not turn over.

Usage is not reaching Stripe

Ask the outbox, not the reporter. Every intent is a row with a state.

AuroraMeter.Pro.Recovery.list_uncertain(limit: 20)
select state, count(*) from aurora_meter_outbox_items group by state;

What each state means for this symptom:

  • pending with a next_attempt_at in the future: backoff after a retry. Normal. The delay is computed, never taken from a provider header.
  • pending and nothing is moving: no deliverer is running. The Oban cron entry is the host’s to schedule. Operations has the schedule.
  • quarantined: enqueue or a lease found a mapping, a mode or a period problem, and the software refused to guess. The row’s quarantine_reason says which. Fix the cause, then Pro.Recovery.requeue_item/2. It is never released automatically.
  • uncertain: the answer never arrived, or the attempt cap or the uncertainty horizon was reached. Recovery.
  • rejected: terminal and visible. Stripe refused it for a reason recorded in last_error.

A feature’s usage stopped at a particular date

That is what a source cutover looks like from the outside. A feature has one reporting source, and the changeover has a watermark: periods before it report from the old source and periods after it from the new one. If the new source has nothing enqueued, usage appears to stop at the watermark.

select feature, effective_period_start, state from aurora_meter_source_cutovers;

Cutover has the procedure and how to move a watermark that was set wrong (Recovery‘s reschedule_cutover/2).

A payment is in an unknown state

Do not retry the charge. The pending payment machine persists the parameters and the idempotency key before the request, so the right move is to resume rather than to re-POST.

An empty search result in the Stripe dashboard is not proof that no charge occurred. Recovery says what evidence counts and which guarded operation applies.

Auto top-up stopped for a tenant

Three causes, and they are distinguishable:

  1. A dispute. Any dispute, including an inquiry, drops the saved card and stops recharge. That is deliberate product behaviour and needs re_enable_auto_top_up/1 once resolved. Top-ups.
  2. Repeated declines. The failure count reached its limit and auto top-up disabled itself with a reason, which is kept on the account row.
  3. No saved payment method. Enabling needs one, and the call answers {:error, :no_payment_method} when there is not.
select tenant_key, auto_top_up_enabled, disabled_reason, failure_count
  from aurora_meter_credit_accounts where tenant_key = $1;

The reconciliation list keeps growing

That is the system working as designed, and the list is meant to be read. Reconciliation reports differences; it does not repair them. An item is a question for a person, and closing it records who closed it and why.

AuroraMeter.Pro.Reconcile.list_items(state: "open", limit: 50)

Reconciliation has the kinds and what each one means.

The Pro dashboard page refuses to mount

By design. It shows outbox state, payments and tenant-level figures, so it needs an explicit authorisation check and, unlike the core page, it does not accept :host_route. Operations has the forms.

A webhook event’s API version does not match the pinned one

Recorded, carried in telemetry, logged once per distinct version, and never refused: the event is a real fact about money and dropping it would be worse than processing it. Stripe serialises an event at the endpoint’s version, or the account default when the endpoint names none, and that is not the version Pro sends on its own requests. Set webhook_api_version to :any to switch the observation off, or align the endpoint in the Stripe dashboard.

The audit log is empty

The plug’s :tenant resolver returned nil, which is how unauthenticated traffic stays out of the log, or the plug is not in the pipeline you think it is. A write that fails is logged at warning and swallowed, because the audit log never breaks the request it is auditing, so check your logs for that warning before assuming nothing was attempted.

Still stuck

Email hello@aurorameter.com with the Pro and core versions, the Elixir and Erlang/OTP versions, the outbox or account row in question with identifiers redacted, and what you expected instead. A suspected vulnerability goes to SECURITY.md‘s private route, never to a public channel.