Calculate tax in your custom payment flow
The Stripe Tax API enables you to calculate tax in your custom payment flow. After your customer completes their payment, record the transaction so it appears in Stripe Tax reporting. The examples in this guide use Stripe payments, but you can use the Tax API with any payment provider.
Collect customer addressClient-side
In most cases, the tax you collect depends on your customer’s location. Collecting your customer’s full address gives the most accurate tax calculation result. Before collecting an address, you can show your customer an estimate based on their IP address.
The form below collects a full postal address:
<form> <label for="address_line1">Address Line 1</label> <input type="text" id="address_line1" /> <label for="address_city">City</label> <input type="text" id="address_city" /> <label for="address_state">State</label> <select id="address_state"> <option value="CA">California</option> <!-- add more states here --> </select> <label for="address_postal_code">Postal code</label> <input type="text" id="address_postal_code" /> <label for="address_country">Country</label> <select id="address_country"> <option value="US">United States</option> <option value="DE">Germany</option> <option value="IE">Ireland</option> <!-- add more countries here --> </select> </form>
You might pass the address to your server endpoint like this:
const address = { line1: document.getElementById('address_line1').value, city: document.getElementById('address_city').value, state: document.getElementById('address_state').value, postal_code: document.getElementById('address_postal_code').value, country: document.getElementById('address_country').value, }; var response = fetch('/preview-cart', { method: 'POST', body: JSON.stringify({address: address}), headers: {'Content-Type': 'application/json'}, }).then(function(response) { return response.json(); }).then(function(responseJson) { // Handle errors, or display calculated tax to your customer. });
The address information required to calculate tax varies by customer country:
- United States: We require your customer’s postal code at a minimum. We recommend providing a full address for the most accurate tax calculation result.
- Canada: We require your customer’s postal code or province.
- Everywhere else: We only require your customer’s country code.
Calculate taxServer-side
You choose when and how often to calculate tax. For example, you can:
- Show a tax estimate based on your customer’s IP address when they enter your checkout flow
- Recalculate tax as your customer types their billing or shipping address
- Calculate the final tax amount to collect when your customer finishes typing their address
Stripe charges a fee per tax calculation API call. You can throttle tax calculation API calls to manage your costs.
The examples below show how to calculate tax in a variety of scenarios.
The calculation response contains amounts you can display to your customer, and use to take payment:
Attribute | Description |
---|---|
amount_total | The grand total after calculating tax. Use this to set the PaymentIntent amount to charge your customer. |
tax_amount_exclusive | The amount of tax added on top of your line item amounts and shipping cost. This tax amount increases the amount_total . Use this to show your customer the amount of tax added to the transaction subtotal. |
tax_amount_inclusive | The amount of tax that’s included in your line item amounts and shipping cost (if using tax-inclusive pricing). This tax amount does not increase the amount_total . Use this to show your customer the tax included in the total they’re paying. |
tax_breakdown | A list of tax amounts broken out by country or state tax rate. Use this to show your customer the specific taxes you’re collecting. |
You can calculate tax for your customer before collecting payment method details and creating a PaymentIntent. For example, you could display a shopping cart total when they provide their postal code.
In the example below, your server defines a /preview-cart
endpoint, where the customer’s address is sent from your client-side form. The server combines the cart’s line items and the customer’s address to calculate tax.
When you’re ready to take payment, create a PaymentIntent from the tax calculation result. Store the tax calculation ID in the PaymentIntent’s metadata or in your own database so you can create a tax transaction when your customer completes payment.
The example below shows a server endpoint that calculates tax, creates (or updates) a PaymentIntent, and returns the result to the client. You can then display the tax to your customer. Use the client_secret
to take the payment.
If your integration uses the Payment Element, fetch updates from the server after updating the PaymentIntent.
Handling customer location errors
The calculation returns the customer_tax_location_invalid
error code if your customer’s address is invalid or isn’t precise enough to calculate tax:
{ "error": { "doc_url": "https://stripe.com/docs/error-codes/customer-tax-location-invalid", "code": "customer_tax_location_invalid", "message": "We could not determine the customer's tax location based on the provided customer address.", "param": "customer_details[address]", "type": "invalid_request_error" } }
When you receive this error, prompt your customer to check the address they’ve entered and fix any typos.
Create tax transactionServer-side
Creating a tax transaction records the tax you’ve collected from your customer, so that later you can download exports and generate reports to help with filing your taxes. You can create a transaction from a calculation until the expires_at timestamp, 48 hours after it’s created. Attempting to use it after this time returns an error.
When creating a tax transaction, you must provide a unique reference
for the tax transaction and each line item. The references appear in tax exports to help you reconcile the tax you collected with the orders in your system.
For example, a tax transaction with reference pi_123456789
, line item references L1
and L2
, and a shipping cost, looks like this in the itemized tax exports:
ID | line_item_id | type | currency | transaction_date | … |
---|---|---|---|---|---|
pi_123456789 | L1 | external | usd | 2023-02-23 17:01:16 | … |
pi_123456789 | L2 | external | usd | 2023-02-23 17:01:16 | … |
pi_123456789 | shipping | external | usd | 2023-02-23 17:01:16 | … |
When your customer pays, use the calculation ID to record the tax collected. Two ways to do this are:
- If your server has an endpoint where your customer submits their order, you can create the tax transaction after the order is successfully submitted.
- Listen for the payment_intent.succeeded webhook event. Retrieve the calculation ID from the PaymentIntent
metadata
.
The example below creates a transaction and uses the PaymentIntent ID as the unique reference:
Store the tax transaction ID so that later you can record refunds. You can store the transaction ID in your database or in the PaymentIntent’s metadata:
Record refundsServer-side
After creating a tax transaction to record a sale to your customer, you might need to record refunds. These are also represented as tax transactions, with type=reversal
. Reversal transactions offset an earlier transaction by having amounts with opposite signs. For example, a transaction that recorded a sale for 50 USD might later have a full reversal of -50 USD.
When you issue a refund (using Stripe or outside of Stripe) you need to create a reversal tax transaction with a unique reference
. Common strategies include:
- Append a suffix to the original reference. For example, if the original transaction has reference
pi_123456789
, then create the reversal transaction with referencepi_123456789-refund
. - Use the ID of the Stripe refund or a refund ID from your system. For example,
re_3MoslRBUZ691iUZ41bsYVkOg
ormyRefund_456
.
Choose the approach that works best for how you reconcile your customer orders with your tax exports.
Fully refund a sale
When you fully refund a sale in your system, create a reversal transaction with mode=full
.
In the example below, tax_1MEFAAI6rIcR421eB1YOzACZ
is the tax transaction recording the sale to your customer:
This returns the full reversal transaction that’s created:
{ "id": "tax_1MEFtXI6rIcR421e0KTGXvCK", "object": "tax.transaction", "created": 1670866467, "currency": "eur", "customer": null, "customer_details": { "address": { "city": null, "country": "IE",
Fully reversing a transaction doesn’t affect previous partial reversals. When you record a full reversal, make sure you fully reverse any previous partial reversals for the same transaction to avoid duplicate refunds.
Partially refund a sale
After issuing a refund to your customer, create a reversal tax transaction with mode=partial
. This allows you to record a partial refund by providing the line item amounts refunded. You can create up to 10 partial reversals for each sale. Reversing more than the amount of tax you collected returns an error.
The example below records a refund of only the first line item in the original transaction:
This returns the partial reversal transaction that’s created:
{ "id": "tax_1MEFACI6rIcR421eHrjXCSmD", "object": "tax.transaction", "created": 1670863656, "currency": "eur", ... "line_items": { "object": "list", "data": [ {
For each line item reversed you need to provide the amount
and amount_tax
reversed. The amount
is tax-inclusive if the original calculation line item was tax-inclusive.
How amount
and amount_tax
are determined depends on your situation:
- If your transactions always have a single line item, use full reversals instead.
- If you always refund entire line items, use the original transaction line item
amount
andamount_tax
, but with negative signs. - If you refund parts of line items, you need to calculate the amounts refunded. For example, for a sale transaction with
amount=5000
andamount_tax=500
, after refunding half the line item you’d create a partial reversal with line itemamount=-2500
andamount_tax=-250
.
We’re building support for reversing a line item by passing a single after-tax amount, instead of having to provide amount
and amount_tax
separately.
Undo a partial refund
Tax transactions are immutable but you can cancel out a partial refund by creating a full reversal of it.
You might need to do this when:
- The payment refund fails and you haven’t provided the good or service to your customer
- The wrong order is refunded or the wrong amounts are refunded
- The original sale is fully refunded and the partial refunds are no longer valid
In the example below, tax_1MEFACI6rIcR421eHrjXCSmD
is the transaction representing the partial refund:
This returns the full reversal transaction that’s created:
{ "id": "tax_1MEFADI6rIcR421e94fNTOCK", "object": "tax.transaction", "created": 1670863657, "currency": "eur", ... "line_items": { "object": "list", "data": [ {
Testing
Tax calculations and transactions behave identically in test mode and live mode, so you can confirm that your integration is working correctly before going live. You can run up to 1,000 tax calculations in test mode. Contact Stripe support if you need a higher limit.