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 https://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
connectandendafterconnect. - 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:
- Billing increments.
increment_initialandincrement_stepon the matched rate row. This is the most common cause when reconciling against another system. - Connect fee. Charged once per answered call, and easy to miss.
- Which row matched. Longest-prefix matching may be picking a more specific row than you expected. Rate Test shows which one.
- Minimum duration.
- Timezone. A tenant in the wrong timezone shifts calls across period boundaries.
- 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:
| Blocker | Fix |
|---|---|
| Unreviewed trunks | Classify them under Trunks |
| Unrated legs | Add the missing destination to the sell plan |
| Unapproved one-time charges | Approve them on the tenant |
| Unpriced recurring subtypes | Add a recurring rate for the subtype. Remember ext:* does not match hunt groups or auto attendants |
| No recipients | Add billing recipients under Tenants |
| Duplicate period | An invoice already exists for this tenant and period. Void it or bill a different period |
| Sibling already pushed | Another 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_disconnectedoraccounting_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 tenant.
- 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.
HTTPS certificate problems
- The domain's A record must point at this server. Check with
dig +short A your-domainand compare tocurl -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 50shows 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.
Getting help
support@vodia.com, with:
curl https://your-domain/api/healthdocker compose ps- The relevant section of
docker compose logs api - The invoice number or tenant domain concerned
Quick reference: empty or wrong invoice
| Symptom | Almost always |
|---|---|
| No usage charges at all | Trunks still non-billable |
| Recurring charges but no usage | Same thing |
Nothing anywhere, unknown_system climbing | System ID not set |
Nothing, unknown_domain climbing | Tenant not configured |
Nothing, unknown_trunk climbing | Discover not re-run after a trunk was added |
not_billable equals accepted | Carrier trunk classified non-billable |
| Usage present but no tax | Tax profile built but never applied to the tenant |
| Extensions missing from recurring | ext:* 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 system | Billing increments, then connect fees, in that order |
| Xero shows 0.00 tax | Unusable TaxType — see the Xero page |
| QuickBooks total differs | Expected; QuickBooks owns the tax |
| Seat count lower than expected | Highest-count rule is correct; check the snapshot history |