5. Sales Tax Calculations
The tax estimate endpoint (POST /v1/tax/estimate) prices sales tax before you take payment. It is the core of the Level 2 (L2) tax engine, enabled once transaction sync (L1) is in place. Rates reflect your nexus, your product taxability, your customer's exemptions, and the address you are shipping to. This guide covers when to call it, how to shape the request, and how to use what comes back.
Understanding Tax Estimates
A tax estimate is a real-time calculation with no permanent transaction record behind it. Each estimate:
- Prices tax against your current registrations
- Applies the taxability rules for each product
- Honors customer exemptions
- Resolves rates for the destination jurisdiction
- Returns a per-line-item tax breakdown
Because estimates create no transaction record, you can call the endpoint as often as customers change their cart or their address.
Estimates do not sync transactions: /v1/tax/estimate calculates tax; it does not record a sale. For full compliance (L2), sync the transaction separately through /v1/transactions once payment is confirmed. Kintsugi determines nexus from those synced transactions, which is what makes accurate calculation possible in the first place, even when Kintsugi is not handling your filing and remittance.
When to Calculate Tax
Calculate during checkout or billing, after address entry and before payment processing. Common integration points:
- Shopping cart pages: When customers review their order
- Checkout flows: Once the shipping address is entered
- Subscription billing: When pricing a recurring charge
- Quote generation: When quoting a total to a customer
Validate the address first: Run addresses through the address validation API before calculating. A verified address means a correct jurisdiction, and a correct jurisdiction means a correct rate.
Tax Estimate Request Structure
An estimate request mirrors the shape of a transaction sync request.
Required Fields
external_id: Your unique identifier for the transaction being priceddate: Transaction date in ISO 8601 formatcurrency: ISO 4217 currency code, for exampleUSDaddresses: At least one address, whereSHIP_TOsets the jurisdictiontransaction_items: The line items to price
Optional Fields
customer: The buyer. Sendexternal_idto match a customer on file and pick up their exemptionsdescription: A label for the estimatesource: Where the transaction originatedmarketplace: Whether the sale runs through a marketplace. Defaults tofalse
Transaction Items
Each line item requires:
date: Item date, normally matching the transaction dateamount: Total amount for the line, after discounts
And should carry:
external_product_id: The product to price. Required unless you classify the item inlineexternal_id: Your identifier for the line itemquantity: Defaults to1.0exempt: Whether this specific line is exempt. Defaults tofalse
Classifying an item inline: If external_product_id is missing or does not match a product on file, send both product_category and product_subcategory so Kintsugi can classify the item, optionally with product_name and product_description. Without either a known product or a category pair, the request fails. Pre-built catalogs keep classification consistent, so treat inline classification as a fallback rather than the default path.
Addresses
Every address entry needs a type of SHIP_TO or BILL_TO, plus street_1, street_2, city, county, state, postal_code, and country (ISO 3166-1 alpha-2).
The SHIP_TO address decides which rates apply. When no SHIP_TO address is present, Kintsugi falls back to BILL_TO.
Tax Estimate Response
The response echoes your request and adds the calculation.
At the top level:
total_tax_amount_calculated: Total tax for the transactiontaxable_amount: Total amount subject to taxtax_rate_calculated: Combined effective rate appliedhas_active_registration: Whether you hold an active registration for this transaction
On each entry in transaction_items:
tax_amount: Tax for that linetaxable_amount: The portion of the line subject to taxtax_rate: Combined rate applied to the lineexemptandexempt_reason: Whether the line was exempt, and whytax_items: The rate components that make up the line's tax, each with aname,rate,amount, andexemptflag
Amounts come back as strings: Monetary values and rates are returned as decimal strings, for example "20.00" and "0.08". Parse them with a decimal-safe type rather than a float so cents do not drift, and note that nexus_met is deprecated in favor of has_active_registration.
Using Tax Amounts
Take the figures from the response to:
- Show tax to customers during checkout
- Calculate the final total
- Carry tax amounts into the transaction you sync later
- Sanity-check the calculation before you charge
Keep the estimate: Store the response so the transaction you sync afterward carries the same tax amounts the customer saw. Your records and your receipts then agree.
Tax Calculation Workflow
The Three Calls
Each one gates the next. A bad address gives a wrong rate; an unrecorded sale never reaches your filings.
Checkout Sequence
Solid step numbers are the Kintsugi calls. Everything else happens in your storefront.
Nexus and Tax Calculation
Kintsugi calculates tax where you hold an active registration. The has_active_registration flag on the response tells you which side of that line the transaction fell.
When Tax Is Calculated
Tax applies when:
- You hold an active registration in the customer's jurisdiction
- The product is taxable there
- No valid exemption covers the sale
When Tax Is Zero
Tax is zero when:
- You hold no active registration in that jurisdiction
- The product is exempt there
- A valid customer or line-level exemption applies
In the exempt cases, exempt_reason on the line item tells you which rule zeroed it out, whether that was the product, the customer, the region, or something else.
Estimates reflect the present: Each estimate uses your registration and nexus status at the moment of the request. Register in a new jurisdiction and subsequent estimates will price tax there, without any change on your side.
Customer Exemptions
To have an exempt customer's status applied, reference the customer on the estimate:
{
"date": "2026-01-15T10:00:00Z",
"external_id": "EST-2026-001",
"currency": "USD",
"customer": {
"external_id": "CUST-789",
"name": "Northwind Nonprofit"
},
"transaction_items": [
{
"external_id": "ITEM-001",
"date": "2026-01-15T10:00:00Z",
"external_product_id": "PROD-12345",
"quantity": 1,
"amount": 100.00
}
],
"addresses": [
{
"type": "SHIP_TO",
"street_1": "123 Main St",
"city": "Seattle",
"state": "WA",
"postal_code": "98101",
"country": "US"
}
]
}
When the external_id matches a customer on file, Kintsugi applies that customer's exemptions. If the customer is not found, the details are ignored for exemption purposes and the estimate still returns. For one-off exemptions that are not tied to a customer record, set exempt: true on the relevant line items instead.
See the Product & Customer Records guide for how to create exempt customers.
Error Handling
Common failures when calculating tax:
- Product cannot be classified: Send a known
external_product_id, or bothproduct_categoryandproduct_subcategory - Invalid address: Validate addresses before calculating
- Missing required fields:
date,external_id,currency,addresses, andtransaction_itemsare all required - Missing authentication headers: Every request needs both
x-api-keyandx-organization-id - Rate limiting: Retry with exponential backoff
See the Error Handling guide for detailed strategies.
Best Practices
Request Structure
- Use consistent external IDs: Keep one identifier across the estimate and the transaction that follows it
- Validate addresses first: Verified addresses resolve to the right jurisdiction
- Reference products precisely:
external_product_idvalues must match your product records - Send complete line items: Every item needs a
dateand anamount
Response Handling
- Store the response: Carry the same tax amounts into transaction sync
- Handle zero tax: No registration and valid exemptions are normal outcomes, not errors
- Show the breakdown: Surface
tax_itemsso customers and your support team can see how tax was composed - Parse decimals safely: Amounts and rates arrive as strings
Performance
- Cache short-lived estimates: Reuse a result while the cart and address are unchanged
- Debounce address input: Wait for typing to settle before calling
- Fail gracefully: Decide in advance what checkout shows if an estimate fails
- Watch your call volume: Track usage so you see rate pressure before your customers do
Integration Patterns
E-Commerce Checkout
- Customer adds items to the cart
- Customer enters a shipping address
- Validate the address
- Calculate tax with
/v1/tax/estimate - Display the total with tax
- Process payment
- Sync the transaction with those tax amounts
Subscription Billing
- Customer selects a plan
- Customer provides a billing address
- Calculate tax for the first billing cycle
- Store the tax amount for recurring charges
- Recalculate when the address changes or the subscription renews
Multi-Step Checkout
- Calculate tax once the shipping address step is complete
- Recalculate when the customer changes address
- Recalculate when the customer changes the cart
- Update the displayed total after each recalculation
Reading the Tax Breakdown
Each line item's tax_items array shows the rate components behind its tax, for example a state rate and a county rate, each with its own name, rate, and amount. Together with the line's tax_rate and tax_amount, and the transaction-level total_tax_amount_calculated, that gives you a complete picture of the calculation.
Use it to:
- Show customers how their tax was composed
- Produce receipts and invoices with real tax detail
- Debug unexpected results against a specific rate component
- Reconcile totals before you charge
Next Steps
With tax calculation integrated:
-
Sync transactions: After payment, sync the sale with the tax amounts from the estimate. See the Syncing Transaction Records guide.
-
Handle errors: Build the failure path before you need it. See the Error Handling guide.
-
Tune performance: Cache and debounce to cut calls and keep checkout fast.
For endpoint-level detail, see: