This integration playbook covers the three dominant commercial tax engines and how they connect to major ERP platforms (SAP, Oracle, Microsoft Dynamics 365, NetSuite). All three engines follow the same fundamental pattern: intercept the tax determination point in the ERP transaction lifecycle, call the external engine, and write back calculated tax amounts.
| System | Role | API Surface | Direction |
|---|---|---|---|
| ERP (SAP, Oracle, D365, NetSuite) | Source of truth for orders, invoices, AP/AR | Varies by ERP | Outbound |
| Tax Engine (Avalara / Vertex / ONESOURCE) | Tax calculation, rate lookup, exemption validation | REST / SOAP | Inbound |
| Exemption Certificate Manager | Stores and validates tax-exempt customer certificates | REST | Bidirectional |
| Tax Filing Module | Prepares and submits tax returns from calculated data | Batch / REST | Inbound |
| Tax Engine | Protocol | Calc Endpoint | Auth Method | Batch | Exemption Mgmt | Filing | Global VAT/GST |
|---|---|---|---|---|---|---|---|
| Avalara AvaTax v2 | REST/JSON | POST /api/v2/transactions/create | Basic Auth / OAuth 2.0 | Yes | CertCapture | Avalara Returns | 175+ countries |
| Vertex O Series v2 | REST/JSON, SOAP, RFC | POST /vertex-ws/v2/supplies | OAuth 2.0 (VERX IDP) | Yes | Vertex ECM | Vertex Returns | Global |
| ONESOURCE Determination | REST/JSON, SOAP | Vendor-specific | WS-Security, REST tokens | Yes | ONESOURCE Cert Mgr | ONESOURCE Compliance | 200+ jurisdictions |
| Limit Type | Avalara AvaTax | Vertex O Series | ONESOURCE | Notes |
|---|---|---|---|---|
| Max line items per transaction | 15,000 | 10,000+ | 10,000+ | Split large orders if exceeded |
| Max request payload | 10 MB | Varies | Varies | Rarely hit in practice |
| Target response time | <200ms typical | <100ms cached, <300ms cold | <200ms typical | Depends on address complexity |
| Concurrent requests | Account-dependent | Deployment-dependent | License-dependent | Contact vendor for limits |
| Limit Type | Avalara AvaTax | Vertex O Series | ONESOURCE | Notes |
|---|---|---|---|---|
| Transactions per second | Plan-dependent (429 throttle) | Scales with deployment | Enterprise SLA-based | Avalara enforces HTTP 429 |
| Daily transaction cap | No hard cap (plan-based) | No hard cap | No hard cap | All scale with licensing tier |
| Batch processing | Dedicated batch endpoint | Async job submission | Batch XML submission | Use for month-end invoice runs |
| Address validation | Separate rate limit | Included in tax calc | Separate service | Avalara: resolve before or during calc |
| Tax Engine | Method | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| Avalara AvaTax | HTTP Basic Auth | N/A (per-request) | N/A | Simplest; use for server-to-server |
| Avalara AvaTax | OAuth 2.0 | Configurable | Yes | For delegated access scenarios |
| Vertex O Series (Cloud) | OAuth 2.0 Client Credentials | Token-based | Re-request | Must use VERX IDP (Legacy deprecated Nov 2025) |
| Vertex O Series (SAP) | RFC connection | Session-based | N/A | Configured via SAP SM59 |
| ONESOURCE | SOAP WS-Security / REST Bearer | Session | Yes | Varies by deployment type |
START — User needs to integrate ERP with a tax engine
|
+-- Which ERP?
| +-- SAP S/4HANA or ECC
| | +-- Avalara: BTP connector (clean core)
| | +-- Vertex: SIC + Accelerator via RFC (deepest)
| | +-- ONESOURCE: SAP Integration Framework (certified)
| +-- Oracle ERP Cloud -> Oracle Tax Partner connectors
| +-- Microsoft D365 Finance -> Native connectors via AppSource
| +-- NetSuite -> SuiteApp / SuiteTalk connectors
| +-- Custom / Other -> REST API directly
|
+-- Transaction type?
| +-- Sales (O2C) -> Real-time on order save, batch on invoicing
| +-- Purchases (P2P) -> On PO receipt for use tax accrual
| +-- Both -> Configure both determination points
|
+-- Volume?
| +-- < 10K txns/day -> Standard real-time API
| +-- 10K-100K/day -> Connection pooling + caching
| +-- > 100K/day -> Batch API + async processing
|
+-- Global scope?
+-- US only -> Any engine works; Avalara broadest US coverage
+-- US + EU/APAC -> Vertex or ONESOURCE (deeper global rules)
+-- 50+ countries -> ONESOURCE (200+ jurisdictions) or Vertex
| Step | Source | Action | Target | Data Objects | Failure Handling |
|---|---|---|---|---|---|
| 1 | ERP | Create/save sales order | Tax Engine | Line items, addresses, customer ID, tax codes | Fail open: estimated tax, flag for recalc |
| 2 | Tax Engine | Calculate tax by jurisdiction | ERP | Tax amounts per line, jurisdiction breakdown | Return cached rate if unreachable |
| 3 | ERP | Write tax to order/invoice lines | ERP DB | Tax line items, GL distribution | Retry 3x, then manual review |
| 4 | ERP | Invoice posted | Tax Engine | Transaction commit | Queue for retry |
| 5 | Tax Engine | Aggregate committed transactions | Filing Module | Period totals by jurisdiction | Reconcile before filing deadline |
| 6 | Filing Module | Generate and submit returns | Tax Authority | Returns, payments | Human review before submission |
| ERP Tax Code | Avalara Code | Vertex Code | ONESOURCE Code | Description |
|---|---|---|---|---|
| TAXABLE (default) | P0000000 | General Tangible | TANGIBLE_GOOD | Tangible personal property |
| SOFTWARE_SAAS | SW054000 | Software SaaS | SOFTWARE_SAAS | SaaS subscriptions |
| DIGITAL_GOODS | D9999999 | Digital Goods | DIGITAL | Digital products |
| SERVICES | S0000000 | Service General | SERVICE | Services (taxability varies by state) |
| FOOD_GROCERY | PF050100 | Food/Grocery | FOOD_UNPREPARED | Unprepared food |
| CLOTHING | PC040100 | Clothing General | CLOTHING | Clothing (exempt in some states) |
| MEDICAL_DEVICE | PM060100 | Medical Equipment | MEDICAL | Medical devices |
| FREIGHT | FR010000 | Freight/Shipping | FREIGHT | Shipping charges |
Set up a sandbox environment with your chosen tax engine. Configure company profile, nexus jurisdictions, and base tax rules. [src1, src3]
Verify: Log into sandbox admin portal and confirm company code, nexus states, and base configuration are visible.
Export your ERP's item master. Map each product category to the tax engine's classification system. [src2]
# Avalara: List available tax codes
curl -X GET "https://sandbox-rest.avatax.com/api/v2/definitions/taxcodes?$top=50" \
-H "Authorization: Basic $(echo -n 'ACCOUNT_ID:LICENSE_KEY' | base64)" \
-H "Content-Type: application/json"
Verify: GET /api/v2/definitions/taxcodes?$filter=taxCode eq 'SW054000' returns the SaaS tax code.
Integrate the engine's address validation as an upstream step or include it in the tax calculation request. [src1, src6]
# Avalara: Resolve/validate an address
curl -X POST "https://sandbox-rest.avatax.com/api/v2/addresses/resolve" \
-H "Authorization: Basic $(echo -n 'ACCOUNT_ID:LICENSE_KEY' | base64)" \
-H "Content-Type: application/json" \
-d '{"line1":"255 S King St","city":"Seattle","region":"WA","postalCode":"98104","country":"US"}'
Verify: Response includes validatedAddresses array with standardized address.
Core integration point: when a sales order or invoice is saved in the ERP, intercept the save event and call the tax engine. [src1, src3]
# Avalara: Create a tax calculation transaction
curl -X POST "https://sandbox-rest.avatax.com/api/v2/transactions/create" \
-H "Authorization: Basic $(echo -n 'ACCOUNT_ID:LICENSE_KEY' | base64)" \
-H "Content-Type: application/json" \
-d '{
"type":"SalesInvoice","companyCode":"DEFAULT","date":"2026-03-03",
"customerCode":"CUST-001",
"addresses":{"shipFrom":{"line1":"255 S King St","city":"Seattle","region":"WA","postalCode":"98104","country":"US"},
"shipTo":{"line1":"1 Market St","city":"San Francisco","region":"CA","postalCode":"94105","country":"US"}},
"lines":[{"number":"1","quantity":1,"amount":999.99,"taxCode":"SW054000","description":"SaaS subscription"},
{"number":"2","quantity":5,"amount":49.99,"taxCode":"P0000000","description":"USB cables"}],
"commit":false}'
Verify: Response includes totalTax field and per-line taxCalculated amounts.
Write jurisdiction-level tax amounts back to ERP lines. When invoice is finalized, commit to the tax engine. [src1]
# Avalara: Commit a transaction (marks it as final for filing)
curl -X POST "https://sandbox-rest.avatax.com/api/v2/companies/DEFAULT/transactions/INV-001/commit" \
-H "Authorization: Basic $(echo -n 'ACCOUNT_ID:LICENSE_KEY' | base64)" \
-H "Content-Type: application/json" \
-d '{"commit":true}'
Verify: GET /transactions/INV-001 shows status: "Committed".
Load existing certificates into the engine's cert manager. Configure ERP to pass customer exemption status. [src1, src2]
Verify: Test transaction with exempt customer returns $0 tax for exempt categories.
# Input: Order line items, ship-to/ship-from addresses, customer code
# Output: Per-line tax amounts, jurisdiction breakdown, total tax
import requests # requests==2.31.0
from base64 import b64encode
AVATAX_BASE = "https://sandbox-rest.avatax.com"
ACCOUNT_ID = "YOUR_ACCOUNT_ID"
LICENSE_KEY = "YOUR_LICENSE_KEY"
auth_header = b64encode(f"{ACCOUNT_ID}:{LICENSE_KEY}".encode()).decode()
def calculate_tax(order_lines, ship_from, ship_to, customer_code, doc_date):
payload = {
"type": "SalesOrder",
"companyCode": "DEFAULT",
"date": doc_date,
"customerCode": customer_code,
"addresses": {"shipFrom": ship_from, "shipTo": ship_to},
"lines": order_lines,
"commit": False
}
resp = requests.post(
f"{AVATAX_BASE}/api/v2/transactions/create",
json=payload,
headers={"Authorization": f"Basic {auth_header}", "Content-Type": "application/json"},
timeout=5
)
resp.raise_for_status()
result = resp.json()
return {
"total_tax": result["totalTax"],
"lines": [{"line": ln["lineNumber"], "tax": ln["taxCalculated"]} for ln in result["lines"]],
"jurisdictions": [{"name": s["jurisName"], "tax": s["tax"]} for s in result["summary"]]
}
// Input: Transaction with line items and addresses
// Output: Tax calculation response with jurisdiction detail
const axios = require('axios'); // [email protected]
const VERTEX_AUTH = 'https://auth.vertexsmb.com/identity/connect/token';
const VERTEX_CALC = 'https://calcconnect.vertexsmb.com/vertex-ws/v2/supplies';
async function getToken(clientId, clientSecret) {
const resp = await axios.post(VERTEX_AUTH, new URLSearchParams({
grant_type: 'client_credentials', client_id: clientId,
client_secret: clientSecret, scope: 'tax-calculation'
}), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
return resp.data.access_token;
}
async function calcTax(token, transaction) {
const resp = await axios.post(VERTEX_CALC, transaction, {
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
timeout: 5000
});
return resp.data;
}
# Test Avalara authentication
curl -s -o /dev/null -w "%{http_code}" \
"https://sandbox-rest.avatax.com/api/v2/utilities/ping" \
-H "Authorization: Basic $(echo -n 'ACCT:KEY' | base64)"
# Expected: 200
# Simple tax calculation (NYC)
curl -X POST "https://sandbox-rest.avatax.com/api/v2/transactions/create" \
-H "Authorization: Basic $(echo -n 'ACCT:KEY' | base64)" \
-H "Content-Type: application/json" \
-d '{"type":"SalesOrder","companyCode":"DEFAULT","date":"2026-03-03",
"customerCode":"TEST","addresses":{"singleLocation":{"line1":"100 Broadway",
"city":"New York","region":"NY","postalCode":"10005","country":"US"}},
"lines":[{"number":"1","quantity":1,"amount":100,"taxCode":"P0000000"}]}'
# Expected: totalTax ~8.875 (NYC combined rate)
| ERP Field | Avalara Field | Vertex Field | Type | Transform | Gotcha |
|---|---|---|---|---|---|
| Ship-to address | addresses.shipTo | destination | Address | Normalize to ISO format | Avalara requires country code |
| Ship-from address | addresses.shipFrom | origin | Address | Include warehouse/origin | Missing = defaults to company HQ |
| Product tax code | lines[].taxCode | product.productClass | String | Map ERP to engine code | Unmapped = fully taxable |
| Customer exempt status | customerCode + exemptionNo | customer.classCode | String + Code | Lookup in cert manager | Certificate must exist first |
| Line amount | lines[].amount | lineItem.extendedPrice | Decimal | Post-discount amount | Avalara default: post-discount |
| Transaction date | date | documentDate | YYYY-MM-DD | Convert from ERP format | Date determines applicable rates |
| Document type | type | transactionType | Enum | Map ERP doc type | SalesOrder = quote; SalesInvoice = final |
| Currency | currencyCode | currency.isoCurrencyCodeAlpha | ISO 4217 | Match invoice currency | Convert in ERP, not engine |
| Code | Avalara | Vertex | Cause | Resolution |
|---|---|---|---|---|
| 429 | Rate limit exceeded | N/A (uses 503) | Too many API calls | Exponential backoff: 2^n seconds, max 5 retries |
| 401 | AuthenticationIncomplete | Unauthorized | Invalid/expired credentials | Verify keys; refresh OAuth token |
| 400 | GetTaxError / InvalidAddress | Invalid Request | Bad address, missing fields | Validate addresses before calc |
| 409 | DocumentAlreadyCommitted | Conflict | Modifying committed transaction | Void original, create adjusted |
| 503 | ServiceUnavailable | ServiceUnavailable | Maintenance or overload | Fail open: cached rates, queue recalc |
| 404 | EntityNotFoundError | NotFound | Invalid company/transaction ID | Verify company code config |
Circuit breaker — after 3 timeouts, fall back to cached rates for 60s. [src8]Monitor certificate expiry; set alerts 30 days before; quarterly re-validation. [src1]Use ISO 3166 codes; pre-validate via address resolution API. [src6]Require tax code mapping in product creation workflow; weekly reconciliation. [src2]Pre-commit validation verifying tax document exists. [src1]Maintain mapping table; validate during integration testing per entity. [src8]# BAD — fires tax calc on every line change, burns API quota
def on_line_change(order):
for line in order.lines:
tax = avalara_calc(order, [line]) # N calls for N lines
line.tax = tax
# GOOD — single API call with all lines, on save/submit
def on_order_save(order):
result = avalara_calc(order, order.lines) # 1 call for N lines
for line, tax_line in zip(order.lines, result.lines):
line.tax = tax_line.tax_calculated
# BAD — hardcoded rates go stale immediately
RATES = {"CA": 0.0725, "NY": 0.08, "TX": 0.0625}
def get_tax(state, amount):
return amount * RATES.get(state, 0.0)
# GOOD — cache real engine responses with TTL
import redis # redis==5.0.0
cache = redis.Redis()
def get_cached_rate(key):
cached = cache.get(f"tax_rate:{key}")
return float(cached) if cached else None
def cache_rate(key, rate):
cache.setex(f"tax_rate:{key}", 86400, str(rate)) # 24h TTL
# BAD — double taxation
payload = {"amount": 107.25} # Price + tax already included
# GOOD — engine calculates tax on net amount
payload = {"amount": 100.00} # Net price only
Environment variables with distinct names; startup validation. [src1]Post-invoice hook to commit; daily reconciliation. [src1]Entity/use codes per line item, not per transaction. [src2]Implement void/return API call on credit memo. [src1, src8]Include year-boundary date testing in integration suite. [src6]Country-specific fields; validate per ISO 3166-1. [src6]# Avalara: Test authentication
curl -s "https://rest.avatax.com/api/v2/utilities/ping" \
-H "Authorization: Basic $(echo -n 'ACCT_ID:LICENSE_KEY' | base64)" | jq .
# Avalara: Check account subscriptions
curl -s "https://rest.avatax.com/api/v2/accounts/ACCT_ID/subscriptions" \
-H "Authorization: Basic $(echo -n 'ACCT_ID:LICENSE_KEY' | base64)" | jq .
# Avalara: Verify a tax code exists
curl -s "https://rest.avatax.com/api/v2/definitions/taxcodes?\$filter=taxCode%20eq%20'SW054000'" \
-H "Authorization: Basic $(echo -n 'ACCT_ID:LICENSE_KEY' | base64)" | jq .
# Avalara: List configured companies
curl -s "https://rest.avatax.com/api/v2/companies" \
-H "Authorization: Basic $(echo -n 'ACCT_ID:LICENSE_KEY' | base64)" | jq '.value[].companyCode'
# Vertex: Get OAuth token
curl -X POST "https://auth.vertexsmb.com/identity/connect/token" \
-d "grant_type=client_credentials&client_id=ID&client_secret=SECRET&scope=tax-calculation"
# Vertex: Test tax calculation
curl -X POST "https://calcconnect.vertexsmb.com/vertex-ws/v2/supplies" \
-H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" \
-d '{"saleMessageType":"INVOICE","lineItems":[{"product":{"productClass":"tangible"},"quantity":1,"extendedPrice":100}]}'
| Engine / API | Version | Release | Status | Breaking Changes | Migration Notes |
|---|---|---|---|---|---|
| Avalara AvaTax REST | v2 | 2016 (GA) | Current | None recent | Stable API; incremental additions |
| Avalara AvaTax REST | v1 | 2012 | Deprecated | N/A | Migrate to v2 |
| Vertex O Series REST | v2 | 2024 | Current | Path: sale → supplies | See v1-to-v2 conversion guide |
| Vertex O Series REST | v1 | 2020 | EOL Apr 30, 2026 | N/A | Must migrate to v2 |
| Vertex Legacy IDP | N/A | N/A | Deprecated Nov 2025 | Auth endpoint removed | Migrate to VERX IDP |
| ONESOURCE Determination | Current | Rolling | Current | SAP BTP added | Evaluate BTP connector |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Multi-state nexus (5+ states) | Single-state, simple tax table | ERP native tax table |
| Complex product taxability (SaaS, digital, services) | All products same tax category | ERP native tax code |
| Multi-country VAT/GST required | US-only, simple product mix | Avalara TaxRates API (free tier) |
| High volume needing automated filing | <100 transactions/month, simple | Manual filing or ERP native |
| B2B exemption certificate management | Pure B2C, no exempt customers | Skip ECM module |
| Audit defense documentation needed | No audit risk, simple profile | ERP native tax reports |
| Capability | Avalara AvaTax | Vertex O Series | ONESOURCE | Notes |
|---|---|---|---|---|
| API Style | REST v2 (JSON) | REST v2, SOAP, RFC | REST, SOAP | Avalara simplest; Vertex most options |
| Authentication | Basic Auth / OAuth | OAuth 2.0 (VERX IDP) | WS-Security, REST tokens | Avalara easiest quick integration |
| US Coverage | 12,000+ jurisdictions | 12,000+ jurisdictions | 12,000+ jurisdictions | All three comprehensive |
| Global Coverage | 175+ countries | Global (Brazil dedicated) | 200+ jurisdictions | ONESOURCE wins 50+ country |
| SAP Integration | BTP connector | SIC + Accelerator (deepest) | SAP Integration Framework | Vertex deepest SAP history |
| Cert Management | CertCapture | Vertex ECM | ONESOURCE Cert Mgr | CertCapture most adopted |
| Filing/Returns | Avalara Returns | Vertex Returns | ONESOURCE Compliance | All three integrated |
| E-commerce Connectors | 1,400+ (broadest) | Major platforms | Major platforms | Avalara dominates SMB |
| Transaction Scale | 10B+/year | Enterprise-scale | 10B+/year | All handle enterprise volume |
| Pricing | Per-transaction + subscription | Enterprise license | Enterprise license | Avalara most transparent SMB |
| Implementation | 2-6 wks (simple) / 3-6 mo (enterprise) | 3-6 months | 3-6 months | Avalara fastest SMB/mid-market |
| Best For | SMB to enterprise, broadest connectors | Large enterprise, deep ERP | Global enterprise, 50+ countries | Choose based on ecosystem + scope |