node-sales-tax
International sales tax calculator for Node (offline, but provides optional online VAT number fraud check). Tax rates are kept up-to-date.
You may use it to calculate VAT rates for countries in the European Union (VAT MOSS), GST in Canada, or get VAT for countries such as China, or even Hong Kong (which has no VAT).
International tax is hard (especially VAT). This library ensures rules are enforced in the code. If you see a rule that is missing or not correctly enforced, please open an issue. Also, when you use the library, make sure to specify your origin country; as it will return full international tax rates if you don't specify it (ie. the country you invoice your customers from).
You can find the raw sales tax rates JSON file here: sales_tax_rates.json
Who uses it?
![]() |
| Crisp |
How to install?
Include sales-tax in your package.json dependencies.
Alternatively, you can run npm install sales-tax --save.
How to use?
This module may be used to acquire the billable VAT percentage for a given customer. You may also use it directly to process the total amount including VAT you should bill; and even to validate a customer's VAT number.
β‘οΈ Import the module
Import the module in your code:
var SalesTax = require("sales-tax");
Ensure that you specify your origin country before you use the library. This will affect how international, regional and national area taxes are handled from your point of view (regional stands for the economic community, eg. the European Union).
Also, ensure that you consume correctly the charge values that get returned. It tells you if the VAT charge should be directly invoiced to the customer via the direct tag (you charge the VAT on your end), or if the customer should pay the VAT on their end via the reverse tag (see VAT reverse charge). If the charge is not direct, then the VAT rate will be 0.00 (it is up to the customer to apply their own VAT rate).
β
Specify the country you charge from
Prototype: SalesTax.setTaxOriginCountry(countryCode<string>, useRegionalTax<boolean?>)<undefined>
international, regional and national VAT gets calculated from a French point of view):
SalesTax.setTaxOriginCountry("FR")international, regional and national VAT gets calculated from a French point of view):
// Set the 'useRegionalTax' argument to false if not liable to VAT MOSS (eg. not enough turnover in another regional country)
SalesTax.setTaxOriginCountry("FR", false)SalesTax.setTaxOriginCountry(null)β
Check if a country has sales tax
Prototype: SalesTax.hasSalesTax(countryCode<string>)<boolean>
Notice: this method is origin-neutral. It means it return values regardless of your configured tax origin country.
Check some countries for sales tax (returns true or false):
var franceHasSalesTax = SalesTax.hasSalesTax("FR") // franceHasSalesTax === true
var brazilHasSalesTax = SalesTax.hasSalesTax("BR") // brazilHasSalesTax === true
var hongKongHasSalesTax = SalesTax.hasSalesTax("HK") // hongKongHasSalesTax === falseβ
Check if a state has sales tax (in a country)
Prototype: SalesTax.hasStateSalesTax(countryCode<string>, stateCode<string>)<boolean>
Notice: this method is origin-neutral. It means it return values regardless of your configured tax origin country.
true or false):
var canadaQuebecHasSalesTax = SalesTax.hasSalesTax("CA", "QC") // canadaQuebecHasSalesTax === true
var canadaYukonHasSalesTax = SalesTax.hasSalesTax("CA", "YT") // canadaYukonHasSalesTax === falsetrue or false):
var unitedStatesCaliforniaHasSalesTax = SalesTax.hasSalesTax("US", "CA") // unitedStatesCaliforniaHasSalesTax === true
var unitedStatesDelawareHasSalesTax = SalesTax.hasSalesTax("US", "DE") // unitedStatesDelawareHasSalesTax === falseβ
Get the sales tax for a customer
Prototype: SalesTax.getSalesTax(countryCode<string>, stateCode<string?>, taxNumber<string?>)<Promise<object>>
Notice: this method is origin-aware. It means it return values relative to your configured tax origin country.
SAS CLEVER CLOUD with VAT number FR 87524172699):
SalesTax.getSalesTax("FR", null, "FR87524172699")
.then((tax) => {
// This customer is VAT-exempt (as it is a business)
/* tax ===
{
type : "vat",
rate : 0.00,
area : "worldwide",
exchange : "business",
charge : {
direct : false,
reverse : true
}
}
*/
});Note: Clever-Cloud is a real living business from France, check their website there.
SAS CLEVER CLOUD with VAT number FR 87524172699):
// Set this once when initializing the library (to France)
SalesTax.setTaxOriginCountry("FR")
SalesTax.getSalesTax("FR", null, "FR87524172699")
.then((tax) => {
// This customer owes VAT in France (as it is a business, and billing is FR-to-FR)
// The `direct` tag is set to `true`, thus VAT should be charged
// The `area` tag is set to `national` as the exchange is done in France
/* tax ===
{
type : "vat",
rate : 20.00,
area : "national",
exchange : "business",
charge : {
direct : true,
reverse : false
}
}
*/
});SAS CLEVER CLOUD with VAT number FR 87524172699):
// Set this once when initializing the library (to Latvia)
SalesTax.setTaxOriginCountry("LV")
SalesTax.getSalesTax("FR", null, "FR87524172699")
.then((tax) => {
// This customer owes a VAT reverse charge in their country (France), no VAT is due in Latvia
// The `reverse` tag is set to `true`, thus the customer should apply a reverse VAT charge in their country
// The `area` tag is set to `regional` as the exchange is done in the European Union
/* tax ===
{
type : "vat",
rate : 0.00,
area : "regional",
exchange : "business",
charge : {
direct : false,
reverse : true
}
}
*/
});SalesTax.getSalesTax("US", "CA")
.then((tax) => {
// This customer has to pay 8.25% VAT (as it is a consumer)
/* tax ===
{
type : "vat",
rate : 0.0825,
area : "worldwide",
exchange : "consumer",
charge : {
direct : true,
reverse : false
}
}
*/
});SalesTax.getSalesTax("LV")
.then((tax) => {
// This customer has to pay 21% VAT (as it is a consumer)
/* tax ===
{
type : "vat",
rate : 0.21,
area : "worldwide",
exchange : "consumer",
charge : {
direct : true,
reverse : false
}
}
*/
});// Set this once when initializing the library (to France)
SalesTax.setTaxOriginCountry("FR")
SalesTax.getSalesTax("LV")
.then((tax) => {
// This customer owes VAT in Latvia (as it is a consumer, and billing is FR-to-LV)
// The `direct` tag is set to `true`, thus VAT should be charged
// The `area` tag is set to `regional` as the exchange is done in the European Union
/* tax ===
{
type : "vat",
rate : 0.21,
area : "regional",
exchange : "consumer",
charge : {
direct : true,
reverse : false
}
}
*/
});SalesTax.getSalesTax("HK")
.then((tax) => {
// Hong Kong has no VAT
/* tax ===
{
type : "none",
rate : 0.00,
area : "worldwide",
exchange : "consumer",
charge : {
direct : false,
reverse : false
}
}
*/
});SalesTax.getSalesTax("ES", null, "ESX12345523")
.then((tax) => {
// This customer has to pay 21% VAT (VAT number could not be authenticated against the VIES VAT API)
/* tax ===
{
type : "vat",
rate : 0.21,
area : "worldwide",
exchange : "consumer",
charge : {
direct : true,
reverse : false
}
}
*/
});β
Process the price including sales tax for a customer
Prototype: SalesTax.getAmountWithSalesTax(countryCode<string>, stateCode<string?>, amount<number?>, taxNumber<string?>)<Promise<object>>
Notice: this method is origin-aware. It means it return values relative to your configured tax origin country.
SalesTax.getAmountWithSalesTax("EE", null, 100.00)
.then((amountWithTax) => {
// This customer has to pay 20% VAT
/* amountWithTax ===
{
type : "vat",
rate : 0.20,
price : 100.00,
total : 120.00,
area : "worldwide",
exchange : "consumer",
charge : {
direct : true,
reverse : false
}
}
*/
});β
Validate tax number for a customer
Prototype: SalesTax.validateTaxNumber(countryCode<string>, taxNumber<string?>)<Promise<boolean>>
SAS CLEVER CLOUD with VAT number FR 87524172699):
SalesTax.validateTaxNumber("FR", "FR87524172699")
.then((isValid) => {
// isValid === true
});SalesTax.validateTaxNumber("US")
.then((isValid) => {
// isValid === false
});SalesTax.validateTaxNumber("LV")
.then((isValid) => {
// isValid === false
});SalesTax.validateTaxNumber("ES", "ESX12345523")
.then((isValid) => {
// isValid === false
});β
Get tax exchange status for a customer (exempt + area + exchange)
Prototype: SalesTax.getTaxExchangeStatus(countryCode<string>, stateCode<string?>, taxNumber<string?>)<Promise<object>>
Notice: this method is origin-aware. It means it return values relative to your configured tax origin country.
SAS CLEVER CLOUD with VAT number FR 87524172699):
SalesTax.getTaxExchangeStatus("FR", null, "FR87524172699")
.then((exchangeStatus) => {
/* exchangeStatus ===
{
exchange : "business",
area : "worldwide",
exempt : true
}
*/
});SalesTax.getTaxExchangeStatus("MA")
.then((exchangeStatus) => {
/* exchangeStatus ===
{
exchange : "consumer",
area : "worldwide",
exempt : false
}
*/
});SalesTax.getTaxExchangeStatus("US", "DE")
.then((exchangeStatus) => {
/* exchangeStatus ===
{
exchange : "consumer",
area : "worldwide",
exempt : true
}
*/
});SalesTax.getTaxExchangeStatus("HK")
.then((exchangeStatus) => {
/* exchangeStatus ===
{
exchange : "consumer",
area : "worldwide",
exempt : true
}
*/
});β
Disable / enable tax number validation
Prototype: SalesTax.toggleEnabledTaxNumberValidation(enabled<boolean>)<undefined>
SalesTax.toggleEnabledTaxNumberValidation(true)SalesTax.toggleEnabledTaxNumberValidation(false)β
Disable / enable tax number fraud check
Prototype: SalesTax.toggleEnabledTaxNumberFraudCheck(enabled<boolean>)<undefined>
Notice: fraud check requires tax number validation to be enabled.
SalesTax.toggleEnabledTaxNumberFraudCheck(true)SalesTax.toggleEnabledTaxNumberFraudCheck(false)Where is the offline tax data is pulled from?
The offline tax data is pulled from VAT, GST and sales tax rates β ey.com.
It is kept up-to-date year-by-year with tax changes worldwide.
Some countries have multiple sales tax, eg. Brazil. In those cases, the returned sales tax is the one on services. Indeed, I consider most users of this module use it for their SaaS business β in other words, service businesses.
I bill from the EU, but sales tax is still being returned for non-EU countries!
As international tax rules can be very complex depending on your business legal structure (eg. if you run a nexus in an US state, you may owe sales tax to this US state, even if you charge from the UK); sales-tax does not void returned tax rate for international countries.
Thus, when the country is international relative to your billing origin country, you need to handle things your own way.
To make things easier for you, sales-tax returns an area parameter in the SalesTax.getSalesTax, that is either international, regional or national (this depends on your configured origin country). For regional and national areas, you can trust the returned rate. However, you may need to override all international area rates and void them all to zero; for instance if you charge from France to the United States, and you know that you do not owe sales tax in the US as you do not run a nexus company in the US.
How are tax numbers validated?
πͺπΊ Europe
European VAT numbers can be fraud-checked against the official ec.europa.eu VIES VAT API, which return whether a given VAT number exists or not. This helps you ensure a customer-provided VAT number really exists. This feature, as it may incur significant delays (while querying the VIES VAT API) is disabled by default. There's a switch to enable it.
In all cases, the syntax of the European VAT numbers get validated from offline rules. Although, it only checks number syntaxical correctness; thus it is not sufficient to tell if the number exists or not.
You can manually check a VAT number on VIES VAT number validation.
πΊπΈ United States
United States EIN (U.S. Employer Identification Number) are validated against EIN format rules.
π¨π¦ Canada
Canada BN (Business Number) are validated against BN format rules.
π΄ Rest of the world
If a country or economic community is not listed here, provided tax identification numbers are ignored for those countries (considered as invalid β so do not rely on validation methods as a source of truth).
If you need tax number validation for a missing country, feel free to submit a Pull Request.
