Skip to main content

Troubleshooting

Start here​

cd /opt/vodia-billing
curl https://your-domain/api/health
docker compose ps
docker compose logs api --tail 100

The health endpoint reports database state, mode, version and the ingest counters, which between them explain most problems.

Calls not appearing​

Check the ingest counters in Spend, or ingest in the health response.

Nothing at all is arriving. The PBX is not sending here. Verify the CDR URL on the PBX is webcdrs://your-domain/cdr and that it is appended space-delimited rather than replacing an existing receiver. Check docker compose logs caddy --tail 50 for requests reaching the box at all.

unknown_system climbing. CDRs are arriving but the system ID is not configured. Read the hash from the counters and paste it into the system record under Systems.

unknown_domain climbing. A PBX domain that is not a configured tenant. Run Discover, then configure the tenant.

unknown_trunk climbing. A trunk not in the registry. Run Discover, then classify it under Trunks.

not_billable climbing and everything else quiet. The calls are arriving and being recognised, and the trunk is classified non-billable. That is either correct or the classification is wrong.

Calls arriving but not charged​

  • The trunk is classified transit or non-billable. Check Trunks.
  • The tenant has no sell plan. Check Tenants.
  • The call is inbound. Only outbound calls are charged.
  • The call was not answered. A billable call needs a non-empty connect and end after connect.
  • No rate row matched. The call is reported as unrated, and unrated calls block finalization rather than being priced at zero. Use Rate Test on the number to see what the plan does with it.

Wrong amounts​

Check in this order:

  1. Billing increments. increment_initial and increment_step on the matched rate row. This is the most common cause when reconciling against another system.
  2. Connect fee. Charged once per answered call, and easy to miss.
  3. Which row matched. Longest-prefix matching may be picking a more specific row than you expected. Rate Test shows which one.
  4. Minimum duration.
  5. Timezone. A tenant in the wrong timezone shifts calls across period boundaries.
  6. Inclusion allowance. Overflow uses the group's overflow rate only, with no connect fee.

Invoice will not finalize​

The draft reports its blockers. Each one has a fix:

BlockerFix
Unreviewed trunksClassify them under Trunks
Unrated legsAdd the missing destination to the sell plan
Unapproved one-time chargesApprove them on the tenant
Unpriced recurring subtypesAdd a recurring rate for the subtype. Remember ext:* does not match hunt groups or auto attendants
No recipientsAdd billing recipients under Tenants
Duplicate periodAn invoice already exists for this tenant and period. Void it or bill a different period
Sibling already pushedAnother invoice for this period has been pushed to accounting

No email being sent​

  • SMTP is not configured. Check Settings, and use the test email button.
  • The tenant has no billing recipients. Reported as no_recipients.
  • The tenant is set to be emailed by the accounting package, so Vodia Billing deliberately does not send. Check the tenant's delivery setting.
  • The invoice is not finalized. Drafts are not sent.

If SMTP is unconfigured, bill runs still prepare and finalize, and the interface says email is disabled.

Accounting push failing​

  • Token expired. Alerts accounting_disconnected or accounting_expiring. Re-authorise under Accounting → Connect; mappings survive.
  • No customer mapping. Map the tenant under Accounting → Mapping.
  • No due date. Set payment terms under Terms & Reminders.
  • Already pushed. A second push is refused locally. There is no re-push; void and reissue if it genuinely needs redoing.
  • Daily ceiling reached. Xero allows 1,000 calls per day per organisation, about 300 pushed tenants. The run reports what remains; resume the next day.

Xero shows 0.00 tax​

The tax type on the lines is unusable for a manual tax document. TaxType: "AVALARA" and GUID-shaped Avalara-managed values both return success with zero tax. Use the tax rates that Discover pulled from your own Xero organisation.

QuickBooks total differs from Vodia Billing​

Expected. QuickBooks computes its own tax and owns the total. See QuickBooks Online.

Reminders not sending​

  • Payment sync marked the invoice paid. Check the invoice.
  • The tenant is set for the accounting package to chase.
  • No recipients on the customer. Billing email addresses belong to the party that is invoiced, not to a site.
  • The tick has not reached the configured hour yet. The hour is a floor from container boot, not an exact time.

A reminder pass that correctly sends nothing writes no log line, so silence is ambiguous. Look for scheduler activity generally in the API log.

Card payment problems​

The customer has no Pay button​

Eligibility is evaluated on every page load, and each refusal names itself. Check the Online payment column on the Accounting screen for that customer:

  • Stripe is not set up. Settings → Stripe, then Test connection. Saving a key is not enough.
  • The customer is bound to an accounting company. This is the rule, not a fault: a pushed invoice is paid in the package it was pushed to.
  • Online payment is off, or inherited off. Change the row.
  • The invoice is not final, or nothing is outstanding.
  • The tax was computed by QuickBooks. What is owed is their figure; it is collected there.

The customer paid and the invoice is still unpaid​

Almost always the webhook. Open Settings → Stripe: if it says no event has ever reached this installation, the endpoint is missing or the signing secret is wrong, and no payment will ever be recorded.

  • Endpoint URL is https://your-domain/api/stripe/webhook
  • The signing secret pasted here is the one for that endpoint
  • Alerts will carry stripe_webhook_rejected with a rising count

If the event arrived but nothing was written, look for stripe_payment_unattributed, stripe_payment_unknown_invoice or stripe_mode_mismatch in Alerts. The last one means the key was swapped between test and live without the endpoint following.

A bank debit is not a payment on the day it is started. ACH, SEPA, BACS and BECS sit in processing for days; the invoice is correctly unpaid until the debit clears.

Stripe refused the payment page​

The refusal is shown to the customer in Stripe's own words, because Stripe knows things this system cannot. The common ones:

  • Below the minimum, applied to your account's settlement currency after conversion — so a USD invoice on an AU account can be refused even though it clears the USD minimum.
  • No payment method enabled for that currency in your Stripe dashboard.
  • The restricted key is missing a permission. Press Test connection; it reports Stripe's sentence about which one.

A refund is not showing, or the invoice went back to unpaid​

A refund is a reversing entry: the money went back, so the invoice is owed again. That is correct. If the refund was goodwill, raise a credit note as well — the stripe_refund_recorded alert says so with the new figure.

A refund against a payment this system never recorded raises stripe_refund_unmatched and writes nothing, because there is no row to reverse.

Stripe rows cannot be unapplied by hand. Refund in the Stripe dashboard.

HTTPS certificate problems​

  • The domain's A record must point at this server. Check with dig +short A your-domain and compare to curl -s https://api.ipify.org.
  • Ports 80 and 443 must be open to all sources. Let's Encrypt uses multi-perspective validation.
  • docker compose logs caddy --tail 50 shows the ACME exchange.
  • A rate limit from a previous failed attempt can delay issuance. Wait rather than retrying in a loop.

Configuration seems to be ignored​

Check for exported shell variables:

env | grep -E '^(DOMAIN|MONGO_URI|DB_NAME|JWT_SECRET|ENCRYPTION_KEY|PORT)='

Docker Compose reads exported shell variables ahead of .env. A stray DOMAIN= exported by an unrelated project on the same server silently overrides this configuration, and the symptom looks like a TLS or routing problem rather than a configuration one.

Locked out of an account​

No email reset exists by design. From the server:

cd /opt/vodia-billing
docker compose exec mongo mongosh vodia_billing --eval \
'db.users.updateOne({email:"you@example.com"},{$set:{totp_enabled:false},$unset:{totp_secret:"",totp_pending_secret:""}})'

The next login re-enters forced 2FA setup.

Passkeys stopped working​

The hostname changed. Passkeys are bound to RP_ID, which is set from DOMAIN. Each user must re-register. Log in with TOTP, which is unaffected.

Container problems​

docker compose ps                      # all should be Up
docker compose logs api --tail 200 -f
docker compose logs mongo --tail 50
docker compose up -d --build # apply changed source

The API not starting immediately after an upgrade is usually index building. Watch the log and wait rather than restarting.

Quick reference: empty or wrong invoice​

SymptomAlmost always
No usage charges at allTrunks still non-billable
Recurring charges but no usageSame thing
Nothing anywhere, unknown_system climbingSystem ID not set
Nothing, unknown_domain climbingTenant not configured
Nothing, unknown_trunk climbingDiscover not re-run after a trunk was added
not_billable equals acceptedCarrier trunk classified non-billable
Usage present but no taxTax profile built but never applied to the tenant
Extensions missing from recurringext:* used, and the missing ones are hunt groups or attendants
Will not finalize, "unpriced subtype"A recurring rate is missing for that entity type
Will not finalize, "unrated legs"A destination is missing from the sell plan
Amounts slightly off vs old systemBilling increments, then connect fees, in that order
Xero shows 0.00 taxUnusable TaxType — see the Xero page
QuickBooks total differsExpected; QuickBooks owns the tax
Seat count lower than expectedHighest-count rule is correct; check the snapshot history

→ Quick Start → Worked Example