Skip to content
SimpleCold

API docs

Endpoints

Rate limits

On this page

Rate limits keep the API fast and fair for everyone. Normal use stays far below them: they are there to stop runaway loops and abuse, not to slow down a real integration.

Limits per endpoint

Limits are counted per API key. Your organisation also has a cap of three keys' worth on each endpoint, so creating more keys doesn't multiply the budget.

EndpointPer API keyPer organisation
GET /api-validation/single and GET /api-validation/single/result (shared)100 / minute300 / minute
POST /api-validation/batch10 / minute30 / minute
POST /api-validation/bulk10 uploads / minute30 / minute
GET /api-validation/bulk/:id/status120 / minute360 / minute
GET /api-validation/bulk/:id/download-result30 / minute90 / minute
GET /api-validation/validate60 / minute–
Webhook endpoint management (/webhooks/*)–120 / minute
Webhook test sends and resends–20 / minute, 100 / hour

Two more limits apply to everyone:

  • Invalid API keys: 30 requests with an invalid, inactive or expired key per 15 minutes from one IP address. After that, requests from that address get a 429 even with a valid key, until the window passes. Requests with a valid key never count toward this.
  • Per IP address: 3,000 requests per minute across the whole API, a safety net far above any single integration.

How the window works

Each limit is a token bucket. A bucket holds as many tokens as the limit (100 for single checks) and refills evenly over the window (one token every 0.6 seconds for 100 per minute). So you can send a burst of up to the full limit at once, then keep going at the steady rate. You never have to wait for a whole minute to pass.

Test keys

Test keys (sc_test_…) have their own budgets, separate from live ones: single 100 per minute, batch 10 per minute, bulk upload 5 per hour, status 120 per minute and download 30 per minute per test key, with three keys' worth per organisation. Sandbox traffic never uses up your live limits. To test your 429 handling without waiting for a real limit, send error-429@… with a test key.

Headers

Every response from a limited endpoint carries:

HeaderMeaning
RateLimit-LimitThe limit that is closest to running out, for example 100.
RateLimit-RemainingRequests you can still send right now under that limit.
RateLimit-ResetSeconds until that bucket is full again. On a 429 it equals Retry-After: the seconds until the next request can succeed.
RateLimit-PolicyEvery limit on the endpoint, as limit;w=window-in-seconds, for example 100;w=60, 300;w=60 (per key, per organisation).

A 429 also carries Retry-After: the number of seconds to wait before the next request can succeed.

The 429 response

429 · Rate limited
{
  "code": "RATE_LIMITED",
  "message": "Rate limit exceeded. Maximum 100 requests per minute per API key.",
  "data": {
    "policy": "api.single",
    "retryAfterSeconds": 1,
    "limit": 100,
    "scope": "apiKey",
    "blocked": false
  },
  "status": 429,
  "error": "Rate limit exceeded. Maximum 100 requests per minute per API key."
}
FieldMeaning
codeAlways RATE_LIMITED.
messageA plain-language explanation.
data.policyWhich limit you hit, for example api.single or api.bulk-status.
data.retryAfterSecondsSame as the Retry-After header.
data.limitThe limit you hit.
data.scopeWhat it is counted against: apiKey, org (your organisation) or ip.
data.blockedtrue when the address is temporarily blocked after repeated limit hits (see below).
status, errorKept for integrations written against older versions. error equals message.

Temporary blocks

Sign-in, sign-up and the invalid-key limit escalate: an address that keeps sending requests after getting 429s is blocked for 15 minutes, then for 1 hour, then for 6 hours if it continues. A blocked request gets the same 429 with data.blocked: true and a long Retry-After. Respecting Retry-After never leads to a block. If you think an address was blocked by mistake, contact support.

Backing off

  1. On a 429, wait Retry-After seconds before sending again. Don't retry immediately: the request will fail again and, on sign-in routes, count toward a block.
  2. On 5xx errors and network failures, retry with exponential backoff and jitter: wait a random time between 0 and base × 2^attempt seconds, capped (for example at 60 seconds), and give up after a few attempts.
  3. Watch RateLimit-Remaining and slow down before it reaches 0, especially in loops.
  4. Spread work out. For many addresses, use batch (50 per request) or bulk instead of many single checks, and poll bulk status at the time nextUpdateAt suggests.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export async function requestWithRetry(url, init = {}, { maxAttempts = 5, baseMs = 500, capMs = 60_000 } = {}) {
  for (let attempt = 0; ; attempt++) {
    let res;
    try {
      res = await fetch(url, init);
    } catch (error) {
      if (attempt + 1 >= maxAttempts) throw error;
      await sleep(Math.random() * Math.min(capMs, baseMs * 2 ** attempt));
      continue;
    }

    if (res.status === 429 && attempt + 1 < maxAttempts) {
      // Respect the server: Retry-After is in seconds.
      const retryAfter = Number(res.headers.get("retry-after") ?? 1);
      await sleep(retryAfter * 1000 + Math.random() * 250);
      continue;
    }
    if (res.status >= 500 && attempt + 1 < maxAttempts) {
      // Full jitter: a random wait between 0 and the exponential cap.
      await sleep(Math.random() * Math.min(capMs, baseMs * 2 ** attempt));
      continue;
    }
    return res;
  }
}

const res = await requestWithRetry(
  `https://simplecold.com/api/v1/api-validation/single?email=${encodeURIComponent("[email protected]")}`,
  { headers: { Authorization: `Bearer ${process.env.SIMPLECOLD_API_KEY}` } },
);
console.log(res.status, res.headers.get("ratelimit-remaining"), await res.json());
Rate limits | SimpleCold API docs