Skip to content

Reports

The ledger, how refunds net out, the export, and what is not in it.

Every collected payment leaves one row, every refund leaves a negative one, and the report is that ledger summed over a period. Nothing is recomputed at read time, so a report of last quarter shows the rates that applied then, not the rates that apply now.

What goes into the ledger

A row is written when the payment is credited, not when it is created and not when it is attempted. Four things stop it.

ConditionResult
Tax is off on your accountNo row, and no catching up later if you switch it on
The payment is a sandbox oneNo row, ever
The payment is a funding top-up of your own balanceNo row
The computed tax is zeroNo row, so a zero-rated country leaves nothing behind

The row is written once per payment. A second attempt on the same payment, or a replay of the event, never duplicates it.

A refund writes a negative row

Refund a payment and the ledger reverses the tax in the same proportion. Refund a third of the amount and a third of the tax comes back, at the original rate, against the original country, with both amounts negative.

That proportion is capped at the whole: refunding more than the recorded base still reverses at most one hundred percent of the tax. And when the original payment left no row, the refund leaves none either: no tax was collected on it, so none is given back. That covers a payment taken while Tax was off, an exempt customer, a reverse-charge sale and a zero-rated product.

The payment's row alone decides, not your settings on the day of the refund. Tax recorded on a payment was collected, so its refund reverses its share even after you switch Tax off or your plan stops covering it. Switching Tax on later never gives a payment that has no row a reversal either.

The consequence is the one you want at filing time: the report nets collections against reversals on its own, and you never subtract anything by hand.

The report

GEThttps://api.wajub.com/tax/reports

Five query parameters, all optional, and the two filters narrow the whole payload rather than adding a section to it.

periodenumoptionaldefault : 30d
One of 7d, 30d, 90d, month, year or custom. month is the current calendar month, year the current calendar year.
start_datedateoptional
Only read when period is custom. Missing, it falls back to thirty days ago.
end_datedateoptional
The end of the window, on any period. Defaults to the end of today.
countrystringoptional
Two letters. Restricts the whole report to one country.
currencystringoptional
Three letters. Restricts the whole report to one currency.

The response nests everything under report, and gives you the same figures three times: once as a total, once per country, once per currency.

Response · 200 OK
{
"code": 200,
"status": "OK",
"report": {
"period": "30d",
"start_date": "2026-08-13",
"end_date": "2026-09-12",
"summary": {
"total_tax_collected": 808500,
"total_taxable_amount": 4200000,
"total_reversals": -12300,
"net_tax": 796200,
"currency": "XAF"
},
"by_country": [
{
"country_code": "CM",
"tax_collected": 808500,
"tax_reversed": -12300,
"taxable_amount": 4200000,
"transactions": 312
}
],
"by_currency": [
{
"currency": "XAF",
"tax_collected": 808500,
"tax_reversed": -12300,
"taxable_amount": 4200000,
"transactions": 312
}
]
}
}

Three details matter when you reconcile against your own books. tax_collected counts only positive rows and tax_reversed only negative ones, so net_tax is their sum and is the figure you file. transactions counts every row in the group, reversals included, so a country with three hundred payments and twelve refunds reports three hundred and twelve. And summary.currency is whatever you filtered on, or the first currency found: on a multi-currency account the summary adds amounts across currencies, which is only meaningful once you have filtered to one.

File country by country, one currency at a time

You owe tax to a jurisdiction in its own currency, so the report you act on is the one with both country and currency set. by_country on an unfiltered report is useful for spotting where you have exposure, not for filling in a return.

The Dashboard report is not the same object

Tax in the Dashboard answers the same question with a different payload, so do not expect the two to line up field for field.

APIDashboard
Periodstart_date and end_dateA period object with start and end
Totalsummary.total_tax_collectedtotal_tax
Per rowtax_collected and transactionstax_amount and count
Collections and reversalsSplit into two fieldsSummed into one
Across currenciesNot convertedConverted into your base currency as global_total
RatesAbsentThe rate table is included alongside

The Dashboard one nets collections and reversals into a single figure per row, which is why a month with refunds reads lower there than tax_collected does on the API. Both are right, they answer slightly different questions.

The CSV export

Tax, then export, downloads the current view as a semicolon-separated CSV with a UTF-8 marker, so Excel opens it correctly in French and Portuguese locales without an import step. It carries the period, the totals, the base currency conversion when you trade in several, then one block per country and one per currency.

Exports count against your plan's monthly quota: ten a month on Pay as you go, unlimited from Growth up. There is no export on the API.

Registration thresholds

Many jurisdictions only require you to register once your local revenue crosses a line. GET /tax/thresholds lists the thresholds Wajub tracks, with the amount, the currency and the period each is measured over.

GET /tax/thresholds/alerts compares them against your own recorded revenue over each threshold's own period (the calendar year, a rolling twelve months or the quarter) and returns only what needs attention: approaching from 80 % of the line, and exceeded at 100 %. An empty array means nothing is close, and it is also what you get before any tax has been recorded.

Your revenue is converted into the threshold's currency at the platform's rates before it is compared: sales in XAF count towards a threshold in GBP at their value in pounds. The EU threshold counts your sales in every member state together. A threshold your revenue cannot be converted for, a currency without a rate, is not measured and raises no alert.

A threshold set for a state rather than a whole country (one with a state_code, such as a US state's economic nexus) is listed by GET /tax/thresholds but never comes back as an alert: a tax row records the country, not the state, so there is nothing to measure it against.

What the ledger does not hold

Worth knowing so you do not look for exports that are not there.

No sub-national rate. A row is written at the country's standard rate, the one POST /tax/calculate gives: a payment carries no state or city to apply a sub-national rate to.

No mobile money levy and no digital services tax. Mobile money levies and digital services taxes are not recorded, shown on a payment, or added to anything. Where a country levies one, it is not in your Wajub figures.

One row per payment, nothing more. A live payment that produced a record carries it as a tax object on GET /payments/{id}: amount, taxable_amount, rate, name, country and inclusive. A refund carries its reversal the same way. Totals and periods are read through the report endpoint and the Dashboard, nowhere else.

No webhook. No event fires when a row is written or reversed.

What did you think of this content?