Help center

Rate limits

Each key gets 60 requests per calendar minute. Every counted response carries X-RateLimit headers, and a 429 carries Retry-After in seconds.

Business plan Updated
On this page
  1. The limit
  2. Headers
  3. Over the limit: 429
  4. Back off and retry
  5. Stay under the limit

The limit

60 requests per minute, per key. The window is the calendar minute in UTC (a fixed window, not a sliding one): the count starts at zero at the top of every minute.

  • The limit is per key, not per workspace. Two integrations with their own keys each get 60 a minute.
  • Because the window is fixed, a burst that straddles a minute boundary can get up to 120 requests through in a few seconds. Don’t rely on that; pace your requests instead.
  • Every request made with a working key counts, including ones that end in an error such as 404 not_found or 422 already_assigned.
  • Requests refused before they reach the limiter don’t count: a missing, malformed, unknown or revoked key, a creator_not_admin key, a key in the URL, an unknown endpoint, a wrong method, a body that isn’t JSON, or a parameter that fails validation.

Headers

Every response that counted against the limit carries these headers:

Header Meaning
X-RateLimit-Limit Requests allowed per minute: 60.
X-RateLimit-Remaining Requests left in the current minute.
X-RateLimit-Reset Unix time, in seconds, when the current minute ends and the count resets.

Responses that didn’t count (see above) don’t carry them.

Shell
curl -i https://app.sopcompass.com/api/v1/boards \
  -H "Authorization: Bearer $SOPCOMPASS_API_KEY"
Shell
HTTP/2 200
content-type: application/json; charset=utf-8
cache-control: no-store
x-sopcompass-api-version: v1
x-ratelimit-limit: 60
x-ratelimit-remaining: 57
x-ratelimit-reset: 1790000040

Over the limit: 429

The 61st request in a minute, and every one after it until the minute ends, gets 429 rate_limited with a Retry-After header: the number of seconds until the next minute starts (at least 1).

Shell
HTTP/2 429
retry-after: 17
x-ratelimit-limit: 60
x-ratelimit-remaining: 0
x-ratelimit-reset: 1790000040
JSON
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. The limit is 60 a minute per key."
  }
}

Wait Retry-After seconds, then retry the same request. A 429 changed nothing, so retrying a POST after a 429 is safe.

Back off and retry

Retry 429 after Retry-After. Retry 500 internal_error with exponential backoff and jitter. Don’t retry any other 4xx: the request itself needs fixing.

JavaScript
const BASE = "https://app.sopcompass.com/api/v1";

async function sopcompass(path, init = {}, attempt = 0) {
  const res = await fetch(`${BASE}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.SOPCOMPASS_API_KEY}`,
      ...(init.body ? { "Content-Type": "application/json" } : {}),
      ...init.headers,
    },
  });

  const retryable = res.status === 429 || res.status >= 500;
  if (retryable && attempt < 5) {
    const retryAfter = Number(res.headers.get("retry-after"));
    const backoff = Math.min(30, 2 ** attempt) + Math.random();
    const waitSeconds = res.status === 429 && retryAfter > 0 ? retryAfter : backoff;
    await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000));
    return sopcompass(path, init, attempt + 1);
  }

  const body = await res.json();
  if (!res.ok) {
    const err = new Error(`${res.status} ${body.error.code}: ${body.error.message}`);
    err.status = res.status;
    err.code = body.error.code;
    throw err;
  }
  return body;
}
Python
import os
import random
import time

import requests

BASE = "https://app.sopcompass.com/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['SOPCOMPASS_API_KEY']}"


class SOPCompassError(Exception):
    def __init__(self, status, code, message):
        super().__init__(f"{status} {code}: {message}")
        self.status = status
        self.code = code


def sopcompass(method, path, max_attempts=6, **kwargs):
    for attempt in range(max_attempts):
        res = session.request(method, f"{BASE}{path}", timeout=30, **kwargs)
        retryable = res.status_code == 429 or res.status_code >= 500
        if retryable and attempt < max_attempts - 1:
            retry_after = res.headers.get("Retry-After")
            if res.status_code == 429 and retry_after:
                wait = int(retry_after)
            else:
                wait = min(30, 2 ** attempt) + random.random()
            time.sleep(wait)
            continue
        body = res.json()
        if not res.ok:
            raise SOPCompassError(res.status_code, body["error"]["code"], body["error"]["message"])
        return body

Stay under the limit

  • Read X-RateLimit-Remaining. When it reaches 0, pause until X-RateLimit-Reset instead of sending a request you know will fail.
  • Use limit=100 on GET /sops and GET /assignments. Fewer, larger pages use fewer requests.
  • Sync incrementally. Poll GET /sops with updated_since rather than re-reading everything. See Filtering and updated_since.
  • Fetch full SOPs only when they changed. GET /sops/{id} costs one request per SOP; compare updated_at first.
  • One key per integration. Each gets its own 60 a minute, and a runaway script can’t starve your other integrations.