TaxLite API Documentation

Reference for the running TaxLite API. This page documents what the server in this repo does today: calculate tax, record sales, view filing buckets, and export reports.

Scope: TaxLite currently supports general US sales tax only (state/county/city/special district). VAT, excise, and income tax are not supported yet.

Quickstart

This is the fastest path to a working integration.

Base URLs

  • Local dev (default): http://localhost:3000 (or PORT env)
  • Production: https://api.taxlite.io

Get an API key

  • Trial key (no login): POST /api/keys/create-trial returns a tl_trial_... API key.
  • Signup (recommended): registering a user automatically provisions a tl_trial_... key for the new company.
  • Live key: login to the Dashboard and regenerate your key there (when on a paid plan).
POST /api/keys/create-trial

Public endpoint. Returns a trial API key (40 calls, 7 days).

curl -X POST https://api.taxlite.io/api/keys/create-trial

Calculate tax (checkout path)

Call /api/calculate during checkout to get the tax amount. The response is intentionally simple: rates are returned as integer *_rate_dbps only.

curl -X POST https://api.taxlite.io/api/calculate \
  -H "Content-Type: application/json" \
  -H "x-company-key: tl_live_your_api_key" \
  -d '{
    "state_code": "CO",
    "city": "Denver",
    "zip_code": "80202",
    "address": "123 Main St",
    "taxable_base_cents": 10000
  }'

Example response (shape):

{
  "taxable_base_cents": 10000,
  "tax_amount_cents": 915,
  "total_amount_cents": 10915,
  "total_rate_dbps": 9150,
  "state_rate_dbps": 2900,
  "local_rate_dbps": 6250,
  "confidence": 1,
  "source": "POLYGON_DATABASE",
  "api_log_id": "abc123...",
  "calculation_id": "abc123...",
  "expires_at": "2026-01-01T00:00:00.000Z"
}

Record a sale (ledger + reporting)

TaxLite does not auto-record. After payment succeeds, call /api/record-sale.

Important: Persist filing identifiers per sale.
Send api_log_id (alias: calculation_id) and let the server hydrate filing_payload from logs.

If filing_payload is missing, TaxLite will still record the sale (no 500), flag it for ops backfill, and it won’t appear in filing buckets/exports until fixed.

Recommended approach: call calculate, then pass the returned api_log_id into record-sale so the server can hydrate the quote details.

curl -X POST https://api.taxlite.io/api/calculate \
  -H "Content-Type: application/json" \
  -H "x-company-key: tl_live_your_api_key" \
  -d '{
    "state_code": "CO",
    "city": "Denver",
    "zip_code": "80202",
    "address": "123 Main St",
    "taxable_base_cents": 10000
  }'

Authentication

TaxLite uses two auth modes.

API key auth (server-to-server)

Used for /api/calculate, /api/record-sale, and /api/report.

MethodHow to send it
Headerx-company-key: tl_live_...
BearerAuthorization: Bearer tl_live_...
Query (GET only)?api_key=tl_live_...

Session auth (Dashboard JWT)

Used for Dashboard endpoints like /api/reports/* and /api/reports/export.

POST /api/auth/login
Content-Type: application/json

{
  "email": "you@company.com",
  "password": "your_password"
}

Send the returned token on subsequent requests:

Authorization: Bearer <jwt_token>

POST /api/calculate

Compute the total sales tax rate and tax amount for a location.

POST /api/calculate

Auth: API key

Request body

FieldDescription
state_code (required)2-letter state code.
taxable_base_cents (required)Integer cents. Example: 10000 = $100.00.
city (required*)Either city or zip_code must be provided.
zip_code (required*)Either city or zip_code must be provided.
addressOptional. If provided, PO Boxes are rejected.
lat, lngOptional. If provided, skips geocoding (faster and less dependent on address parsing).

(required*) means “at least one of these is required.”

Validation rule: right now the server requires city or zip_code even if you provide lat/lng.

Response

Response shape (successful 200): taxable_base_cents, tax_amount_cents, total_amount_cents, total_rate_dbps, state_rate_dbps, local_rate_dbps, api_log_id, calculation_id, expires_at.
Usually present: confidence, source (omitted if unknown).

About *_rate_dbps: this is an integer “deci-bps” field (0.1 bps granularity). Convert to percent via percent = dbps / 1000.

Rate units (bps/dbps)

  • bps = basis points. 100 bps = 1%. Example: 915 bps = 9.15%.
  • dbps = deci-bps (0.1 bps) as an integer. Example: 9150 dbps = 915 bps.

You generally should not compute tax yourself: use tax_amount_cents from the API response.

POST /api/record-sale

Persist a sale (tax event) for reporting + exports.

POST /api/record-sale

Auth: API key

Preferred (best UX): api_log_id-only

If you already called /api/calculate, you should record the sale by sending only api_log_id (alias: calculation_id). TaxLite will hydrate all filing context from logs.

Recommended fields

  • api_log_id (or calculation_id)
  • idempotency_key (prevents duplicate transactions on retries; unique per order/payment)
  • event_id, payment_id (your identifiers)
Hydration rules (api_log_id):
  • The linked /api/calculate response must exist in logs (calculate logging is async).
  • If you also send state_code/taxable_base_cents/total_tax_cents alongside api_log_id, they must match the logged quote (safety check).
  • If you call record-sale immediately after calculate, TaxLite retries briefly to wait for the log row. If still not found, retry record-sale.

Example (preferred)

{
  "event_id": "order-123",
  "payment_id": "pay-456",
  "idempotency_key": "order-123",
  "api_log_id": "abc123..."
}

Alias: you can also send calculation_id instead of api_log_id.

Manual fallback (no api_log_id)

This is an ops-only escape hatch. If you did not call /api/calculate, you can still record manually by providing the core fields. TaxLite will record money totals, but filing identifiers may be missing. This path triggers alerts so gaps get backfilled ASAP.

{
  "event_id": "order-123",
  "payment_id": "pay-456",
  "idempotency_key": "order-123",
  "state_code": "CO",
  "taxable_base_cents": 10000,
  "total_tax_cents": 915
}

Response

{
  "status": "ok",
  "tax_event_id": "uuid-or-null",
  "filing_payload_missing": false,
  "quote_mismatch": false,
  "warnings": []
}

POST /api/refund-sale

Record a refund/return for a previously recorded sale.

POST /api/refund-sale

Auth: API key

Mental model: TaxLite stores refunds as a negative mirror row in tax_events. That means reports, rollups, and filing exports remain correct by summing events.

Required fields

  • original_tax_event_id: the tax_events.id of the sale you are refunding
  • refund_id: your refund identifier (used for idempotency; unique per company)
  • refund_taxable_base_cents: refund base magnitude (positive integer)

Optional fields

  • refund_total_tax_cents: refund tax magnitude (positive integer). If omitted, TaxLite prorates from the original effective rate.
  • apply_to_original_period: boolean (default false). If true, the refund is applied to the original sale month (restatement).
  • refund_timestamp: ISO string or unix seconds/ms. Default: now.
Idempotency + collisions:
  • Retrying the same refund_id returns the existing refund event.
  • Reusing the same refund_id for a different original_tax_event_id is rejected.

Example

{
  "original_tax_event_id": "uuid-of-sale",
  "refund_id": "rf_123",
  "refund_taxable_base_cents": 5000,
  "refund_total_tax_cents": 412,
  "apply_to_original_period": false
}

Response

{
  "status": "ok",
  "original_tax_event_id": "uuid-of-sale",
  "refund_tax_event_id": "uuid-of-refund",
  "refund_id": "rf_123"
}

Reporting

TaxLite supports a legacy state totals report and a filing buckets report.

GET /api/report

Auth: API key. Returns state totals for a date range.

  • start=YYYY-MM-DD (optional)
  • end=YYYY-MM-DD (optional)
  • state=IL (optional)

Filing buckets

Filing buckets aggregate recorded sales into “buckets” derived from each transaction’s filing_payload (state-specific filing identifiers).

GET /api/reports/filing-buckets

Auth: session JWT. Returns bucket totals for one state.

  • state=IL (required)
  • start=YYYY-MM-DD (optional)
  • end=YYYY-MM-DD (optional)
  • filing_category=general_sales (optional, default: general_sales)

Buckets contain bucket_key, identifiers, and money totals. The identifier keys are state-specific.

Exports

Exports are generated from recorded sales (stored tax_events.filing_payload + money fields).

POST /api/reports/export

Auth: session JWT. Returns a file download (CSV/XLSX/NDJSON).

Filing export

{
  "format": "csv",
  "report_type": "filing_csv",
  "state_code": "ALL",
  "filing_category": "general_sales",
  "start": "2025-12-01",
  "end": "2025-12-31"
}

state_code supports "IL" for a single state, or "ALL"/"*" for an all-states export. If no data exists, this endpoint returns 404 with {"error":"No filing_payload data found for export"}.

Errors

All errors return JSON:

{
  "error": "message"
}
StatusMeaning
400Invalid input
401Missing/invalid API key or session
404Not found (or no data for export)
429Rate limit / usage limit exceeded (when enabled)
500Server error

Best practices

  • Recommended flow: calculate at checkout → charge customer → record-sale after payment succeeds.
  • Record with api_log_id: send api_log_id to /api/record-sale so TaxLite hydrates and stores the per-sale filing_payload (filing identifiers).
  • Provide lat/lng when you have it: faster and less dependent on address parsing.
  • Retry strategy: on 429/5xx, retry with exponential backoff.