Rate Limiting & Errors

How Ziptax throttles traffic, what headers to read, and how to back off cleanly

Ziptax enforces a per-API-key rate limit on every call to the /request/v* endpoints. If you send too many requests in a short window, you’ll get an HTTP 429 Too Many Requests with application code 108. This page covers the exact window, the response headers, the recommended backoff strategy, and the shape of every error response so you can branch on it reliably.

The rate limit

Every API key has a request_rate entitlement that sets how many requests it can make per 60-second sliding window. The default on new keys is 10,000 requests per minute. Higher limits are available on Pro and Enterprise plans. Reach out to support@zip.tax if you’re hitting the ceiling.

The window is a rolling 60 seconds, not a fixed minute boundary. If you send 10,000 requests at 12:00:30, you won’t get another success until 12:01:30, not at the top of the next minute.

The rate limit applies per API key across all API versions (v10 to v60) and across regions. Splitting traffic across v50 and v60 from the same key won’t get you extra headroom.

The TIC feed is separate

GET /data/tic has its own 100 requests per minute limiter that isn’t shared with /request/v*. It’s meant as a reference feed, so pull the catalog once, cache it, and refresh weekly. See Taxability Information Codes for the recommended pattern.

Rate limit headers

Every response (success or 429) includes two headers describing the current state of your window:

HeaderMeaning
X-RateLimit-LimitYour request_rate entitlement, the ceiling for the window.
X-RateLimit-RemainingRequests still available before you’ll start getting 108s.

Read these on every response, not just errors. They’re your early warning signal that you’re approaching the limit.

Ziptax does not send a Retry-After header on 429s. Use the backoff strategy below instead of waiting for the server to tell you when to retry.

What a 108 looks like

When you exceed the limit, Ziptax returns HTTP 429 with the standard response envelope and code: 108:

1{
2 "metadata": {
3 "version": "v60",
4 "response": {
5 "code": 108,
6 "name": "RESPONSE_CODE_REQUEST_LIMIT_MET",
7 "message": "API request limit met.",
8 "definition": "http://api.zip-tax.com/request/v60/schema"
9 }
10 }
11}

There are no baseRates or taxSummaries fields on a 108, so branch on the code before reading rate data.

Backoff strategy

The right pattern depends on whether your traffic is user-driven or batch.

User-driven traffic (checkout, quote)

A single retry with a short delay is usually enough, because you’re probably hitting the limit from burst traffic that will clear on its own:

1import time, requests
2
3def lookup_with_retry(address, max_attempts=3):
4 for attempt in range(max_attempts):
5 res = requests.get(
6 "https://api.zip-tax.com/request/v60",
7 headers={"X-API-Key": "YOUR_API_KEY"},
8 params={"address": address},
9 )
10 data = res.json()
11 code = data["metadata"]["response"]["code"]
12
13 if code == 100:
14 return data
15 if code == 108 and attempt < max_attempts - 1:
16 # exponential backoff: 1s, 2s, 4s
17 time.sleep(2 ** attempt)
18 continue
19 return data # non-retryable or out of attempts

Batch traffic (catalog sync, nightly reconciliation)

For large jobs, smooth the traffic proactively instead of reacting to 429s. Watch X-RateLimit-Remaining and slow down before you hit zero:

1def batch_lookup(addresses):
2 results = []
3 for address in addresses:
4 res = requests.get(
5 "https://api.zip-tax.com/request/v60",
6 headers={"X-API-Key": "YOUR_API_KEY"},
7 params={"address": address},
8 )
9 results.append(res.json())
10
11 remaining = int(res.headers.get("X-RateLimit-Remaining", "10000"))
12 if remaining < 100:
13 # pre-emptively pause; we're close to the limit
14 time.sleep(5)
15 return results

A simpler approach is client-side pacing: if your entitlement is 10,000 per minute, cap your sender at ~150 requests per second and you’ll never trip 108s.

The error envelope

All Ziptax errors share the same envelope as success responses. The numeric code at metadata.response.code is the stable contract. Always branch on it, not on the HTTP status or the human-readable message.

1{
2 "metadata": {
3 "version": "v60",
4 "response": {
5 "code": 109,
6 "name": "RESPONSE_CODE_ADDRESS_INCOMPLETE",
7 "message": "The provided address is missing, incomplete, or not a valid address.",
8 "definition": "http://api.zip-tax.com/request/v60/schema"
9 }
10 }
11}

Legacy versions (v50 and earlier) return the same fields at the top level as rCode, rName, and rMessage instead of nested under metadata.response. The numeric codes are identical.

Errors you’ll actually hit

Most production integrations only ever see a handful of codes. Here’s what to wire up first:

CodeWhenWhat to do
100Normal path.Read taxSummaries[0].rate.
108Rate limit.Back off (see above). Transient, so retry is safe.
109Address didn’t resolve.Surface to the user, don’t retry.
101Bad API key.Alert your team; probably a config issue.
106Unknown server error.Exponential backoff, then escalate.
112Canadian lookup without entitlement.Don’t retry; upgrade plan or skip.
113taxabilityCode without entitlement.Don’t retry; upgrade plan or skip.

See the full code list for every value Ziptax can return.

Common mistakes

HTTP 429 is specifically a rate limit signal, so back off rather than retrying hard. A tight retry loop on 429 will make it worse because every retry counts against the window.

The message string is documented here for humans but may be refined over time. Branch on the numeric code instead; it’s stable across versions.

Ziptax doesn’t send Retry-After. Use exponential backoff or pre-emptively pace your sender based on X-RateLimit-Remaining.

The TIC feed is a reference table, not a per-request lookup. Pull it once, cache it, and refresh weekly. Its 100 req/min limiter will cut you off fast if you treat it like the rate endpoint.

Codes 101 through 105, 109, and 111 are deterministic input failures. Retrying won’t help. Fix the input, or surface the error to the caller.