Avalara
Sales tax calculation with Avalara AvaTax
Avalara
Connect to Avalara AvaTax to calculate sales tax inside a workflow — typically right before the step that charges the customer.
Prerequisites
- An Avalara AvaTax account
- Your Account ID and a License Key (Avalara admin → Settings → License and API keys)
- The company code of the AvaTax company the transactions belong to
Authentication
Avalara uses static Basic Auth — no OAuth flow. Connect it from Integrations → Tax & Compliance → Avalara and provide:
| Field | Required | Notes |
|---|---|---|
| Environment | yes | Production or Sandbox. Determines which Avalara host every request goes to. |
| Account ID | yes | Combined with the License Key into a single encrypted credential. |
| License Key | yes | Stored encrypted; never displayed again. |
| Company Code | no | Default company code for steps that do not set their own. |
Requests go to https://rest.avatax.com (production) or
https://sandbox-rest.avatax.com (sandbox). Those two hosts are fixed — a
workflow cannot point the integration at any other address.
To rotate a license key, open Manage, then use the connection's ⋯ actions menu and choose Update API key. That repairs the existing connection, so workflow steps that reference it keep working.
The rotate dialog comes up with Account ID empty, and that is deliberate:
the account ID is combined with the license key into one encrypted credential
(accountId:licenseKey) and is never stored in full on its own, so it cannot be
prefilled. The Integrations list stores and shows only the account ID's last
four characters alongside the Production/Sandbox ledger. Re-enter both halves. The
Environment and Company Code fields ARE prefilled from the stored
connection — check the environment before submitting, because it decides which
ledger your documents are filed against.
Available Actions
createTransaction
Creates an AvaTax transaction and returns the calculated tax. By default the
document is created uncommitted (SalesInvoice with commit off), so it is
recorded and auditable without being finalized for filing.
Inputs
| Field | Required | Description |
|---|---|---|
documentCode | yes | Stable identifier for the transaction. The runtime checks for an existing matching document before creation — see Reusing a document code. |
customerCode | yes | Your identifier for the customer. |
lines | yes | [{ "number": "1", "amount": 250, "quantity": 2, "itemCode": "WIDGET", "taxCode": "P0000000", "description": "Widget" }] |
shipFrom | yes | { "line1": "...", "city": "...", "region": "PA", "postalCode": "19103", "country": "US" } |
shipTo | yes | Same shape as shipFrom. |
companyCode | no | Falls back to the code stored on the connection. |
documentType | no | SalesInvoice (default), SalesOrder for a calculation that persists nothing, or ReturnInvoice for a credit (see Return invoices). |
referenceCode | no | AvaTax referenceCode. On a ReturnInvoice, the code of the invoice it credits. |
documentDate | yes | YYYY-MM-DD; must be a real calendar date. |
currencyCode | no | Omit it for USD. A value that is present must be a 3-letter ISO code; a blank or malformed one fails the step rather than being filed as USD. |
discount | no | Reserved. A non-zero document discount currently fails before AvaTax is called. |
exemptionNo, entityUseCode | no | Customer exemption details. |
commit | no | Defaults to false. Turn on only when the document is final. |
taxOverride | no | { "type": "TaxAmount", "taxAmount": 16.5, "reason": "Tax charged on invoice INV-0001" }. Files a tax amount decided outside AvaTax — typically what the customer was already charged — instead of AvaTax's own calculation; AvaTax distributes it across the lines. taxAmount is in currency units (16.50 means $16.50), not cents, with at most two decimals, and may not exceed the document's total line amount — a Stripe amount in cents bound here fails. 0 is filed as zero. Only TaxAmount is accepted, reason is required, and any other field is refused. Omit the field for no override: a value that is present but blank (a template that resolved to nothing) fails the step instead of quietly filing AvaTax's own figure. |
Outputs
transactionId, code, status, documentType, taxDate, currencyCode,
totalAmount, totalTaxable, totalExempt, totalDiscount, totalTax,
totalTaxCalculated, grandTotal, lineCount, linesTruncated, lines[], summaryCount,
summaryTruncated, summary[],
messages[].
lines[] is capped at 500 rows so a consolidated invoice cannot persist an
unbounded array into every checkpoint and the run record. When the cap applies,
linesTruncated is true and lineCount holds the number Avalara actually
returned.
Each line also includes jurisdiction details[] with rate, taxableAmount,
and tax. Details are capped at 32 per line; check detailsCount and
detailsTruncated before using them to calculate a combined rate. A missing
numeric value is null.
summary[] is normalized to country, region, jurisType, jurisName,
taxType, rate, taxable, and tax, and capped at 100 rows.
summaryCount and summaryTruncated make that cap explicit.
If Avalara's response is missing totalAmount or totalTax, the step fails
— it does not report a zero-dollar grandTotal that a payment step would then
charge. The totals that do not feed the charge (totalTaxable, totalExempt,
totalDiscount) and the per-line numbers come back as null when Avalara omits
them, so "unknown" is never presented as "zero".
With a taxOverride, totalTax is the amount that was filed and
totalTaxCalculated is what AvaTax would have charged on its own; comparing the
two is how you see a rate or rounding difference. The step fails if AvaTax
reports a totalTax more than a cent away from the override (the override was
not applied) or omits totalTaxCalculated while an override was filed — both
after AvaTax has answered, so verify the document in Avalara and void it before
filing again. Without an override, totalTaxCalculated is null when Avalara
omits it.
grandTotal is totalAmount + totalTax — the tax-inclusive figure to map into
the payment step. Discount-bearing requests are refused until AvaTax's total
semantics are verified. It is in currency units (270.63 means $270.63), not
cents — see Using tax in a later step for the
conversion required before charging through Stripe.
Return invoices
A ReturnInvoice records a credit, for example a Stripe credit note against an
earlier invoice. It is the only document type that takes a negative
taxOverride.taxAmount; on SalesInvoice and SalesOrder the override must
be zero or more. On a return:
- give each line the credited amount as a negative number (
-50credits $50), with at most two decimals. The lines must total a negative amount; - set
taxOverride.taxAmountto the credited tax, zero or negative. It may not exceed the line total in size (compared in whole cents); - set
referenceCodeto the code of the invoice being credited, anddocumentCodeto the credit's own number.
{
"action": "avalara.createTransaction",
"config": {
"documentCode": "CN-0001",
"documentType": "ReturnInvoice",
"referenceCode": "INV-1001",
"documentDate": "2026-10-02",
"customerCode": "CUST-77",
"lines": [{ "number": "1", "amount": -50, "quantity": 1, "itemCode": "WIDGET" }],
"taxOverride": { "type": "TaxAmount", "taxAmount": -4.12, "reason": "Tax credited on credit note CN-0001" },
"commit": true
}
}shipFrom and shipTo are required as on any other document.
voidTransaction
Voids a document AvaTax already holds. A re-billed quote voids its Stripe invoice and issues a new one under a new number; without this step the AvaTax document committed against the old number stays committed and over-reports tax for filing.
Inputs
| Field | Required | Description |
|---|---|---|
documentCode | yes | The document to void — the same code the createTransaction step used. |
companyCode | no | Falls back to the code stored on the connection. Must be the company the document was filed under. |
documentType | no | SalesInvoice (default) or SalesOrder. AvaTax needs it when more than one document shares a code. |
voidReasonCode | no | AvaTax VoidReasonCode. Defaults to DocVoided, which voids the document and sets its status to Cancelled. DocDeleted, PostFailed, AdjustmentCancelled, and Unspecified are also accepted. AvaTax refuses a void that carries no reason, so a blank value fails the step rather than being defaulted. |
A company or document code containing a space, /, +, ?, %, or # is
refused before the request: AvaTax requires its own escaping for those
characters, so percent-encoding them would address a different document. A code
whose path segment is . or .. is refused for a different reason — those
segments are removed when the URL is resolved, which would point the void at
another document. Dots inside a code are fine: INV.2026.01 voids normally.
Outputs
transactionId, code, status, documentType.
The full AvaTax TransactionModel is deliberately not returned — it
restates the customer's address and line amounts, and step outputs persist for
the whole run. status is the document's post-void status (Cancelled after a
successful DocVoided).
code and status are the values AvaTax confirmed, not the ones the step
submitted. The step fails rather than reporting a void when the response
carries no document code, carries a code for a different document, carries no
status, or carries a status that does not mean the document was cancelled — a
2xx alone is not evidence the void happened.
Transactions already reported to a tax authority by Avalara Managed Returns cannot be voided. That comes back as a provider error on the step.
Using tax in a later step
Place the Avalara step before the payment step and bind the tax-inclusive amount into the charge:
{{ steps.<avalara-step-id>.grandTotal | multiply: 100 }}Convert units — never bind grandTotal on its own.
Avalara returns grandTotal in currency units (270.63 means $270.63).
Stripe's amount field expects the smallest currency unit (cents) and
passes whatever value it receives straight through to the Stripe API with no
conversion of its own. Binding the bare
{{ steps.<avalara-step-id>.grandTotal }} — without the filter — charges
270.63 cents ($2.71) instead of $270.63: a 100x undercharge.
The | multiply: 100 filter shown above performs both the conversion and the
rounding (Math.round(grandTotal * 100)). Always include it whenever you map
an Avalara total into a Stripe (or any other cents-denominated) amount field.
If Avalara cannot calculate the tax — bad credentials, an unserviceable address, a timeout — the step fails. Under the default error handling that ends the run, so the payment step never executes with an untaxed amount.
That protection depends on the step keeping the default error handling (Fail). If you set this step to Continue, the run proceeds and the payment step executes with an unresolved amount. Leave tax calculation on Fail.
Document date and filing periods
AvaTax picks both the rate schedule and the filing period from the document
date. documentDate is required because the workflow runtime does not carry a
tenant timezone; guessing from the worker's UTC date could move an evening
transaction in the Americas into the next filing period. The step returns the
effective date as taxDate.
Document-level discounts
Document-level discounts and line items with discounted: true are currently
refused before AvaTax is called. The integration will not expose a chargeable
grandTotal for a discounted request until a sandbox measurement establishes
whether AvaTax's totalAmount is gross or net of totalDiscount.
Reusing a document code
Before creating a transaction, the runtime looks up the same company code, document type, and document code. If a document already exists, the step refuses to submit another and tells you to inspect the existing document or use a new code.
That is measured, not assumed: the same workflow was run twice against the real
Avalara sandbox with a fixed documentCode and commit: false. Both runs
succeeded, both returned status: "Saved", and the two transaction IDs were
different. AvaTax neither rejected the duplicate code nor updated the first
document.
What is not established: whether the first document survives alongside the
second or is superseded by it (the integration exposes no lookup action, so
telling them apart needs the AvaTax UI or a direct API query), and what happens
with commit: true or with document types other than SalesInvoice — those
were not tested, and AvaTax may well behave differently for a committed
document.
This guard was added because sandbox evidence showed that direct replay without the lookup can create a second document with a new transaction id. Before re-running a workflow that may have reached this step:
- void the earlier document in Avalara, or
- use a new
documentCode.
Derive documentCode from something stable and unique per invoice so you can
find the earlier document when you need to void it.
If AvaTax does reject a duplicate — which it may for a committed document — the step fails with a named error rather than an opaque provider 400:
An Avalara transaction already exists for document code "INV-1001" and type "SalesInvoice". Use a new document code, or void/adjust the existing transaction in Avalara.
The create call itself is never automatically retried after a 429 or transport failure because AvaTax may have persisted it before the failure became visible. Verify the document code in AvaTax before a manual retry.
Limitations
- Only
createTransactionandvoidTransactionare available. Committing, retrieving, adjusting, unvoiding, and listing transactions are not yet exposed. - There are no Avalara webhook triggers.
- Filing, remittance, nexus, and exemption-certificate management are out of scope.
Loopfour