> This page is for version v6.0 (default).
> For other versions, use one of these documentation indexes:
> - v6.0 (default): https://docs.zip.tax/v-6-0/llms.txt
> - v5.0: https://docs.zip.tax/v-5-0/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.zip.tax/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.