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.
Live only, private key
Every /tax endpoint needs a private key (sk.) or a restricted key with tax.read or
tax.write, and refuses a sandbox key with
403 This feature is only available in live mode.
Settings
https://api.wajub.com/tax/settingscurl https://api.wajub.com/tax/settings \
-H "Authorization: $WAJUB_API_KEY"The configuration comes back under tax:
enabledbooleanoptionalinclusivebooleanoptionaltrue means the amount you send already contains the tax; false means tax is added on top.default_countrystringoptionaltax_id_typestringoptionaltax_registration_numberstringoptionalTurning 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"
}'The request field is `registration_number`
You write registration_number and you read tax_registration_number. The two names refer to
the same value; only the direction differs.
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.
https://api.wajub.com/tax/calculateamountnumberrequiredcountrystringrequired422, so a typo is never taxed at 0 %.currencystringoptionaldefault : XAFtax_inclusivebooleanoptionaldefault : falseamount already contains the tax.customer_idstringoptional404 Customer not found.tax_codestringoptionalcurl 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:
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.
| Endpoint | Returns under | Filters | Paginated |
|---|---|---|---|
GET /tax/rates | rates | country | No |
GET /tax/codes | tax_codes | category | Yes |
GET /tax/codes/{code} | tax_code | n/a | |
GET /tax/jurisdictions | jurisdictions | country, state, type | Yes |
GET /tax/thresholds | thresholds | country | No |
GET /tax/rates is the shortest of them, one standard rate per country:
The two paginated lists carry meta next to their array:
{
"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.
| Method | Endpoint | Returns |
|---|---|---|
GET | /tax/registrations | registrations |
POST | /tax/registrations | registration, 201 |
GET | /tax/registrations/{id} | registration |
PUT | /tax/registrations/{id} | registration |
DELETE | /tax/registrations/{id} | confirmation |
country_codestringrequiredstate_codestringoptionaltypestringoptionalstandard, simplified, ioss, oss.registration_numberstringoptionalregistered_atdateoptionalexpires_atdateoptionalregistered_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_codestringoptionalEU for the EU-wide threshold.state_codestringoptionalnull in an alert: state thresholds cannot be measured.threshold_typestringoptionalrevenue, or transaction_count for a threshold counted in sales.threshold_amountnumberoptionalthreshold_currency (a number of sales for transaction_count).threshold_currencystringoptionalcurrent_amountnumberoptionalthreshold_currency, or your number of taxed sales.percentagenumberoptionalcurrent_amount as a share of threshold_amount, rounded to one decimal.periodstringoptionalmeasurablebooleanoptionaltrue in an alert. On a threshold your revenue cannot be converted for, it is false and no alert is raised.approachingbooleanoptionaltrue from 80 % up to 100 %.exceededbooleanoptionaltrue from 100 %.descriptionstringoptionalCustomer 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.
Customer permissions, not tax ones
These routes sit under /customers, so a restricted key needs customer.read to list and
customer.write to add or remove. A key holding only tax.read or tax.write gets 403.
| Method | Endpoint | Returns |
|---|---|---|
GET | /customers/{customer_id}/tax_ids | tax_ids |
POST | /customers/{customer_id}/tax_ids | tax_id, 201 |
DELETE | /customers/{customer_id}/tax_ids/{id} | confirmation |
typestringrequiredvaluestringrequiredcountry_codestringrequiredThe 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.