Sage Intacct REST – Gotchas

Service ID: sage-intacct-rest

Increase ROI and transform your organization with the first and only preferred provider of the AICPA for accounting and financial management software for companies of any size.

⚠️

45 gotchas across 20 resources

These are connector-specific behaviors and limitations to be aware of when integrating.

Attachments3 gotchas

allattachmentsAll

At most one attachment is visible per record. Sage attaches files to a record through a single supporting document reference on the record itself, and each upload creates a new supporting document and repoints that reference at it. The previously attached file still exists in Sage under Company > Supporting Documents, but it is no longer linked to the record, so it does not appear here and cannot be fetched or downloaded through the attachment endpoints.

Uploading a second file to the same reference_id therefore replaces the first rather than adding to it, and returns 201 either way. If a record needs several documents, attach them outside the API for now, or keep them in one file.

This is a limitation of the integration, not of Sage: a Sage supporting document can hold several files, and they can be added one at a time. Lifting it is tracked work.

uploadattachmentsUpload

Uploading a second file to the same reference_id REPLACES the first rather than adding to it, and returns 201 either way. Sage links files to a record through a single supporting document reference; each upload creates a new supporting document and repoints that reference. The earlier file remains in Sage under Company > Supporting Documents but is no longer linked to the record, so it stops appearing in the attachment list and cannot be fetched or downloaded.

If a record needs several documents, combine them into one file. This is a limitation of the integration rather than of Sage — a Sage supporting document can hold several files — and lifting it is tracked work.

deleteattachmentsDelete

The id must belong to the record named by reference_type/reference_id. An id from a different record returns 404 rather than deleting it — attachments are scoped to the record they are attached to.

The delete removes the file itself, not just the link.

allbankFeedStatementsAll

transactions are only returned when fetching a single statement. Sage's query service cannot return child collections, so the list endpoint omits the array entirely — fetch the statement by id to get its transactions.

start_balance, end_balance and their credit/debit indicators are never populated: Sage's cash-management/bank-feed object does not carry statement balances.

addbankFeedStatementsAdd

source_transaction_id is only stored as Sage's txnId when it is entirely numeric — Sage rejects the whole feed otherwise, with an error that names no field. A non-numeric value is stored as the transaction's bank reference instead, and still reads back as source_transaction_id.

Sage derives the transaction direction from the sign of the amount, so credit_or_debit (falling back to transaction_type) is what decides it: debit is sent as a negative amount and comes back as a withdrawal. Sage ignores any explicit type on write.

Statement-level fields other than the period and the account are not supported: start_balance, end_balance and their credit/debit indicators have no equivalent on Sage's cash-management/bank-feed object, and end_date is ignored because a Sage feed carries a single feed date (start_date).

Sage requires at least one transaction — a statement with an empty transactions array is rejected. Sage does not deduplicate on source_transaction_id: reposting the same statement creates duplicates.

Bill Payments4 gotchas

allbillPaymentsAll

filter[bill_id] is a reverse lookup, not a plain field filter — Sage's payment object carries no bill reference itself, only its child accounts-payable/payment-detail rows do. Each request with this filter costs one extra batched query resolving the matching payment keys before the main list runs. Live-verified: a bill with a payment applied returns exactly that payment; a bill with none returns an empty list rather than every bill-payment on the connection.

addbillPaymentsAdd

Sage Intacct applies a bill payment to individual bill LINES, while unified allocations reference a bill. Apideck resolves the bill to its lines automatically, and applies the allocation to the FIRST line of each bill. For a bill split across GL accounts or departments that means the whole allocated amount lands on line 1 rather than being spread across lines.

An allocation whose bill cannot be resolved fails the request rather than posting a payment that applies to nothing.

addbillPaymentsAdd

Sage has one caller-settable identifier on an AP payment, and it is the payment number, so number is what gets sent — falling back to reference when number is absent. Either way the value reads back as number. reference on its own is never stored: Sage accepts the field it maps to and silently discards it, so reference always reads back as null on payments created through the API.

account (the bank or GL account the payment is drawn from) is required. It maps to Sage's financialEntity, and an AP payment submitted without it is refused by Sage's business-logic layer rather than by field validation — so the rejection arrives as a generic BL… code that does not name the cause (the same codes cover closed periods, invalid dates and similar). Read the specific reason from the error detail (description2) rather than the code alone. When routing a payment to a specific entity via x-apideck-company-id or subsidiary.id, the account must be one that is valid in that entity.

account.id must be the bank account's display_id, not the id that bank_accounts.list returns — verified live: account.id set to the list's numeric id (e.g. "1") is rejected (422 BL01001973 "The bank account '1' is invalid."), while the same account's display_id (e.g. "100_SVB") succeeds.

updatebillPaymentsUpdate

Only note and payment_method can be changed after a payment exists. Sage accepts a payment date and a document id on the update call but silently discards both, and rejects any attempt to change the payment number, so those fields are not sent. Amount, supplier, account and allocations are fixed at creation — delete and recreate the payment to change them.

Changing payment_method can make Sage post the payment immediately. If the bill and the paying bank account belong to different entities with no inter-entity relationship configured, that posting fails and the whole update is rejected with a GL balancing error.

Bills4 gotchas

allbillsAll

sub_total equals total and does not exclude tax. Sage Intacct's REST bill object exposes only document-level totals (totalTxnAmount, totalBaseAmount) with no pre-tax subtotal field, so the two unified fields are populated from the same source. For a bill with no tax this is correct; for a taxed bill sub_total will be the tax-inclusive amount rather than the net. Derive the net from the line items (line_items[].total_amount) when the distinction matters.

accounting_period is derived, not read from Sage. The REST bill object exposes no period field, so the returned MM-YY is computed from the posting date. That is the correct period for a calendar-aligned fiscal year; a company using custom fiscal periods may see a posting near a period boundary attributed to the adjacent month.

line_items and payment_allocations are each attached via one extra batched query per page, covering every bill on that page. Sage caps a query at 4000 rows. If the combined line count across a single page of bills exceeds 4000, the remaining bills on that page receive an empty (rather than incomplete) line_items. If the combined payment-allocation count exceeds 4000, the lookup is truncated and no bill on that page carries payment_allocations — the field is omitted rather than returned empty, so a truncated page is never mistaken for a page of unpaid bills. Reduce the page size (limit) if you have unusually large bills or bills with many applied payments.

addbillsAdd

line_items[].tax_rate, line_items[].unit_price and line_items[].type are not supported. Sage's underlying bill-line object is a flat GL-distribution line (account + amount only) — it has no per-line tax field (rejected outright as "not a valid field", unlike the header-level tax_inclusive, which is writable), no quantity/unit-price breakdown, and no line-type classification.

po_number is not supported on write or read. Sage exposes only a linked purchasing-document reference (a full PO transaction), not a plain PO-number string field; a value here previously read back from the header's reference field, which was a fabricated value, not the caller's own — that has been removed rather than fixed, since Sage has nothing to fix it to.

source_document_url is not supported. A caller-supplied value has no field to write to, and the read side previously returned Sage's own webURL (a link to view the record in Sage's own UI) in its place — a different thing from the caller's source document, and indistinguishable from a real value without checking the URL's domain. Removed rather than left silently misleading.

line_items[].location_id is required on every line, not optional, in a multi-entity tenant — omitting it fails with 422 REST-7001 / SL-0401 / SL-0592 "Location is empty. A valid Multi-entity location must be specified". There is no header-level location field or default; each line needs its own. This is a tenant-setup fact to surface up front, not a mapping bug — it also applies to expenses (verified live) and likely every other transaction type on a multi-entity tenant.

onebillsOne

payment_allocations is populated on both bills.get and bills.list via an extra batched query against accounts-payable/payment-detail, filtered by the bill's own key — this is a permanent added query on every bill read, not something only bills with payments pay for, since a bill with none still needs the lookup to know that. Live-verified: a bill with an applied payment returns payment_allocations: [{id: "<payment id>", allocated_amount, date}]; one with none returns payment_allocations: null.

updatebillsUpdate

line_items[].tax_rate, line_items[].unit_price and line_items[].type are not supported — Sage's bill-line object is a flat GL-distribution line with no per-line tax, quantity/unit-price, or type field. po_number is not supported either, on read or write — Sage exposes only a linked purchasing-document reference, not a plain PO-number string.

Companies1 gotcha

allcompaniesAll

companiesAll lists Sage entities (company-config/entity), which exist only in a multi-entity company. In a single-entity company the list is empty, and in a multi-entity one the top-level company itself is not an entity so it does not appear.

The x-apideck-company-id header has no effect on this connector. Entity scoping in Sage REST is per line, not per request: a transaction carries its entity on location, and the document header's own entity field is usually empty even on records written that way. The header is accepted and ignored rather than rejected, so a request sent with it returns unscoped, cross-entity data. Scope by line location_id instead.

This differs from the XML-based sage-intacct connector, which declares multi-company support and takes a required Default Entity setting that scopes every request. A customer migrating loses that request-level scoping with no error.

Credit Notes3 gotchas

allcreditNotesAll

sub_total equals total and does not exclude tax. Sage's accounts-receivable/adjustment object exposes only document-level totals with no pre-tax subtotal field, so both unified fields come from the same source. Derive the net from line_items[].total_amount when the distinction matters.

Amounts are SIGNED as Sage stores them, so a credit adjustment reports a negative total_amount while a debit adjustment on the same endpoint reports a positive one. Nothing else on the record distinguishes the two.

On create and update, line_items[].total_amount is accepted in either sign and always stored as a credit (negative) in Sage — this endpoint only ever creates credit adjustments, never debit ones. A positive amount is not rejected or misread; it is simply normalized.

addcreditNotesAdd

Line-level item is written as Sage's item dimension on the AR adjustment line, reaching parity with invoices: line_items[].item.id takes an Invoice Items id (Sage's internal key), with item.code as a fallback. It tags the line only — there is no inventory effect.

Line-level tax is not written, for the same reason as invoices: Sage captures per-line tax only under an Advanced Tax solution with multi-line tax enabled, and otherwise discards a tax entry silently despite returning 201. There is also no header-level tax field on Sage's accounts-receivable/adjustment object, so total_tax cannot be set.

sub_total equals total and does not exclude tax. Sage's object exposes only document-level totals with no pre-tax subtotal field, so both unified fields come from the same source. For an untaxed document that is correct; for a taxed one sub_total is the tax-inclusive amount rather than the net. Derive the net from line_items[].total_amount when the distinction matters.

allocations set on this resource have no effect — Sage has no way to apply a credit memo to an invoice from the credit memo record itself. To apply a credit memo, create a payment on the payments resource with two allocations: one of type invoice for the invoice being credited and one of type credit_memo for this credit memo, both carrying the amount being applied. The payment still requires an account (financial entity) even though no cash moves, since Sage requires one on every payment record.

updatecreditNotesUpdate

allocations set on this resource have no effect — Sage has no way to apply a credit memo to an invoice from the credit memo record itself. To apply a credit memo, create a payment on the payments resource with two allocations: one of type invoice for the invoice being credited and one of type credit_memo for this credit memo, both carrying the amount being applied. The payment still requires an account (financial entity) even though no cash moves, since Sage requires one on every payment record.

line_items[].total_amount is accepted in either sign and always stored as a credit (negative) in Sage — this endpoint only ever updates credit adjustments, never debit ones.

Customers1 gotcha

addcustomersAdd

Only active and inactive are applied. Sage's status field has no other members, so any other value from the unified enum is ignored and the record keeps its current status rather than the write being rejected.

Employees1 gotcha

addemployeesAdd

Only active and inactive are applied. Sage's status field has no other members, so any other value from the unified enum is ignored and the record keeps its current status rather than the write being rejected.

Expenses4 gotchas

allexpensesAll

supplier_id and supplier.id are read from Sage's payee field on the underlying credit-card transaction, which has no other vendor reference. This connector writes the real vendor key into payee on create/update, so an expense round-tripped through this connector returns a resolvable id — but payee is free text, and an expense created directly in Sage (or by another integration) can have it set to the vendor's display name instead. Verify the value against suppliers.list before using it as a lookup key.

addexpensesAdd

source_document_url has no destination on Sage's underlying credit-card transaction object — the API exposes no user-facing web link for this object at all (only internal API resource paths). The field is declared unsupported rather than mapped to anything.

line_items[].tax_rate is not supported on write. Sage accepts a taxDetail value on the line with no validation error, but it is silently discarded — verified live by creating a transaction with taxDetail.key set and reading it back as null. The correct write path for per-line tax on this object has not been found; treat this as an open gap, not a working feature.

reference and number are the same field downstream — Sage's credit-card transaction has one referenceNumber. Whichever you send is written to it and read back on both reference and number, so a round trip never loses the value but also cannot tell the two apart. When both are supplied, number wins (matching the bill-payments resource's existing precedence) — send only one if you need predictable behaviour.

account.id is Sage's credit-card account key — a different key space from the ids bank_accounts.list returns, and the two do not cross-reference by display_id. Verified live: an expense on card TestCCId reads back account: { id: "9", code: "TestCCId" }, while bank_accounts.list returns that same card as id: "6" and an unrelated savings account as id: "9". Sending a bank_accounts.list id fails with 422 "A credit card is required". Take the value from an existing expense's account.id, or resolve the card through the Proxy API.

oneexpensesOne

supplier_id and supplier.id are read from Sage's payee field on the underlying credit-card transaction, which has no other vendor reference. This connector writes the real vendor key into payee on create/update, so an expense round-tripped through this connector returns a resolvable id — but payee is free text, and an expense created directly in Sage (or by another integration) can have it set to the vendor's display name instead. Verify the value against suppliers.list before using it as a lookup key.

updateexpensesUpdate

source_document_url has no destination on Sage's underlying credit-card transaction object and is declared unsupported. line_items[].tax_rate is accepted with no validation error but silently discarded on write — verified live, treat as an open gap rather than a working feature. When both reference and number are supplied, number wins.

allinvoiceItemsAll

filter[type] accepts inventory, service and other, but the type field in the response can also be non_inventory — the unified filter enum is narrower than the response enum. Two consequences:

  • filter[type]=other matches every non-inventory Sage item type, so results can include items whose type reads non_inventory as well as other.
  • filter[type]=service always returns an empty list. Sage Intacct has no service item type, so there is nothing for it to match.

Invoices3 gotchas

allinvoicesAll

sub_total equals total and does not exclude tax. Sage's accounts-receivable/invoice object exposes only document-level totals with no pre-tax subtotal field, so both unified fields come from the same source. For an untaxed invoice that is correct; for a taxed one sub_total is the tax-inclusive amount rather than the net. Derive the net from line_items[].total_amount when the distinction matters.

total_tax is not populated: the object carries no header tax field, only a tax-solution reference. Tax exists solely as per-line entries.

addinvoicesAdd

Line-level item is written as Sage's item dimension, so line_items[].item.id must be an item id as returned by Invoice Items (Sage's internal key); item.code is accepted as a fallback and matches Sage's human-readable item id. This tags the line with the item — it does not create an order-entry document, so it has no inventory or fulfilment effect.

Line-level tax is not written. Sage only captures per-line tax under an Advanced Tax solution with multi-line tax enabled; where it is not, a tax entry on a line is either rejected outright ("You cannot use Tax solution '' to capture tax in this transaction") or accepted with a 201 and silently discarded. line_items[].tax_rate and tax_amount are therefore not sent.

Header-level total_tax is not supported: Sage's accounts-receivable/invoice object carries no tax field at all, only a tax-solution reference. Tax exists solely as per-line entries on this object.

sub_total equals total and does not exclude tax. Sage's object exposes only document-level totals with no pre-tax subtotal field, so both unified fields come from the same source. For an untaxed document that is correct; for a taxed one sub_total is the tax-inclusive amount rather than the net. Derive the net from line_items[].total_amount when the distinction matters.

updateinvoicesUpdate

Send line_items[].id for every line you want to UPDATE. Sage matches existing lines by that id; a line sent without one is added as a new line rather than replacing anything. So reading an invoice and posting its lines back without their ids doubles the invoice.

Lines you leave out of the request are kept as they are — a partial update touches only the lines it names, and does not delete the rest.

The same applies to bills, credit notes, journal entries and quotes.

alljournalEntriesAll

journal_symbol is required when creating a journal entry, and Sage Intacct reserves some journals for automated subledger posting. Posting to ARJ or APJ is rejected with "Smart rule JOURNAL_CHECK was violated - this journal is reserved for automated posting from a subledger". Use a general journal such as GJ instead, or enter the adjustment through the relevant subledger (invoices, bills).

Line items must balance and each line needs type (debit or credit) plus a ledger_account. In a multi-entity company each line also needs location_id.

accounting_period is derived, not read from Sage. The REST journal-entry object exposes no period field, so the returned MM-YY is computed from the posting date. That is the correct period for a calendar-aligned fiscal year; a company using custom fiscal periods (a 4-4-5 calendar, or periods that do not start on the 1st) may see a posting near a period boundary attributed to the adjacent month.

On write, accounting_period is not a substitute for a posting date — set posted_at. A full ISO date placed in accounting_period is still accepted as a fallback, but a period identifier such as 01-24 is ignored rather than posted as a date.

addjournalEntriesAdd

number cannot be set on create. Sage's general-ledger/journal-entry object has no writable number field — the number you read back is txnNumber, which Sage assigns on post. The only client-supplied identifier it accepts is a reference, exposed as source_id, so use that as the idempotency/recovery key and expect number to be server-assigned.

addledgerAccountsAdd

Only active and inactive are applied. Sage's status field has no other members, so any other value from the unified enum is ignored and the record keeps its current status rather than the write being rejected.

Payments1 gotcha

addpaymentsAdd

Sage Intacct applies a payment to individual invoice LINES, while unified allocations reference an invoice. Apideck resolves the invoice to its lines automatically, and applies the allocation to the FIRST line of each invoice. For a multi-line invoice that means the whole allocated amount lands on line 1 rather than being spread across lines.

An allocation whose invoice cannot be resolved fails the request rather than posting a payment that applies to nothing.

account.id must be the Sage financial entity id of the bank account (for example 100_SVB), not a unified bank-account id.

A credit memo is applied to an invoice through this endpoint, not through the credit-note resource: include one allocation of type invoice for the invoice being credited and one of type credit_memo for the credit memo funding it, both carrying the amount applied. account.id is still required even though no cash moves for a credit-only payment. At most one credit_memo allocation is supported per request; it is applied across every invoice allocation in the same request, so splitting one invoice's balance across more than one credit memo needs separate calls.

Projects1 gotcha

addprojectsAdd

Only active and inactive are applied on a write. Sage's own status field is an enable/disable flag with no other members, so a lifecycle value from the unified enum (in_progress, completed, on_hold, …) is ignored and the record keeps its current status rather than the write being rejected.

On READ, status reflects Sage's Project Status (for example in_progress), which is a different field. So a lifecycle status can be read but not written.

Quotes4 gotchas

allquotesAll

Sage Intacct has no distinct quote object: quotes are order-entry documents, and that same object also holds sales orders, invoices and credit memos. Apideck restricts the list to the document types your company defines as quote-class, so total_count reflects quotes only. Those type names are defined per company, so Apideck resolves them from your Order Entry transaction definitions on each request. If none can be resolved the list returns empty rather than returning every order-entry document.

addquotesAdd

Sage writes order-entry documents only against a named transaction definition (document::{definition}), and those definitions are created per company — there is no universal "quote" type. The connector resolves one for you, so no extra field is needed on the request:

  • If the company has exactly one active quote definition, it is used automatically.
  • If it has several, set the one you want as the Quote Transaction Definition (quote_document_type) setting on the connection. Without it the create is rejected with an error naming the definitions it found, rather than guessing — the definition determines the document's downstream workflow.

line_items[].item is a real order-entry item reference here (unlike on invoices, where Sage models the item as a line dimension). Sage requires a unit on every line, so send unit_of_measure — omitting it fails with "Missing unit Select a valid Unit in line no. N".

total_tax and line-level tax are not written; see the Invoices notes for why.

Creating quotes is not available on the sage-intacct (XML) connector.

updatequotesUpdate

Sage writes order-entry documents only against a named transaction definition (document::{definition}), and enforces it — addressing a quote through the wrong definition returns "could not be found" rather than touching another record. The connector reads the definition off the quote being written, so this needs no configuration and cannot be pointed at the wrong document. A quote id that does not resolve fails the request instead of falling back to a default.

deletequotesDelete

Sage writes order-entry documents only against a named transaction definition (document::{definition}), and enforces it — addressing a quote through the wrong definition returns "could not be found" rather than touching another record. The connector reads the definition off the quote being written, so this needs no configuration and cannot be pointed at the wrong document. A quote id that does not resolve fails the request instead of falling back to a default.

Refunds5 gotchas

allrefundsAll

A refund cannot be created on its own. Sage Intacct refunds something that already exists, so a create must reference a credit memo, customer advance, over-payment or negative invoice — a request without one is rejected with "No refund details were found in the request."

payment_method accepts only two values: eft and processInAP. The AP route additionally requires the customer to be linked to a vendor record, otherwise the create fails with "Cannot find a linked vendor for this customer."

number is only populated once Sage assigns a document number; a refund created without one reads back with number: null.

allrefundsAll

Customer refunds are off by default in Sage Intacct. Until the feature is enabled, this endpoint returns an empty list rather than an error, so an empty result does not mean the company has no refunds. Enable it at Accounts Receivable > Setup > Configuration in the Customer refunds section, then set the journal refunds post to and the document sequence for refund numbers. Only after the feature is on does a Customer Refunds permission subject appear, which must then be granted on the role of the user who authorized the connection. Searching the permissions report for "refund" before enabling the feature returns no results, which looks like a missing permission but is a disabled feature.

addrefundsAdd

Customer refunds must be enabled in Sage Intacct before this operation works, and the authorizing user's role needs the Customer Refunds permission. Without the permission Sage returns "The user does not have permission for POST operation on resource objects/accounts-receivable/customer-refund"; without the feature the permission subject does not exist to grant. Enable at Accounts Receivable > Setup > Configuration (Customer refunds section), choose the journal and document sequence, then grant the permission under Company > Admin > Roles. Refunds attach to a customer's existing credits, so the customer must have an unapplied credit, advance or overpayment to refund against.

onerefundsOne

Customer refunds are off by default in Sage Intacct. Until the feature is enabled, this endpoint returns an empty list rather than an error, so an empty result does not mean the company has no refunds. Enable it at Accounts Receivable > Setup > Configuration in the Customer refunds section, then set the journal refunds post to and the document sequence for refund numbers. Only after the feature is on does a Customer Refunds permission subject appear, which must then be granted on the role of the user who authorized the connection. Searching the permissions report for "refund" before enabling the feature returns no results, which looks like a missing permission but is a disabled feature.

updaterefundsUpdate

Customer refunds must be enabled in Sage Intacct before this operation works, and the authorizing user's role needs the Customer Refunds permission. Without the permission Sage rejects the request with "The user does not have permission for ... resource objects/accounts-receivable/customer-refund"; without the feature enabled the permission subject does not exist to grant in the first place. Enable at Accounts Receivable > Setup > Configuration (Customer refunds section), choose the journal and document sequence, then grant the permission under Company > Admin > Roles. Refunds attach to a customer's existing credits, so the customer must have an unapplied credit, advance or overpayment to refund against.

Subsidiaries2 gotchas

addsubsidiariesAdd

display_id becomes the Sage entity code and cannot be changed afterwards; it falls back to name when omitted. parent_id and address have no equivalent on Sage's company-config/entity object and are ignored, and only the first entry of currencies is used, as the entity's base currency.

Sage requires a tax solution on every entity, which the unified Subsidiary has no field for. The tax solution an existing entity already uses is applied, falling back to the company's first configured one. To pin a specific one, pass pass_through[taxSolution][key]. A company with no tax solution configured at all cannot create entities through the API.

updatesubsidiariesUpdate

Only name, status and the base currency (the first entry of currencies) can be changed. display_id is the Sage entity code and is fixed once the entity exists; parent_id and address have no equivalent on Sage's company-config/entity object.

Suppliers1 gotcha

addsuppliersAdd

Only active and inactive are applied. Sage's status field has no other members, so any other value from the unified enum is ignored and the record keeps its current status rather than the write being rejected.

Tax Rates1 gotcha

alltaxRatesAll

Tax rates read Sage Intacct's tax/tax-detail objects, and those only exist once the company has a configured tax solution. A company without one returns an empty list — that is a configuration state, not an error, and no API call can distinguish the two.

A tax solution requires the Taxes subscription, an accrual accounting method, and a business user with admin privileges, and it is created in the Sage UI under Taxes > Setup > Tax solutions. It cannot be created through the API, so an empty result here is not something an integration can resolve on the customer's behalf — the customer has to complete tax setup in Sage first.

Each tax detail belongs to exactly one tax solution. In a multi-entity company, verify the entity is assigned the intended tax solution, operating country and tax ID: entities configured against a different solution return a different set of rates.

See Sage's advanced tax setup guide.