Loopfour
IntegrationsTax & Compliance

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:

FieldRequiredNotes
EnvironmentyesProduction or Sandbox. Determines which Avalara host every request goes to.
Account IDyesCombined with the License Key into a single encrypted credential.
License KeyyesStored encrypted; never displayed again.
Company CodenoDefault 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

FieldRequiredDescription
documentCodeyesStable identifier for the transaction. The runtime checks for an existing matching document before creation — see Reusing a document code.
customerCodeyesYour identifier for the customer.
linesyes[{ "number": "1", "amount": 250, "quantity": 2, "itemCode": "WIDGET", "taxCode": "P0000000", "description": "Widget" }]
shipFromyes{ "line1": "...", "city": "...", "region": "PA", "postalCode": "19103", "country": "US" }
shipToyesSame shape as shipFrom.
companyCodenoFalls back to the code stored on the connection.
documentTypenoSalesInvoice (default), SalesOrder for a calculation that persists nothing, or ReturnInvoice for a credit (see Return invoices).
referenceCodenoAvaTax referenceCode. On a ReturnInvoice, the code of the invoice it credits.
documentDateyesYYYY-MM-DD; must be a real calendar date.
currencyCodenoOmit 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.
discountnoReserved. A non-zero document discount currently fails before AvaTax is called.
exemptionNo, entityUseCodenoCustomer exemption details.
commitnoDefaults to false. Turn on only when the document is final.
taxOverrideno{ "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 (-50 credits $50), with at most two decimals. The lines must total a negative amount;
  • set taxOverride.taxAmount to the credited tax, zero or negative. It may not exceed the line total in size (compared in whole cents);
  • set referenceCode to the code of the invoice being credited, and documentCode to 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

FieldRequiredDescription
documentCodeyesThe document to void — the same code the createTransaction step used.
companyCodenoFalls back to the code stored on the connection. Must be the company the document was filed under.
documentTypenoSalesInvoice (default) or SalesOrder. AvaTax needs it when more than one document shares a code.
voidReasonCodenoAvaTax 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 createTransaction and voidTransaction are 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.

On this page