Calculate VAT and convert prices
Show a price in the customer's currency, including its VAT breakdown, without creating a payment. This example does not need a payment form, Stripe setup, or a webhook.
We will use a product listed at 10.00 EUR, an example exchange rate of 1 EUR = 10 SEK, and an applicable VAT rate of 25%. These are illustrative assumptions, not a live exchange-rate or tax-rate lookup.
| How you list the product | Price before VAT | VAT | Total shown to the customer |
|---|---|---|---|
| 10.00 EUR before VAT | 100.00 SEK | 25.00 SEK | 125.00 SEK |
| 10.00 EUR including VAT | 80.00 SEK | 20.00 SEK | 100.00 SEK |
These are two ways to price the product, not two calculations of the same final total.
Before you start
In Test, save a Swedish business profile. For your Integration, use EUR as the base currency, enable SEK, and set its rate to 10. For this walkthrough, leave the default price setting as VAT included. See Currencies and rates for setup.
The requests simulate an anonymous customer in Sweden. No customer profile is needed for this example. The assumed domestic VAT rate is 25%; the service uses its tax data rather than a rate supplied in the request.
Run the commands in a backend terminal, using the same terminal throughout. Replace the service address and Test key below with your own. Keep the API key out of browser code, screenshots, and source control; see API keys and safety.
1. Get the payment currency and exchange rate
export EUNIPAY_BASE_URL="https://pay.example.com"
export EUNIPAY_API_KEY="replace-with-your-test-key"
curl --fail-with-body -sS "$EUNIPAY_BASE_URL/v1/payment-contexts/resolve" \
-X POST \
-H "Authorization: Bearer $EUNIPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"buyer_ip": "203.0.113.10",
"location_fixture": { "country": "SE" }
}'
What success looks like: HTTP 200. Under the example settings, the relevant part of the response is:
{
"data": {
"currency": "SEK",
"base_currency": "EUR",
"base_to_payment_rate": "10",
"minor_exponent": 2,
"tax_semantics": "inclusive"
}
}
base_to_payment_rate means payment currency per unit of base currency. Your backend converts the listed price first: 10.00 EUR × 10 = 100.00 SEK.
The tax preview accepts an amount in the payment currency, not the original EUR price. With minor_exponent equal to 2, send 10000 for 100.00 SEK. Use decimal money arithmetic and round to the payment currency's smallest unit, not to the number of digits chosen for display.
location_fixture is only for Test. In Live, remove it and send the real customer IP obtained by your backend through a trusted connection or proxy.
2. Add VAT to a price that excludes tax
Suppose your listed 10.00 EUR is a price before VAT. Send its converted value, 100.00 SEK, and choose tax_semantics as exclusive.
curl --fail-with-body -sS "$EUNIPAY_BASE_URL/v1/tax-previews" \
-X POST \
-H "Authorization: Bearer $EUNIPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"buyer_ip": "203.0.113.10",
"location_fixture": { "country": "SE" },
"payment_currency": "SEK",
"amount": 10000,
"tax_semantics": "exclusive"
}'
What success looks like: HTTP 200 with the following amounts. The response below shows only the relevant fields; money amounts are in the smallest currency unit.
{
"data": {
"estimated": true,
"mode": "domestic_vat",
"tax_semantics": "exclusive",
"vat_rate_bps": 2500,
"price_multiplier": "1.2500",
"base_to_payment_rate": "10",
"payment": {
"currency": "SEK",
"net_amount": 10000,
"vat_amount": 2500,
"gross_amount": 12500
},
"base": {
"currency": "EUR",
"net_amount": 1000,
"vat_amount": 250,
"gross_amount": 1250
}
}
}
Show the customer 100.00 SEK + 25.00 SEK VAT = 125.00 SEK. In the business's base currency, that is 10.00 EUR + 2.50 EUR VAT = 12.50 EUR.
3. Split VAT out of a price that includes tax
Now suppose your listed 10.00 EUR is the total including VAT. Its converted total is still 100.00 SEK. Send the same amount but choose tax_semantics as inclusive.
curl --fail-with-body -sS "$EUNIPAY_BASE_URL/v1/tax-previews" \
-X POST \
-H "Authorization: Bearer $EUNIPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"buyer_ip": "203.0.113.10",
"location_fixture": { "country": "SE" },
"payment_currency": "SEK",
"amount": 10000,
"tax_semantics": "inclusive"
}'
What success looks like: HTTP 200. The corresponding response excerpt is:
{
"data": {
"estimated": true,
"mode": "domestic_vat",
"tax_semantics": "inclusive",
"vat_rate_bps": 2500,
"price_multiplier": "1.2500",
"base_to_payment_rate": "10",
"payment": {
"currency": "SEK",
"net_amount": 8000,
"vat_amount": 2000,
"gross_amount": 10000
},
"base": {
"currency": "EUR",
"net_amount": 800,
"vat_amount": 200,
"gross_amount": 1000
}
}
}
Show 100.00 SEK total, including 20.00 SEK VAT. The net price is 80.00 SEK. In the base currency, the total is 10.00 EUR, including 2.00 EUR VAT.
VAT is split out by dividing the total by 1.25: 100 ÷ 1.25 = 80. Do not subtract 25% of the total or add VAT again.
Use the results in your page
| Returned value | What to use it for |
|---|---|
payment | Customer-facing net price, VAT, and total in the payment currency |
base | The same breakdown converted to your business's base currency |
vat_rate_bps | The VAT rate: 2500 means 25% |
price_multiplier | The net-to-gross VAT factor: 1.2500 at 25%; this is not an exchange rate |
base_to_payment_rate | The exchange rate: 10 SEK per EUR in this example |
Use the returned breakdown directly for display. The VAT multiplier is the same in both examples; it does not mean you should multiply an already VAT-inclusive total again. If you omit the price-setting field, the service uses your Integration's default.
For another payment currency: resolve the customer's currency again, convert the listed price with the returned rate, then request the preview in that currency. This flow returns one selected payment currency at a time; it does not return prices for every enabled currency. Changing only the currency code or symbol is not a conversion. If settings change and the preview returns payment_currency_mismatch, start again from step 1.
For a known customer: include the same external_customer_id in the resolve and preview requests so the saved billing details can be used. Business billing does not automatically mean zero VAT; see VAT and the payment total for the required checks. If a preview fails, resolve the error rather than displaying zero VAT as a fallback.
When the customer wants to pay: this result is an estimate, not a charge or receipt. Follow Your first test payment to create a Checkout, which calculates the final amounts using the current details. You do not need to create a separate Quote for that managed Checkout flow.