CompSniper Docs

Rate limits and quotas

Handle CompSniper per-minute rate limits and monthly quota exhaustion without retry storms.

Two independent limits protect every account.

The Shipping Rates API is a separate product with its own quota and per-minute limit. Its usage does not change the sold-comps limits described below. See Shipping Rates API.

Per-minute rate limit

The Basic plan allows 30 requests per minute. Starter, Growth, and Scale allow 60 requests per minute. High-volume plans allow 120 requests per minute. Paid subscriptions and accounts using purchased credits also receive priority in the live-search queue during busy periods. When a per-minute limit is exceeded, the API returns:

{
  "error": "Per-minute rate limit exceeded.",
  "code": "rate_limited",
  "retry_after": 12
}

Wait for Retry-After, add a small random jitter, and retry with a strict maximum attempt count.

Sold history searches

/v1/research/sold also has its own cap of 20 searches per minute per account, on top of the plan limit above. Over it, the API answers 429 with code history_rate_limited and a Retry-After header. Every history response includes X-History-RateLimit-Limit and X-History-RateLimit-Remaining.

Carryover, monthly quota, and purchased credits

When an existing subscriber pays the full price to upgrade, the new billing cycle starts immediately with the full new allowance. Unused monthly requests from the previous plan carry over until the next renewal and are used first. CompSniper then uses the new monthly allowance. If both are exhausted and the account has purchased credits, each successful request automatically uses one credit. Credits cost $5 per 1,000, can be purchased in increments of 100, and never expire. No header is needed for this. Optionally, send X-Credit-Source: credits to spend purchased credits before your monthly allowance. When your credits run out, those requests automatically fall back to your monthly allowance instead of failing.

When both selected billing sources are empty, the API returns:

{
  "error": "Free plan quota exhausted. Upgrade your CompSniper plan to continue now, or wait for your monthly reset.",
  "code": "quota_exceeded",
  "plan": "basic",
  "quota": 100,
  "used": 100,
  "remaining": 0,
  "rollover_balance": 0,
  "reset_at": "2026-09-18T05:30:36.000Z",
  "upgrade_url": "https://compsniper.com/dashboard/subscription",
  "credit_balance": 0,
  "purchase_credits_url": "https://compsniper.com/dashboard/subscription#credits"
}

Do not retry quota_exceeded. Stop the job, show purchase_credits_url or upgrade_url, or wait until reset_at.

Sending many requests at once

The synchronous GET /v1/scrape endpoint is built for interactive, one-keyword-at-a-time calls. Behind it we fetch live data straight from eBay, and eBay's systems do not like being hammered: a sudden burst of concurrent requests looks like abuse to them and gets rate-limited or challenged at the source. When that happens you'll see 503 server_busy (we're briefly holding traffic back to stay under eBay's limits) or 502 upstream_blocked (eBay challenged that request). These are transient, but retrying them immediately just adds more load and makes it worse, not better.

So if you need to run many keywords or send a high-volume burst, do not fan them out at /v1/scrape. Submit them as an async job instead. The queue paces the work at a rate eBay accepts, spreads it safely across the pool, and returns every result, so a large workload completes reliably without the burst errors:

  • Scrape jobs (POST /v1/scrape/jobs): send a list of keyword queries, get a jobId, then poll GET /v1/scrape/jobs/{jobId} for results as each one finishes. Submit your whole batch as one job (up to 100 queries), and when it's done submit the next. This is the endpoint for any bulk or bursty workload.
  • Card Batch (/docs/card-batch): same engine with structured card fields (player, year, set, grade) for trading-card sellers.
  • Max Mode (/docs/max-mode): deep pagination of a single keyword, with CSV export.

Rule of thumb: a handful of interactive calls is fine on /v1/scrape. Anything bulk or bursty belongs on /v1/scrape/jobs, because eBay punishes bursts and the job queue is what keeps you under its radar.

Response headers

HeaderMeaning
X-RateLimit-LimitRequests allowed in the current minute
X-RateLimit-RemainingRequests left in the current minute
X-RateLimit-ResetUnix timestamp when the minute window resets
X-Usage-LimitMonthly plan allowance
X-Usage-RemainingMonthly requests remaining
X-Credit-SourceBilling source used: rollover, subscription, credits, or none
X-Rollover-BalanceTemporary carried-over requests remaining
X-Credit-BalancePurchased credits remaining
Retry-AfterSeconds before a temporary retry

Shipping responses use X-Shipping-Plan, X-Shipping-Usage-Limit, X-Shipping-Usage-Used, X-Shipping-Usage-Remaining, and X-Shipping-Usage-Reset so the two products cannot be confused.

Bounded retry example

async function searchWithRetries(url, apiKey) {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    const body = await response.json();

    if (response.ok) return body;
    if (response.status === 429 && body.code === "quota_exceeded") {
      throw new Error(`Quota exhausted. Buy credits: ${body.purchase_credits_url}`);
    }

    const temporary =
      (response.status === 429 && body.code === "rate_limited") ||
      [500, 502, 503].includes(response.status);

    if (!temporary || attempt === 4) throw new Error(body.error);
    const delay = Number(response.headers.get("Retry-After") || body.retry_after || 2 ** attempt);
    await new Promise((resolve) => setTimeout(resolve, (delay + Math.random() * 0.5) * 1000));
  }
}

On this page