> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.zip.tax/v-6-0/guides/reference/rate-limiting-errors/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.zip.tax/_mcp/server. # Rate Limiting & Errors 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 Your plan sets how many requests an API key can make per 60-second sliding window: | Plan | Requests per minute | | ---------- | ------------------- | | Starter | 10 | | Growth | 100 | | Pro | 500 | | Enterprise | Defined per account | Upgrading raises the ceiling on your existing key; no rotation is required. Reach out to [support@zip.tax](mailto:support@zip.tax) if you're hitting the limit on your current plan. > **Info** > > The window is a rolling 60 seconds, not a fixed minute boundary. On > Growth, if you send your 100th request 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](../product-rules/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: | Header | Meaning | | ----------------------- | ---------------------------------------------------------- | | `X-RateLimit-Limit` | Your plan's ceiling for the window. | | `X-RateLimit-Remaining` | Requests 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. > **Warning** > > 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`: ```json { "metadata": { "version": "v60", "response": { "code": 108, "name": "RESPONSE_CODE_REQUEST_LIMIT_MET", "message": "API request limit met.", "definition": "http://api.zip-tax.com/request/v60/schema" } } } ``` 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: #### Python ```python import time, requests def lookup_with_retry(address, max_attempts=3): for attempt in range(max_attempts): res = requests.get( "https://api.zip-tax.com/request/v60", headers={"X-API-KEY": "YOUR_API_KEY"}, params={"address": address}, ) data = res.json() code = data["metadata"]["response"]["code"] if code == 100: return data if code == 108 and attempt < max_attempts - 1: # exponential backoff: 1s, 2s, 4s time.sleep(2 ** attempt) continue return data # non-retryable or out of attempts ``` #### Node.js ```javascript async function lookupWithRetry(address, maxAttempts = 3) { for (let attempt = 0; attempt < maxAttempts; attempt++) { const res = await fetch( "https://api.zip-tax.com/request/v60?address=" + encodeURIComponent(address), { headers: { "X-API-KEY": "YOUR_API_KEY" } } ); const data = await res.json(); const code = data.metadata.response.code; if (code === 100) return data; if (code === 108 && attempt < maxAttempts - 1) { await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt)); continue; } return data; } } ``` ### Batch traffic (catalog sync, nightly reconciliation) For large jobs, smooth the traffic proactively instead of reacting to 429s. Read `X-RateLimit-Limit` off the response and space your calls evenly across the window, so the same code works on any plan: ```python def batch_lookup(addresses): results = [] for address in addresses: started = time.monotonic() res = requests.get( "https://api.zip-tax.com/request/v60", headers={"X-API-KEY": "YOUR_API_KEY"}, params={"address": address}, ) results.append(res.json()) # Pace off the ceiling the API reports rather than a hardcoded # number, so this keeps working after a plan change. limit = int(res.headers.get("X-RateLimit-Limit", "10")) interval = 60.0 / limit elapsed = time.monotonic() - started if elapsed < interval: time.sleep(interval - elapsed) return results ``` The rule behind it: divide your plan's per-minute limit by 60 and cap your sender at that many requests per second. On Pro that's about 8 per second; on Starter it's one every 6 seconds. ## 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. ```json { "metadata": { "version": "v60", "response": { "code": 109, "name": "RESPONSE_CODE_ADDRESS_INCOMPLETE", "message": "The provided address is missing, incomplete, or not a valid address.", "definition": "http://api.zip-tax.com/request/v60/schema" } } } ``` 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: | Code | When | What to do | | ------- | ------------------------------------------- | -------------------------------------------------- | | **100** | Normal path. | Read `taxSummaries[0].rate`. | | **108** | Rate limit. | Back off (see above). Transient, so retry is safe. | | **109** | Address didn't resolve. | Surface to the user, don't retry. | | **101** | Bad API key. | Alert your team; probably a config issue. | | **106** | Unknown server error. | Exponential backoff, then escalate. | | **112** | Canadian lookup not included in your plan. | Don't retry; upgrade plan or skip. | | **113** | `taxabilityCode` not included in your plan. | Don't retry; upgrade plan or skip. | See the [full code list](./response-codes) for every value Ziptax can return. ## Common mistakes #### Treating 429 as a generic 'server problem' 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. #### Parsing the \`message\` field 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. #### Waiting for \`Retry-After\` Ziptax doesn't send `Retry-After`. Use exponential backoff or pre-emptively pace your sender based on `X-RateLimit-Remaining`. #### Hammering \`/data/tic\` in a loop 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. #### Retrying 4xx input errors 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. ## Related #### [Response Codes](response-codes) The full table of codes 100 through 113. #### [Account Metrics](account-metrics) Query your current usage and quota with `/account/v{N}/metrics`. #### [REST API](../rest-api/overview) The endpoints that enforce the rate limit. > How Ziptax throttles traffic, what headers to read, and how to back off cleanly