Integrating Avalara with Shopify Checkout to Clearly Show Recycling Fees
When you sell mattresses across multiple U.S. states, taxes are only part of the story. Some states, like Rhode Island and Connecticut, also charge recycling fees that must be shown as a separate line item on the customers receipt and checkout. Shopify’s built-in tax display and the standard Avalara app were not able to do this out of the box, so we needed a custom integration.
This article walks through how we integrated Avalara AvaTax with Shopify Checkout UI extensions and custom middleware to surface a detailed tax breakdown including state-specific recycling fees directly in checkout for two brands: Mattress Warehouse (MW) and Sleep Outfitters (SOU).
Regulatory background and the core problem
Legal guidance for our client was clear: in certain states, the mattress recycling fee could not be hidden inside a generic tax total. It had to be broken out and labeled as its own fee.
However, Shopify checkout normally shows a single line like Estimated taxes, and even after installing the official Avalara app, the checkout UI still did not expose the detailed tax lines we needed. The Avalara app improved tax calculation, but not tax display.
We needed three things simultaneously:
Correct tax and fee calculation via Avalara AvaTax.
Per-line tax details (for example, sales tax vs. Rhode Island Recycling Fee) from Avalara’s API.
A way to show that breakdown in Shopify checkout without modifying Shopifys built-in tax line.
Why the default Shopify + Avalara setup was not enough
Our initial attempt was to rely on the Avalara Shopify app alone. We enabled it on a development store and instrumented checkout with debug logs to see whether Avalara was sending back detailed tax lines.
We discovered that:
The app acted as a tax calculation service in the background but did not expose any extra tax breakdown in the checkout UI.
Shopify still only showed a single combined Estimated taxes line.
Checkout UI extensions cannot modify or replace Shopifys built-in tax summary they can only add new sections in allowed locations.
That meant we had to build our own solution that:
Called Avalara’s
transactions/createAPI directly,Used the same inputs Shopify was using, to keep totals aligned, and
Rendered the resulting breakdown in a custom checkout block.
Architecture overview
We ended up with a simple but powerful pattern that we implemented for both MW and SOU:
Metafield ingestion: A nightly integration reads a STORIS CSV feed, extracts the tax class ID, and stores it as a variant-level metafield
avalara.taxcodein Shopify.Middleware proxy: A secure middleware endpoint
/api/avalara-taxreceives requests from checkout and forwards them to Avalara, avoiding CORS issues and hiding credentials.Checkout UI extension:A custom extension named
avalara-tax-breakdowncalls the middleware during checkout and renders a clean tax summary, including recycling fees, in the order summary area.
The rest of this article dives into each of these parts.
1. Surfacing Avalara tax codes on product variants
Avalara uses a tax code to understand how a product should be taxed and whether additional fees (like recycling) apply. For our client, this code comes from their ERP (STORIS) as a field named TaxClassID.
The challenge was that TaxClassID is not available via Shopifys standard product APIs. It only exists in the STORIS CSV feed.
Parsing the STORIS CSV feed
To bridge the gap, we created a middleware process with the following responsibilities:
Read the STORIS CSV file from FTP.
Find the column
storis Product[TaxClassID].Match each CSV row to the correct Shopify variant by SKU or variant ID.
Write a variant-level metafield named
avalara.taxcodewith theTaxClassIDvalue.
This metafield is the key that later gets passed to Avalara as lines.taxCode in the API request.
Keeping metafields in sync
Because product and tax data can change, the integration was designed to be idempotent and repeatable:
Each run processes the latest CSV and upserts metafields instead of blindly inserting.
Variants that are removed or have no tax class can be handled explicitly (for example, clearing or ignoring the metafield).
Downstream code assumes metafields may be missing and behaves gracefully in that case.
Once this pipeline was in place, checkout logic could reliably queryavalara.taxcode for each line item.
2. Calling Avalara from checkout without CORS issues
Our next step was to fetch the detailed tax breakdown directly from Avalara whenever a customer reached checkout. The obvious place to do this was a Checkout UI extension but there was a big obstacle: CORS.
The CORS problem
When the extension attempted to call Avalara’s REST API from the browser, we hit this familiar error:
Access to fetch has been blocked by CORS policy... No
Access-Control-Allow-Originheader
AvaTax does not allow browser-originating calls like this, which makes sense from a security perspective.
Introducing the middleware proxy
To solve this, we introduced an application middleware layer between the extension and Avalara:
The checkout extension calls
/api/avalara-taxon our middleware (for example,https://mw-omni.example.com/api/avalara-tax).The middleware performs a server-side POST to Avalara’s
/api/v2/transactions/createendpoint.The middleware relays the response back to the extension in a simplified, UI-friendly format.
This keeps Avalara credentials and environment details entirely on the server and cleanly bypasses CORS.
Configuration and safeguards
To keep environments flexible, we added environment variables to the middleware, such as:
AVALARA_API_BASE_URL(sandbox vs production)AVALARA_AUTH_HEADER(Basic auth header, generated from account ID and license key)AVALARA_CLIENT_HEADER(optional, for Avalara client identification)
We also followed Avalara’s guidance to avoid creating real transactions during testing:
Set
typetoSalesOrderinstead ofSalesInvoice.Set
committofalseso the transaction is not recorded.
Company code. For both MW and SOU, the client chose to use the same Avalara companyCode,MAT, in both sandbox and production. That decision came from their finance and compliance teams; our code simply consumes the configured value.
Customer code. Avalara requires a non-empty customerCode. Documentation indicates that if no value is supplied, the literal string"Username"is used, so we explicitly send"Username"for consistency.
3. Building the Checkout UI extension for tax breakdown
With tax codes in metafields and a middleware proxy in place, we could finally build the piece customers actually see: thecheckout tax breakdown.
Where the extension renders
We built a Shopify Checkout UI extension namedavalara-tax-breakdownand targeted it to thepurchase.checkout.block.renderlocation (an order summary slot often referred to asORDER_SUMMARY3).
Because extensions cannot change the built-inEstimated taxesline, our extensionadds a new section beneath it. The built-in tax total remains intact for consistency, and our new section explains how that total is composed.
Mapping checkout data to Avalara
Inside the extension, we gather the necessary information about the current checkout:
Line items and quantities.
Each variants
avalara.taxcodemetafield.Each variants SKU for
itemCode.Shipping address (line1, city, region, country, postalCode).
Order date and an appropriate
purchaseOrderNo.
The extension then sends a payload to the middleware, which in turn calls Avalara. The response includes asummaryarray in which Avalara conveniently breaks down tax by jurisdiction and fee type. This is where we see entries like:
Sales Taxfor the standard tax component, andRhode Island Recycling Feefor the recycling fee.
Rendering the tax summary (including recycling fees)
At first, we only displayed the recycling fee line in our new section. That satisfied the bare minimum legal requirement, but it made the tax story hard to follow. If the customer sawEstimated taxes: $53.09and a separateRecycling Fee: $20.50, it was not obvious how those numbers related.
After user and stakeholder feedback, we updated the UI to show theentire tax breakdownreturned from Avalara:
Every tax line item from Avalaras
summaryarray (for example, state tax, local tax, recycling fee).Thetotal taxcalculated by Avalara.
A typical example for a Rhode Island mattress order might look like this in the new section:
RI STATE TAX — $32.59
Rhode Island Recycling Fee — $20.50
Total Tax— $53.09
We intentionally repeat the total tax amount, even though Shopify also includes it in the main order total. Redundancy here improves clarity.
Labeling: from hardcoded Sales Tax to Avalaras taxName
Initially, we labeled tax lines generically asSales Tax. This worked as long as there was only one tax line, but once we added recycling fees and other components, multiple lines would all be labeledSales Tax, which was confusing.
We switched to using thetaxName field returned from Avalarafor each summary entry. That sometimes yields technical labels likeRI STATE TAX, but it avoids ambiguous duplicates and keeps MW and SOU consistent.
4. Edge cases and tricky behaviors
Once the basic flow worked, we spent considerable time on edge cases and subtle behaviors.
Products without an Avalara tax code
Not every product in the catalog has a recycling fee or a defined Avalara tax code. During QA, we noticed that if a cart only contained productswithoutanavalara.taxcode, Avalaras summary would not include any recycling fee line at all.
We updated the extension logic to handle three scenarios:
No Avalara tax code on any item:show only the standard tax components (for example, sales tax, shipping tax, total tax).
Mixed cart:show taxes for all items, plus a recycling fee line when at least one product has a tax code that triggers it.
Only recycling-fee-eligible products:show sales tax, recycling fee, and total tax.
States without recycling fees
We tested the integration against multiple addresses:
Rhode Island (RI) and Connecticut (CT) where recycling fees apply.
California (CA) and Oregon (OR) where, in our configuration, they do not.
For CA and OR, the recycling fee line should not appear. The extension behaves accordingly, even if the same products carry an Avalara tax code that could trigger recycling fees in other states.
Shopifys Estimated taxes and timing quirks
While testing, we noticed that Shopifys nativeEstimated taxesline sometimes appeared to disappear or update only after certain actions (for example, toggling a checkbox or moving to the next checkout step).
We confirmed that this behavior also occurred on environmentswithoutour extension installed, so it is part of Shopifys own client-side update logic, not something introduced by our code. The extension itself does not alter Shopifys tax calculation or its built-in display; it only adds a new explanatory block.
Reconciling Avalara total tax with Shopifys Estimated taxes
During QA, we also found that ShopifysEstimated taxesamount did not always match Avalarastotal taxfield exactly. After investigation, we observed a consistent pattern:
Estimated taxesas shown by Shopify often equaled
Avalara total tax + (shipping cost × state tax rate).
Once we understood that relationship, we were able to explain discrepancies in QA and to stakeholders. Our breakdown still displays theraw Avalara tax total, while Shopify continues to show its own composite figure inEstimated taxes.
5. Rolling the pattern out to a second brand (SOU)
After the Mattress Warehouse implementation proved successful, the client decided to use the same approach for their Sleep Outfitters brand.
Separate middleware, shared Avalara configuration
One design question was whether SOU should call the existing MW middleware endpoint or have its own. For maintainability and clear boundaries between brands, we chose to:
Create aSOU-specificmiddleware endpoint (for example,
https://sou-omni.example.com/api/avalara-tax), andReuse the same underlying Avalara account and
companyCodevalue (MAT), as instructed by the clients team.
This preserved a clean separation of codebases and deployment pipelines while leveraging the same tax engine and credentials behind the scenes.
Mirroring the checkout extension
On the Shopify side, we created a SOU version of theavalara-tax-breakdownextension, adjusted naming and configuration for the SOU storefront, and executed a similar round of QA:
Test carts with and without Avalara tax codes.
Multiple addresses (RI, CT, CA, OR).
Verification that recycling fees appear only when expected.
Consistency of labels and totals with the MW implementation.
The result is that both brands now share aconsistent, legally compliant, and transparent tax displayin checkout.
Key takeaways and lessons learned
Shopify + Avalara can do more than the default integration suggests. By combining AvaTax APIs, metafields, middleware, and Checkout UI extensions, you can expose very detailed, jurisdiction-specific tax information during checkout.
Metafields are a powerful bridge to external systems. Ingesting the STORIS
TaxClassIDinto a variant metafield (avalara.taxcode) gave Avalara the product-level context it needed without changing core product schemas.A middleware layer is essential. It solves CORS, secures credentials, centralizes configuration, and lets you reuse the same AvaTax integration across multiple storefronts and brands.
UI details matter for trust and compliance. Showing the full breakdown (sales tax, shipping tax, recycling fee, total tax) and clear labels from Avalara (
taxName) greatly improves transparency for both customers and auditors.Expect subtle differences between platforms. Shopify’s Estimated taxes line is not a simple mirror of Avalara’s total tax, especially when shipping is involved. Understanding and documenting those differences is key for successful QA and stakeholder sign-off.
For teams considering a similar integration, the main pattern is reusable: get the right tax attributes onto your products, proxy secure API calls through middleware, and use Checkout UI extensions to tell a clear tax story to your customers.
