Skip to content

Tax

DayZero charges sales tax on invoices from the business's default tax rate. Tax rates are the percentages, by jurisdiction, and where the collected tax is booked. Tax codes mark product variants as taxable or not. This guide covers both, how tax is worked out when an invoice is finalized, and the tax fields on invoices.

The Tax Settings page is switched off in the DayZero app for now and new businesses start with no rates or codes, but these endpoints work. A business with no default rate charges no sales tax.

All requests need Authorization: Bearer dz_… and x-business-id. GET needs read access, POST and PUT write access, DELETE delete access. Successful responses are wrapped as {"success": true, "data": …}; the examples below show data.

Tax rates

Create a tax rate

POST /api/v1/tax/rates returns 201.

bash
curl -X POST "https://api.ondayzero.com/api/v1/tax/rates" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "California Sales Tax",
    "rate": 0.0725,
    "tax_type": "sales",
    "country": "US",
    "state": "CA",
    "is_default": true
  }'
json
{
  "id": "019a0d41-2b7c-7e19-8f3a-4c6d9e0b1a22",
  "business_id": "019a0b11-4e6f-7a20-8c3d-5b9e2a7f1d44",
  "name": "California Sales Tax",
  "rate": 0.0725,
  "rate_percent": 7.25,
  "tax_type": "sales",
  "country": "US",
  "state": "CA",
  "county": null,
  "city": null,
  "tax_liability_ledger_id": null,
  "is_default": true,
  "is_active": true,
  "created_at": "2026-10-14T15:20:03Z",
  "updated_at": "2026-10-14T15:20:03Z"
}

rate is a decimal fraction, not a percentage: 0.0725 = 7.25%. Up to six decimal places are kept, so 0.08875 stays 8.875%.

Field Type Description
name string Display name (required)
rate number Decimal rate from 0 to 1 (required)
tax_type string sales (default), use, vat, gst, hst, pst
country / state / county / city string Jurisdiction; country defaults to US
tax_liability_ledger_id UUID Ledger the collected tax is booked to. Must be a ledger of this business; Sales Tax Payable is used when omitted
is_default boolean Make this the rate invoices are charged; clears the previous default

List tax rates

bash
curl "https://api.ondayzero.com/api/v1/tax/rates?country=US&state=CA" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
Parameter Type Description
active_only boolean Only return active rates (default true). Ignored when country is set
country string Filter by country code; returns active rates only
state string Filter by state or region; only applied together with country

data is {"items": [...], "total": N, "has_next": false, "has_prev": false}: the whole list in one page, sorted by name. GET /api/v1/tax/rates/{rate_id} returns one rate.

Get the default rate

bash
curl "https://api.ondayzero.com/api/v1/tax/rates/default" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

Returns 404 when no active rate is the default.

Update a tax rate

PUT /api/v1/tax/rates/{rate_id} takes the create fields plus is_active, all optional. Send only what changes.

bash
curl -X PUT "https://api.ondayzero.com/api/v1/tax/rates/{rate_id}" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{"rate": 0.0775, "tax_liability_ledger_id": "019a0b13-9c21-7d0e-8b4f-1e2d3c4b5a60"}'
  • null for name, rate, tax_type or country leaves the field as it is.
  • "is_active": false also clears is_default, so the business has no default until you pick another.
  • A new rate applies to invoices finalized afterwards; invoices already finalized keep their tax.

Tax codes

Tax codes classify product variants: a variant whose code has is_taxable: false is left out of sales tax. Codes don't attach to customers; to charge no tax on one invoice, use tax_exempt.

Seed defaults

Loads TAXABLE (taxable), EXEMPT, NONTAXABLE and RESALE (not taxable) when the business has no codes yet. If it has any code, active or not, nothing is created and the existing codes are returned. Returns 200.

bash
curl -X POST "https://api.ondayzero.com/api/v1/tax/codes/seed-defaults" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

Create a tax code

bash
curl -X POST "https://api.ondayzero.com/api/v1/tax/codes" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "FOOD",
    "name": "Unprepared food",
    "description": "Groceries exempt from state sales tax",
    "is_taxable": false
  }'

code and name are required, description is optional and is_taxable defaults to true. A code the business already has returns 409.

GET /api/v1/tax/codes?active_only=true lists codes sorted by code, in the same one-page shape as the rate list. GET /api/v1/tax/codes/{code_id} returns one. PUT /api/v1/tax/codes/{code_id} updates code, name, description, is_taxable and is_active; renaming to a code that already exists returns 409.

Assign a code to a product variant

Send tax_code_id when you create or update a variant (null clears it, which leaves the variant taxable). The code must belong to the business, otherwise 404. Variant responses return tax_code_id.

Deactivating rates and codes

bash
curl -X DELETE "https://api.ondayzero.com/api/v1/tax/rates/{rate_id}" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

DELETE /api/v1/tax/codes/{code_id} works the same way. Both deactivate (is_active: false) rather than delete: tax lines on existing invoices keep pointing at the rate. Deactivating a rate also clears is_default. Reactivate with PUT … {"is_active": true}.

If an integration import (QuickBooks, Shopify, Square …) created duplicate rates or codes, POST /api/v1/tax/dedupe-imported deactivates the duplicates and returns rates_deactivated and codes_deactivated.

AI rate suggestions

bash
curl -X POST "https://api.ondayzero.com/api/v1/tax/suggest-rates" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"

No body. Suggestions are based on the business's existing rates and come back as suggestions, each with jurisdiction, rate (a percentage here, unlike the decimal rate on /tax/rates), code and reason. Nothing is created; POST /api/v1/tax/rates the ones you want after checking them.

How tax is applied

Tax is worked out once, when an invoice is finalized (PUT /api/v1/invoices/{invoice_id}/finalize, or a bulk finalize). A draft carries no tax.

  1. If the business has no active default rate, no tax is added.
  2. The taxable amount is the sum of the taxable lines:
    • A Products & Services catalog line is taxable only when the item's taxable flag is true (it defaults to false).
    • An inventory variant line is taxable unless the variant's tax code has is_taxable: false.
    • A custom line is taxable.
    • A negative custom line (a discount or credit) lowers the taxable amount in proportion to the taxable share of the invoice; a negative item line lowers it only when that item is taxable. The taxable amount never goes below 0.
  3. Tax = taxable amount × rate, rounded to the nearest cent (half up). It is saved as one tax line (rate, taxable amount, tax amount) and the invoice total becomes the lines plus the tax.
  4. The finalization journal entry debits Accounts Receivable for the total, credits Sales for the lines and credits the rate's tax_liability_ledger_id (Sales Tax Payable when none) for the tax. Voiding reverses all three.

Tax already on an invoice is not recalculated when rates change. Contract invoices carry no default-rate tax: the contract booking posts their total.

Stripe invoices

Before Stripe finalizes an invoice, DayZero adds the tax to the Stripe draft as its own invoice item, in the invoice's currency, so the customer is charged exactly the amount DayZero books. Retrying a failed finalize replaces that item instead of adding another.

  • If the Stripe invoice uses Stripe's automatic tax, DayZero adds nothing and books Stripe's own figures, even when Stripe's tax is 0.
  • If Stripe's final total still differs from the lines plus DayZero's tax, DayZero drops its tax line, books the total Stripe charged, and logs an error.

Exempting one invoice

Send "tax_exempt": true on POST /api/v1/invoices, or on PUT /api/v1/invoices/{invoice_id} while the invoice is a draft (false charges the default rate again; leave it out to keep the current setting).

  • The exemption is recorded against the business's default rate. With no default rate there is nothing to exempt: nothing is stored and the response returns "tax_exempt": false.
  • Changing it on a finalized invoice returns 400 (BUSINESS_061). Sending the value the invoice already has is accepted.

Tax fields on invoice responses

Field Description
subtotal Sum of the lines before tax, in cents
tax_total Tax charged, in cents; 0 when none; null when the response did not load the tax lines
tax_lines One entry per rate: tax_rate_id, name, rate_percent (e.g. 7.25), taxable_amount, tax_amount
tax_exempt true when the invoice is marked to charge no sales tax

The invoice PDF lists the tax per rate between the subtotal and the total.

Errors

Status Code When
400 GEN_002 Validation: missing name/rate, rate outside 0–1, missing code
400 BUSINESS_061 Changing tax_exempt on an invoice that is no longer a draft
404 NOT_FOUND_001 Rate or code not found or in another business; no default rate (/rates/default); a variant tax_code_id from another business
404 NOT_FOUND_014 tax_liability_ledger_id is not a ledger of this business
409 CONFLICT_020 The business already has that tax code
401 / 403 — Missing or invalid token, or the key lacks the permission for the method