Skip to content
Loading keys…

Tax

Enable tax collection, calculate it, and manage your registrations.

Tax turns a rate table and a set of registrations into an amount on every payment. Once you switch it on, each transaction is taxed and recorded automatically, and a tax object appears on payment and refund responses. Until then, these endpoints let you look up rates and try calculations without committing to anything.

Settings

GEThttps://api.wajub.com/tax/settings
curl https://api.wajub.com/tax/settings \
-H "Authorization: $WAJUB_API_KEY"

The configuration comes back under tax:

Response · 200 OK
{
"code": 200,
"status": "OK",
"tax": {
"enabled": false,
"inclusive": false,
"default_country": "CM",
"tax_id_type": null,
"tax_registration_number": null
}
}
enabledbooleanoptional
Whether tax is calculated and recorded on every transaction.
inclusivebooleanoptional
true means the amount you send already contains the tax; false means tax is added on top.
default_countrystringoptional
The country whose rate applies when a transaction carries none.
tax_id_typestringoptional
The kind of registration number you hold, such as a VAT number.
tax_registration_numberstringoptional
Your own registration number. Wajub prints it nowhere: not on your receipts, not on your invoices.

Turning tax on is a PUT on the same path. enabled: true makes tax_id_type and registration_number mandatory in the same request, and the number is checked against the format expected for its type.

curl -X PUT https://api.wajub.com/tax/settings \
-H "Authorization: $WAJUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "enabled": true,
  "inclusive": true,
  "default_country": "CM",
  "tax_id_type": "vat",
  "registration_number": "M071812345678A"
}'

Calculating before charging

POST /tax/calculate answers the question "what will this cost with tax" without creating anything. Use it to show a total at checkout.

POSThttps://api.wajub.com/tax/calculate
amountnumberrequired
Amount in the major unit, at least 0.01.
countrystringrequired
ISO 3166-1 alpha-2 country whose rate applies. A code that names no country answers 422, so a typo is never taxed at 0 %.
currencystringoptionaldefault : XAF
ISO 4217 code.
tax_inclusivebooleanoptionaldefault : false
Whether amount already contains the tax.
customer_idstringoptional
A customer uid. Their exemption and reverse-charge status are then applied. A customer that is not one of yours answers 404 Customer not found.
tax_codestringoptional
A product tax code, for goods taxed differently from the standard rate. Ignored when the customer is exempt or under reverse charge.
curl https://api.wajub.com/tax/calculate \
-H "Authorization: $WAJUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "amount": 25000,
  "currency": "XAF",
  "country": "CM",
  "customer_id": "cus_Qe7Tb2Wn5Lr9Xc4Mv8Hk1Zs3"
}'

total is the figure to show the customer, and the figure to pass as amount when you create the payment:

Response · 200 OK
{
"code": 200,
"status": "OK",
"calculation": {
"amount": 25000,
"tax_amount": 4813,
"taxable_amount": 25000,
"total": 29813,
"rate": 19.25,
"tax_name": "TVA",
"country_code": "CM",
"currency": "XAF",
"tax_inclusive": false,
"tax_code": null,
"customer_exempt": false,
"reverse_charge": false
}
}

XAF has no decimals, so 19.25 % of 25 000 is rounded to 4 813. Tax is rounded to the currency's own precision: whole units for XAF, XOF and the other zero-decimal currencies, three decimals for KWD, BHD, OMR, JOD and TND, two for every other currency.

Exempt and reverse charge are answers, not errors

When customer_exempt or reverse_charge is true, tax_amount is zero and total equals amount. That is a correct calculation for a business customer with a verified tax ID, paying from another country than your default_country, not a failure to calculate.

Reference data

Read-only lists, identical for every merchant. None of them accepts a cursor; the two that paginate take page, from 1, and per_page, default 50 and maximum 100, and add a meta object.

EndpointReturns underFiltersPaginated
GET /tax/ratesratescountryNo
GET /tax/codestax_codescategoryYes
GET /tax/codes/{code}tax_coden/a
GET /tax/jurisdictionsjurisdictionscountry, state, typeYes
GET /tax/thresholdsthresholdscountryNo

GET /tax/rates is the shortest of them, one standard rate per country:

Response · 200 OK
{
"code": 200,
"status": "OK",
"rates": [
{
"country_code": "CM",
"name": "TVA",
"rate": 19.25,
"type": "standard"
},
{
"country_code": "CI",
"name": "TVA",
"rate": 18,
"type": "standard"
}
],
"total": 2
}

The two paginated lists carry meta next to their array:

GET /tax/codes?page=2&per_page=50
{
"code": 200,
"status": "OK",
"tax_codes": [ … ],
"meta": { "total": 124, "per_page": 50, "current_page": 2, "last_page": 3 }
}

jurisdiction_type on a jurisdiction is one of country, state, county, city or district, and is_compound says whether its rate stacks on top of a parent's rather than replacing it.

GET /tax/codes/{code} returns one code plus every country rate attached to it, under rates, so a single call tells you how a product category is taxed everywhere you sell.

Registrations

Where you are registered to collect tax. Five endpoints, the usual shape, all scoped to your account.

MethodEndpointReturns
GET/tax/registrationsregistrations
POST/tax/registrationsregistration, 201
GET/tax/registrations/{id}registration
PUT/tax/registrations/{id}registration
DELETE/tax/registrations/{id}confirmation
country_codestringrequired
ISO 3166-1 alpha-2.
state_codestringoptional
For countries that tax below national level.
typestringoptional
One of standard, simplified, ioss, oss.
registration_numberstringoptional
Your number in that jurisdiction.
registered_atdateoptional
When the registration took effect.
expires_atdateoptional
Must be after registered_at.

A new registration is created with status: "active". There is one per country and state: a second POST for the same country_code and state_code, a revoked one included, answers 422 on country_code. Change the existing one with PUT instead.

PUT accepts the fields above except the country and state, plus status, one of active, expired, pending or revoked. Setting active on a revoked registration reactivates it. DELETE sets revoked; the registration stays listed. An {id} that is not one of your registrations, or not an id at all, answers 404.

Thresholds and alerts

Many jurisdictions only require registration past a turnover threshold. GET /tax/thresholds lists them; GET /tax/thresholds/alerts compares them against your own volume and returns the countries where you are approaching or already past the line. A threshold with a state_code is listed but never raised as an alert.

Each threshold is measured over its own period (calendar_year, rolling_12m or quarter). Your revenue is converted into the threshold's currency at the platform's rates before it is compared, and the EU threshold counts your sales in every member state together.

curl https://api.wajub.com/tax/thresholds/alerts \
-H "Authorization: $WAJUB_API_KEY"

Each alert, under alerts with their count in total, reads:

country_codestringoptional
The country, or EU for the EU-wide threshold.
state_codestringoptional
Always null in an alert: state thresholds cannot be measured.
threshold_typestringoptional
revenue, or transaction_count for a threshold counted in sales.
threshold_amountnumberoptional
The line, in threshold_currency (a number of sales for transaction_count).
threshold_currencystringoptional
ISO 4217 code of the threshold.
current_amountnumberoptional
Your taxed revenue over the period, converted into threshold_currency, or your number of taxed sales.
percentagenumberoptional
current_amount as a share of threshold_amount, rounded to one decimal.
periodstringoptional
The period the threshold is measured over.
measurablebooleanoptional
Always true in an alert. On a threshold your revenue cannot be converted for, it is false and no alert is raised.
approachingbooleanoptional
true from 80 % up to 100 %.
exceededbooleanoptional
true from 100 %.
descriptionstringoptional
What the threshold is, as Wajub holds it.

Customer tax IDs

A customer's own tax IDs live under the customer, not under /tax. Adding one queues an asynchronous verification, so the ID comes back with verification_status: "pending" and settles later. These routes are live only too, and the tax ID id they return is a UUID.

MethodEndpointReturns
GET/customers/{customer_id}/tax_idstax_ids
POST/customers/{customer_id}/tax_idstax_id, 201
DELETE/customers/{customer_id}/tax_ids/{id}confirmation
typestringrequired
The kind of identifier, up to 64 characters.
valuestringrequired
The identifier itself, up to 128 characters, with or without the spaces and dashes its format writes.
country_codestringrequired
ISO 3166-1 alpha-2.

The value is checked against the format its type has in the list your own registration number is checked against (Configuration gives them). Adding the same type and number again does not add a copy: it answers 200 with the existing entry, back to pending while it is checked again. That is how you retry an unavailable check. DELETE answers 404 for an {id} that is not one of the customer's tax IDs, a malformed one included.

Tax IDs change the calculation

A verified tax ID is what makes reverse_charge come back true from POST /tax/calculate, for a business customer paying from another country than your default_country. It never makes a customer exempt. Add the ID before you calculate, not after.

Reports

GET /tax/reports aggregates tax collected over a period. It accepts period, one of 7d, 30d, 90d, month, year or custom, plus start_date and end_date for custom, and optional currency and country filters. The result comes back under report.

What did you think of this content?