API Reference
Endpoints, request parameters, and response fields for the Net Amount tax APIs.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /health | Return API health and loaded data metadata. |
GET | /lookup | Look up sales tax by address or coordinates from query parameters. |
POST | /lookup | Look up sales tax by address or coordinates from a JSON body. |
POST | /v1/tax/validate-address | Validate a transaction address against the lookup source. |
POST | /v1/tax/calculate | Calculate line-level seller-collected sales tax. |
POST | /v1/tax/calculate/batch | Calculate up to 50 transaction requests. |
GET | /v1/tax/calculations/{calculation_id} | Retrieve a committed calculation record. |
POST | /v1/tax/calculations/{calculation_id}/replay | Replay a stored request against current rules. |
GET | /v1/tax/codes | List tax-code taxonomy and rule coverage counts. |
GET | /v1/tax/codes/{tax_code} | Inspect one tax code and its rules. |
GET | /v1/tax/coverage | Report rule coverage by jurisdiction and tax code. |
GET | /v1/tax/states/{state_code} | Return imported state guidance, sourcing, filing, and source metadata. |
GET | /v1/tax/states/{state_code}/sourcing | Return only the sourcing slice for an imported state. |
POST | /v1/tax/exemptions/validate | Validate a stored exemption certificate reference. |
POST | /v1/transactions | Create a transaction ledger record. |
POST | /v1/transactions/{id}/void | Append a void event. |
POST | /v1/transactions/{id}/refund | Append a refund event. |
POST | /v1/transactions/{id}/adjust | Append an adjustment event. |
Authentication
Keyless GET /lookup and POST /lookup are available for the public website/demo path when public lookup is enabled. Requests that include an API key use the paid API-key quota path.
The /v1/tax/* and /v1/transactions* endpoints always require Authorization: Bearer <key> or X-Api-Key when billing is enabled. Local offline development may set BILLING_ENABLED=false.
Lookup requests consume the lookup quota. Versioned tax and transaction requests consume the separate calculate meter and return X-Usage-Meter: calculate with quota headers when billing is enabled.
Calculate setup
Database-backed Calculate requires the billing schema plus the full calculate
migration chain. Run bun run db:migrate:calculate only after confirming the
database target. The migration scripts use DATABASE_DIRECT_URL ?? DATABASE_URL,
so remote or shared targets should be intentional and approved before migration.
After migration, run:
bun run audit:calculate-rule-sources -- --as-of 2026-06-29Audit errors are blockers. Active needs_review rows are warning-gated coverage
for QA and source review, not reviewed tax guidance.
To smoke test the live Calculate surface, run the scenario pack against an API
started with TAX_DATA_SOURCE=database:
NET_AMOUNT_BASE_URL="http://localhost:8788" NET_AMOUNT_SCENARIO_ENVIRONMENT="test" \
bun qa/calculate-scenarios/tools/run-scenarios.ts --scenarios qa/calculate-scenarios/scenariosLookup inputs
Provide either address or both lat and lon.
| Field | Type | Notes |
|---|---|---|
address | string | Street address or place text to geocode. |
lat | number | Latitude for coordinate lookup. |
lon | number | Longitude for coordinate lookup. |
asOfDate | YYYY-MM-DD | Optional rate-effective date. Defaults to the current date. |
geocodingProvider | googlemaps, geocodeearth, or geoapify | Optional address geocoder. Defaults to Google Maps. |
provider | googlemaps, geocodeearth, or geoapify | Alias for geocodingProvider. |
Rate dates
Rate-date lookup lets you ask which rates were active on a specific calendar date. Pass asOfDate with a YYYY-MM-DD value.
When a rate date is provided, Net Amount matches the address or coordinate to jurisdictions, then filters each jurisdiction's rates to rows active on that date. In database mode this uses the imported rate effective period; in file mode it uses the source effDate and endDate values. The normalized date is echoed back as input.asOfDate.
Historical coverage currently starts at 2018-01-01. Requests for dates before 2018 are outside the supported historical dataset and may return incomplete or no historical rate coverage. When no rate date is provided, the API uses the current date.
Historical (past) rate dates are a paid feature on the Scale plan and above. A rate date strictly before today returns 403 historical_lookup_requires_plan for keyless/public requests and for API keys below Scale. Current-date and future-date lookups are available on every plan, including keyless demo traffic.
Rate dates apply to tax rates. The current lookup still uses the available place boundary dataset to determine which jurisdictions contain the point.
GET lookup
curl --get http://localhost:8787/lookup \
--data-urlencode "address=200 E Colfax Ave, Denver, CO 80203" \
--data-urlencode "asOfDate=2026-05-01"curl "http://localhost:8787/lookup?lat=39.7392&lon=-104.9903"curl --get http://localhost:8787/lookup \
--data-urlencode "lat=39.7392" \
--data-urlencode "lon=-104.9903" \
--data-urlencode "asOfDate=2024-07-01"POST lookup
curl http://localhost:8787/lookup \
--request POST \
--header "Content-Type: application/json" \
--data '{
"address": "200 E Colfax Ave, Denver, CO 80203",
"asOfDate": "2026-05-01"
}'Validate address
curl http://localhost:8787/v1/tax/validate-address \
--request POST \
--header "Content-Type: application/json" \
--header "Authorization: Bearer $NET_AMOUNT_API_KEY" \
--data '{
"address": {
"line1": "200 E Colfax Ave",
"city": "Denver",
"state": "CO",
"postal_code": "80203",
"country": "US"
},
"asOfDate": "2026-05-01"
}'The response includes validation_id, status, normalized address data, point, matching jurisdictions, data version, warnings, and rule trace.
Calculate tax
Money fields are integer minor units, so 10000 means $100.00 for USD. Quantities are decimal strings with up to 6 decimals.
curl http://localhost:8787/v1/tax/calculate \
--request POST \
--header "Content-Type: application/json" \
--header "Authorization: Bearer $NET_AMOUNT_API_KEY" \
--header "Idempotency-Key: order_1001_calc" \
--data '{
"transaction": {
"type": "sale",
"document_code": "order_1001",
"transaction_date": "2026-05-01",
"currency": "USD",
"commit": false
},
"seller": {
"registrations": [
{
"state": "CO",
"status": "registered",
"effective_from": "2020-01-01"
}
]
},
"addresses": {
"ship_to": {
"line1": "200 E Colfax Ave",
"city": "Denver",
"state": "CO",
"postal_code": "80203",
"country": "US"
}
},
"amounts": {
"shipping_amount": 0
},
"line_items": [
{
"line_id": "line_1",
"quantity": "1",
"unit_amount": 10000,
"tax_code": "commodity.01"
}
]
}'Calculate resolves taxability from effective-dated rule rows. Imported product-taxability commodity codes such as commodity.01 and commodity.05.110 calculate after running bun run db:import:product-taxability; those imported rules are state-scoped and return a review warning until promoted. MVP seed rows are visible for review but are not production calculation inputs. Known codes without reviewed/data-backed rules return unsupported_jurisdiction_rule by default. Requests with transaction.commit=true only return success after durable audit rows are stored; persistence failures return an error response.
Batch calculate
curl http://localhost:8787/v1/tax/calculate/batch \
--request POST \
--header "Content-Type: application/json" \
--header "Authorization: Bearer $NET_AMOUNT_API_KEY" \
--data '{ "calculations": [/* calculate request objects */], "continue_on_error": true }'When continue_on_error=true, a mixed batch can return HTTP 207 with
status:"partial_failure". Each result is marked succeeded or failed, and
failed results include an error object. Batch calculate is draft-only: requests
with transaction.commit=true are rejected with
error.code:"committed_batch_not_supported". Use single-request Calculate or
POST /v1/transactions for committed, durable records.
Catalog and coverage
Use GET /v1/tax/codes, GET /v1/tax/codes/{tax_code}, and GET /v1/tax/coverage to inspect taxonomy, source/review status, effective dates, and unsupported boundaries before relying on a category.
State guidance
Imported summary/sourcing responses are exposed through GET /v1/tax/states/{state_code}. The response includes normalized sourcing, filing, state rate fields, summary text/html, source links, raw-source metadata, and review warnings. These rows are needs_review guidance until promoted into reviewed rule rows.
curl http://localhost:8787/v1/tax/states/AL \
--header "Authorization: Bearer $NET_AMOUNT_API_KEY"Transactions and replay
Committed calculations can be retrieved with GET /v1/tax/calculations/{calculation_id} and replayed with POST /v1/tax/calculations/{calculation_id}/replay. Replay returns original_calculation_id, original_request_hash, replayed_at, and a nested response object. POST /v1/transactions creates a ledger record from a non-lifecycle committed calculation, or from an inline non-lifecycle calculate request, and returns HTTP 201 with status:"committed" and a calculation ID. Lifecycle calculation types must be appended to the original transaction through the void, refund, or adjust endpoints. Those endpoints can append event-only lifecycle evidence or accept an embedded calculate request that is forced to the matching lifecycle transaction type, committed, persisted, and linked to the appended event. Event-only refund and adjustment calls must include a positive amount, linked calculation ID, or positive line, charge, or tax evidence. Successful lifecycle calls return HTTP 200 with status:"voided", status:"refunded", or status:"adjusted". Malformed lifecycle payloads return HTTP 400 with error.code:"invalid_request".
Response shape
type LookupResponse = {
input?: {
address?: string
asOfDate?: string
geocodingProvider?: string
}
point: {
lon: number
lat: number
}
geocode?: {
formatted?: string
confidence?: number
matchType?: string
provider?: string
}
totals: Record<string, { rate: number; percent: number }>
jurisdictions: Array<{
placeId: string
name?: string
placeTypeIds?: string
taxTypeIds?: string
rates?: Array<{
taxTypeName?: string
value?: number
}>
}>
timings: Record<string, number | null>
meta: {
source?: string
matchedJurisdictions?: number
boundaryCount?: number
ratePlaceCount?: number
loadedAt?: string
}
}Errors
Validation errors return 400 with an error object. Versioned calculate errors use stable codes such as unsupported_tax_code, unsupported_jurisdiction_rule, jurisdiction_not_found, and idempotency_conflict. Missing routes return 404 and point callers back to the supported endpoints.
{
"error": {
"code": "missing_lookup_input",
"message": "Provide either address or lon/lat."
}
}