Developer Documentation
The Tax Holiday API provides structured data on every U.S. state sales tax holiday — dates, categories, and per-item price limits — via a simple REST interface.
All requests are authenticated with an API key and an HMAC-SHA256 signature. This page covers everything you need to make your first authenticated request.
| Base URL | |
|---|---|
| Production | https://api.taxholiday.app |
| Response format | application/json |
| Auth method | API Key + HMAC-SHA256 request signing |
| Rate limits | Free: 150 req/day · Pro: 10,000 req/day |
Quick Start
Once you have a key ID and secret, here's the fastest path to a working request:
# 1. Set your credentials KEY_ID="txh_your_key_id" SECRET="your_64_char_secret" METHOD="GET" PATH="/v1/holidays/state/TX" # 2. Build the signature TS=$(date +%s) BODY_HASH=$(printf '' | sha256sum | cut -d' ' -f1) MSG="${TS}|${METHOD}|${PATH}|${BODY_HASH}" SIG=$(printf '%s' "$MSG" | openssl dgst -sha256 -hmac "$SECRET" -hex | cut -d' ' -f2) # 3. Make the request curl https://api.taxholiday.app"$PATH" \ -H "X-API-Key: $KEY_ID" \ -H "X-Timestamp: $TS" \ -H "X-Signature: $SIG"
Required Headers
Every request to a protected endpoint must include all three of the following headers.
| Header | Value | Description |
|---|---|---|
| X-API-Key | txh_<16 hex chars> |
Your public key ID. Identifies your account. |
| X-Timestamp | 1777168000 |
Current Unix time (seconds since epoch, UTC). Must be within ±5 minutes of server time. |
| X-Signature | 64 hex chars |
HMAC-SHA256 of the canonical message string. See Signing a Request. |
Signing a Request
The signature covers the timestamp, HTTP method, path, and a hash of the request body. This prevents tampering and replay attacks.
Algorithm
-
Get the current Unix timestamp
Seconds since epoch (UTC).int(time.time())in Python,Math.floor(Date.now()/1000)in JS,date +%sin shell. -
Hash the request body
SHA-256 the raw body bytes, hex-encode the digest. For GET requests with no body, hash the empty string:sha256(b"").
Result looks like:e3b0c44298fc1c149afb... -
Build the canonical message
Pipe-delimited string with no spaces:
{timestamp}|{METHOD}|{path}|{body_sha256}
Example:1777168000|GET|/v1/holidays/state/TX|e3b0c44... -
HMAC-SHA256 the message with your secret
Use your secret key (not your key ID) as the HMAC key. Hex-encode the output. The result is yourX-Signature.
/v1/holidays/state/TX is correct; https://api.taxholiday.app/v1/holidays/state/TX?foo=bar is not.
Code Examples
Full examples for making a signed request in Python, Node.js, and shell.
import hashlib, hmac, time import requests KEY_ID = "txh_your_key_id" SECRET = "your_64_char_secret" def sign(method, path, body=b""): ts = str(int(time.time())) bh = hashlib.sha256(body).hexdigest() message = f"{ts}|{method}|{path}|{bh}" sig = hmac.new(SECRET.encode(), message.encode(), hashlib.sha256).hexdigest() return { "X-API-Key": KEY_ID, "X-Timestamp": ts, "X-Signature": sig, } # Example: fetch all Texas holidays path = "/v1/holidays/state/TX" resp = requests.get( f"https://api.taxholiday.app{path}", headers=sign("GET", path), ) print(resp.json())
const crypto = require('crypto'); const KEY_ID = 'txh_your_key_id'; const SECRET = 'your_64_char_secret'; function sign(method, path, body = '') { const ts = String(Math.floor(Date.now() / 1000)); const bh = crypto.createHash('sha256').update(body).digest('hex'); const message = `${ts}|${method}|${path}|${bh}`; const sig = crypto.createHmac('sha256', SECRET).update(message).digest('hex'); return { 'X-API-Key': KEY_ID, 'X-Timestamp': ts, 'X-Signature': sig }; } // Example: fetch all Texas holidays const path = '/v1/holidays/state/TX'; fetch(`https://api.taxholiday.app${path}`, { headers: sign('GET', path) }) .then(r => r.json()) .then(console.log);
#!/usr/bin/env bash KEY_ID="txh_your_key_id" SECRET="your_64_char_secret" METHOD="GET" PATH="/v1/holidays/state/TX" TS=$(date +%s) BODY_HASH=$(printf '' | sha256sum | cut -d' ' -f1) MSG="${TS}|${METHOD}|${PATH}|${BODY_HASH}" SIG=$(printf '%s' "$MSG" | openssl dgst -sha256 -hmac "$SECRET" -hex | cut -d' ' -f2) curl "https://api.taxholiday.app${PATH}" \ -H "X-API-Key: $KEY_ID" \ -H "X-Timestamp: $TS" \ -H "X-Signature: $SIG" # macOS note: use gdate (from brew install coreutils) for date +%s # and gsha256sum for sha256sum if the built-ins don't support those flags.
<?php define('KEY_ID', 'txh_your_key_id'); define('SECRET', 'your_64_char_secret'); function sign(string $method, string $path, string $body = ''): array { $ts = (string) time(); $bh = hash('sha256', $body); $message = "$ts|$method|$path|$bh"; $sig = hash_hmac('sha256', $message, SECRET); return [ 'X-API-Key: ' . KEY_ID, 'X-Timestamp: ' . $ts, 'X-Signature: ' . $sig, ]; } // Example: fetch all Texas holidays $path = '/v1/holidays/state/TX'; $ch = curl_init('https://api.taxholiday.app' . $path); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => sign('GET', $path), ]); $data = json_decode(curl_exec($ch), true); print_r($data);
Endpoints
All endpoints are read-only GET requests. A valid X-API-Key, X-Timestamp, and X-Signature are required on every call.
Returns all tax holidays on record for the given state, regardless of date.
TX, FL, TNReturns holidays that are currently active or start within the next N days.
90Returns all active or upcoming holidays across every state we track.
30Returns all holidays that include the given category.
Filter by any combination of state, category, and date.
YYYY-MM-DD — returns holidays active on that date (optional)Resolves a 5-digit US ZIP code to its state and county, then returns all tax holidays that apply to that location. Holidays marked as statewide are always included; holidays restricted to a specific county, city, or ZIP are included only when they match.
78701YYYY-MM-DD — filter to holidays active on that specific date (optional). Omit to return all holidays for the ZIP's state.Returns a ZIP holidays response with the resolved state code, county name, and matching holidays. Returns 404 if the ZIP is unknown.
Returns the current sales tax rate for a 5-digit US ZIP code, broken down by component (state, county, city, special district). The total_rate field is the combined effective rate to apply at checkout.
78701Returns a tax rate response. Returns 404 if the ZIP is unknown or no rate data is available for that state.
Returns the current sales tax rate for a ZIP code alongside the nearest tax holiday matching the requested category and jurisdiction. Designed for checkout flows — one request tells you both the rate to charge and whether a holiday exemption applies.
Holidays are filtered by the ZIP's state and jurisdiction (statewide, county, city, or ZIP-specific). tax_holiday.status is "active" when the holiday is currently in effect, or "upcoming" when it starts in the future. When no matching holiday exists, tax_holiday is false.
78701clothing, school_suppliesReturns a combined response. Returns 404 if the ZIP is unknown or has no rate data.
Category slugs
Known categories returned in categories[].category fields:
clothing— apparel and footwearschool_supplies— pens, paper, folders, etc.computers— personal computers and tabletsemergency_supplies— flashlights, batteries, generatorsenergy_star— ENERGY STAR rated appliancesfirearms— guns and hunting equipmenthunting— hunting supplies
Response Format
All responses are JSON. The top-level shape depends on the endpoint.
State holidays response
{
"state": "TX",
"holidays": [
{
"name": "Back to School Sales Tax Holiday",
"start_date": "2026-08-07",
"end_date": "2026-08-09",
"description": "Clothing, school supplies, and backpacks exempt from sales tax.",
"source_url": "https://comptroller.texas.gov/taxes/publications/98-490/",
"categories": [
{
"category": "clothing",
"price_limit": 100.00,
"notes": "Per item; must be under $100"
},
{
"category": "school_supplies",
"price_limit": 100.00,
"notes": "Per item"
}
]
}
]
}
ZIP holidays response
{
"zip_code": "78701",
"state_code": "TX",
"county": "Travis",
"count": 3,
"holidays": [
{
"name": "Back to School Sales Tax Holiday",
"start_date": "2026-08-07",
"end_date": "2026-08-09",
"description": "Clothing, school supplies, and backpacks exempt from sales tax.",
"source_url": "https://comptroller.texas.gov/...",
"categories": [
{ "category": "clothing", "price_limit": 100.00, "notes": "Per item" }
]
}
]
}
county is the county name the ZIP maps to. count is the total number of holidays returned. Each holiday in holidays[] has the same shape as the state endpoints.
Tax rate response
{
"zip_code": "78701",
"state_code": "TX",
"county": "Travis County",
"state_rate": 0.0625,
"county_rate": 0.0,
"city_rate": 0.02,
"special_rate": 0.0,
"total_rate": 0.0825,
"coverage": "full",
"effective_date": "2026-01-01",
"last_verified": "2026-04-28"
}
The coverage field indicates how complete the rate breakdown is: "full" includes all components; "county" includes state + county only; "state" is the state base rate only (local rates not yet mapped for this state).
Tax rate + holiday response
When a matching holiday is found, tax_holiday is an object. When there is no active or upcoming holiday for that category and location, tax_holiday is false.
{
"zip_code": "78701",
"state_code": "TX",
"county": "Travis County",
"city": "Austin",
"category": "clothing",
"tax_rate": {
"state_rate": 0.0625,
"county_rate": 0.0,
"city_rate": 0.02,
"special_rate": 0.0,
"total_rate": 0.0825,
"coverage": "full",
"effective_date": "2026-01-01",
"last_verified": "2026-04-28"
},
"tax_holiday": {
"name": "Back to School Sales Tax Holiday",
"start_date": "2026-08-07",
"end_date": "2026-08-09",
"description": "Clothing, school supplies, and backpacks exempt from sales tax.",
"source_url": "https://comptroller.texas.gov/taxes/publications/98-490/",
"status": "upcoming",
"price_limit": 100.00,
"notes": "Per item; must be under $100"
}
}
{
"zip_code": "10001",
"state_code": "NY",
"county": "New York County",
"city": "New York",
"category": "clothing",
"tax_rate": { /* rate fields */ },
"tax_holiday": false
}
Field reference
| Field | Type | Description |
|---|---|---|
| start_date / end_date | string | ISO 8601 dates YYYY-MM-DD. Holiday is active on both boundary dates (inclusive). |
| source_url | string | null | Direct link to the official state revenue department page for this holiday. |
| categories[].price_limit | number | null | Maximum item price for the exemption to apply. null means no cap. |
| tax_holiday | object | false | Present on the combined endpoint. false when no active or upcoming holiday matches the category and ZIP's jurisdiction. |
| tax_holiday.status | string | "active" when today falls within the holiday window; "upcoming" when the holiday starts in the future. |
| tax_holiday.price_limit | number | null | Per-item price ceiling for the exemption. Items above this price are taxed at the normal rate. null means fully exempt with no cap. |
| coverage | string | "full" = all rate components available; "county" = state + county only; "state" = state base rate only. |
Error Codes
Errors return a JSON body with a detail field describing the problem.
| Status | detail message | Cause |
|---|---|---|
| 401 | Invalid or inactive API key | X-API-Key not found in the database, or the key has been deactivated. |
| 401 | Request timestamp out of range | X-Timestamp differs from server time by more than 5 minutes. Sync your system clock. |
| 401 | Invalid X-Timestamp | X-Timestamp is not a valid integer. |
| 401 | Invalid request signature | The computed HMAC does not match X-Signature. See troubleshooting below. |
| 404 | ZIP code 12345 not found | The ZIP passed to /holidays/zip/{zip} is not in our database, or is not a valid 5-digit US ZIP code. |
| 404 | No tax rate data for ZIP 12345 | The ZIP is valid but no rate data has been loaded for that state yet. Run the tax rate scrapers. |
| 422 | field required / value is not a valid … | Missing or malformed query parameter. |
| 429 | Daily rate limit exceeded | You've used all requests in your daily quota. Check X-RateLimit-Limit and Retry-After headers. |
| 200 | — | Success. Body is the JSON response. |
Troubleshooting
"Invalid request signature"
This is by far the most common issue. Check each of these in order:
- Are you using the secret key, not the key ID? The HMAC key is the long hex secret (64 chars), not the
txh_…key ID you send inX-API-Key. - Is the method uppercase? The canonical message requires
GET, notget. - Is the path exactly right? No trailing slash, no query string, no scheme/host. Sign
/v1/holidays/state/TX, not/v1/holidays/state/TX/. - Is the body hash correct for GET requests? Hash the empty byte string (
b""/""), not the string"null"or a missing argument. - Is the same timestamp in both the message and the header? Generate
tsonce and use that exact value in both places. - Is the message format exactly
{ts}|{METHOD}|{path}|{body_hash}? Pipe-separated, no spaces, no newlines.
"Request timestamp out of range"
The server rejected your timestamp because it differs from server time by more than 5 minutes.
- Check your system clock:
date -ushould match real UTC time. - If you're running inside a VM or container, the guest clock may have drifted. Resync with
sudo chronyc makesteporsudo ntpdate -u pool.ntp.org. - Don't store and reuse timestamps. Generate a fresh one per request.
"Invalid or inactive API key"
- Verify you're sending the key ID (e.g.
txh_a1b2c3d4e5f6g7h8), not the secret. - The key may have been deactivated. Contact support.
- Make sure the header name is
X-API-Keyexactly (case-insensitive on most servers, but double-check).
Rate limit hit (429)
Check the response headers:
X-RateLimit-Limit: 150 Retry-After: 86400
Retry-After is in seconds. Quotas reset at midnight UTC.
Signature expires immediately
Signatures are only valid for 5 minutes from the timestamp. If you're saving a pre-generated curl command and running it later, it'll be rejected. Generate and execute in one step — the code examples above all do this correctly.
macOS shell differences
macOS ships BSD utilities with different flags. Use Homebrew's GNU coreutils:
brew install coreutils # Then use gdate instead of date, gsha256sum instead of sha256sum TS=$(gdate +%s) BODY_HASH=$(printf '' | gsha256sum | cut -d' ' -f1)