# Rate limits

> Soldgraph allows 60 API calls per minute per account and caps pending searches by plan. See the 429 codes and a client that backs off.

Every plan allows **60 API calls per minute**, counted per account across all your API keys. Searches and job polls both count. The count resets at the start of each minute. Creating another key doesn't raise it.

Each account can also have only a limited number of searches **pending** at once. A search is pending from its 202 response until its job is complete or failed.

| Plan | API calls per minute | Pending searches at once |
| --- | --- | --- |
| Free | 60 | 10 |
| Lite | 60 | 15 |
| Starter | 60 | 25 |
| Pro | 60 | 50 |
| Business | 60 | 100 |
| Scale | 60 | 200 |

Going over either limit returns `429`:

| Code | When | Retry-After |
| --- | --- | --- |
| `rate_limited` | More than 60 calls this minute | 60 |
| `too_many_pending` | Too many searches still pending | 5 |
| `quota_exceeded` | Your plan's allowance is used up and you have no extra requests | 3600 |

A [batch](https://soldgraph.com/docs/api/batches.md) is one API call, however many searches it holds, and each poll of it is one call. A batch can hold as many searches as the pending column above. Its searches count toward your pending cap.

Use long-polling (`?wait=20`) to check jobs. It uses far fewer calls than polling in a loop. When you get `too_many_pending`, wait for your pending searches to finish before you send more.

The API doesn't return `X-RateLimit-*` headers.

## Free plan

The 100 free requests are one set per person. Accounts that share an email address (including `+tag` and Gmail dot variants) or were created from the same network share one set, and deleting an account and signing up again doesn't give a new one. Your dashboard shows the shared total.

Free searches wait behind paid ones when a marketplace is busy, and free accounts together can hold only part of each marketplace's queue. When that part is full, a free search returns `503 queue_full`. Retry with the same `Idempotency-Key`. Paid plans are not affected.

## Marketplace limits

Marketplaces have their own daily limits, separate from yours. When one is reached, new searches on that marketplace return `503 upstream_daily_limit` for up to a day. They cost nothing. Cached results still work. If you see a lot of 503s, check the [status page](https://soldgraph.com/status).

## Backing off

This client sends a search, polls its job, and returns the result. It:

- sends one `Idempotency-Key` per search and reuses it on every retry, so a retry is never charged twice
- long-polls the job instead of sleeping in a loop
- honors `Retry-After`, but gives up instead of sleeping for an hour or a day
- treats `status: "failed"` as a final answer

```typescript title="TypeScript"
const HOST = "https://api.soldgraph.com";
const auth = { Authorization: `Bearer ${process.env.SOLDGRAPH_KEY}` };
const MAX_WAIT = 60; // seconds; quota_exceeded and upstream_daily_limit ask for much longer
const sleep = (s: number) => new Promise((r) => setTimeout(r, s * 1000));

async function call(url: string, headers: Record<string, string>, tries = 5) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, { headers });
    const body = await res.json().catch(() => null);
    if (res.status === 200 || res.status === 202) return body;
    if (body?.status === "failed") return body; // replay of a failed search
    const retryable = res.status === 429 || res.status >= 500;
    const wait = Number(res.headers.get("Retry-After")) || 2 ** attempt;
    if (!retryable || attempt + 1 >= tries || wait > MAX_WAIT) {
      throw new Error(`Soldgraph ${res.status} ${body?.error?.code ?? ""} (X-Request-Id ${res.headers.get("X-Request-Id")})`);
    }
    await sleep(wait);
  }
}

export async function search(path: string, params: Record<string, string>) {
  const key = crypto.randomUUID(); // one key per search, reused on retries
  let job = await call(`${HOST}/v1${path}?${new URLSearchParams(params)}`, { ...auth, "Idempotency-Key": key });
  const deadline = Date.now() + 5 * 60_000;
  while (job.status === "pending") {
    if (Date.now() > deadline) throw new Error(`Soldgraph job ${job.request_id} still pending`);
    job = await call(`${HOST}${job.poll_url}?wait=20`, auth); // poll_url is a path
  }
  if (job.status === "failed") throw new Error(`Soldgraph job failed: ${job.error.code}`);
  return job.result;
}
```

```python title="Python"
import os, time, uuid, requests

HOST = "https://api.soldgraph.com"
AUTH = {"Authorization": f"Bearer {os.environ['SOLDGRAPH_KEY']}"}
MAX_WAIT = 60  # seconds; quota_exceeded and upstream_daily_limit ask for much longer

def call(url, headers, params=None, tries=5):
    for attempt in range(tries):
        res = requests.get(url, params=params, headers=headers, timeout=30)
        try:
            body = res.json()
        except ValueError:
            body = None
        if res.status_code in (200, 202):
            return body
        if body and body.get("status") == "failed":  # replay of a failed search
            return body
        retryable = res.status_code == 429 or res.status_code >= 500
        wait = int(res.headers.get("Retry-After", 0)) or 2 ** attempt
        if not retryable or attempt + 1 == tries or wait > MAX_WAIT:
            code = (body or {}).get("error", {}).get("code")
            raise RuntimeError(f"Soldgraph {res.status_code} {code} (X-Request-Id {res.headers.get('X-Request-Id')})")
        time.sleep(wait)

def search(path, params):
    key = str(uuid.uuid4())  # one key per search, reused on retries
    job = call(f"{HOST}/v1{path}", {**AUTH, "Idempotency-Key": key}, params)
    deadline = time.time() + 300
    while job["status"] == "pending":
        if time.time() > deadline:
            raise RuntimeError(f"Soldgraph job {job['request_id']} still pending")
        job = call(f"{HOST}{job['poll_url']}", AUTH, {"wait": 20})  # poll_url is a path
    if job["status"] == "failed":
        raise RuntimeError(f"Soldgraph job failed: {job['error']['code']}")
    return job["result"]
```

For example, `search("/ebay/sold", { q: "sony wh-1000xm5" })`.

> **Tip:** Pricing a big list? Keep only a few searches pending at a time. [Research a list of items](https://soldgraph.com/docs/guides/price-a-list.md) shows how.
