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.

PlanAPI calls per minutePending searches at once
Free6010
Starter6025
Pro6050
Business60100
Scale60200

Going over either limit returns 429:

CodeWhenRetry-After
rate_limitedMore than 60 calls this minute60
too_many_pendingToo many searches still pending5
quota_exceededYour plan's allowance is used up and you have no extra requests3600

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 free allowance is one per person. Accounts that share an email address (including +tag and Gmail dot variants) or were created from the same network share one free allowance. 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.

#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
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 longerconst 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;}

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