Wolves of Capitol HillAPI Docs

Rate limits & pagination

The minute and month windows, X-RateLimit-* headers, 429 handling with Retry-After, and cursor pagination semantics.

Two windows

Every request consumes from two fixed windows:

  1. a minute burst window (10/min anonymous, 60/min Pack, 300/min Den), and
  2. a calendar-month quota (1,000 anonymous, 100,000 Pack, 1,000,000 Den).

Exceeding either returns 429. Keyed requests are metered per key; anonymous requests are metered per (salted, hashed) client address.

The headers

Every response — success or failure — carries the minute window's state:

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute for the caller.
X-RateLimit-RemainingRequests remaining in the current minute window.
X-RateLimit-ResetWhen the minute window resets (unix seconds).

Handling 429

A denied request answers with Retry-After (seconds) and a machine-readable body:

HTTP/1.1 429 Too Many Requests
Retry-After: 21
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1782043260
{ "error": "rate_limited", "retryAfterSeconds": 21 }

Back off for Retry-After seconds, then retry. All /api/v1 endpoints are GET (idempotent), so retrying is always safe. A monthly-quota denial looks the same but with a much larger Retry-After — treat any value over an hour as "wait for the next month or upgrade."

async function wochFetch(url, init, { maxRetries = 3 } = {}) {
  for (let attempt = 0; ; attempt += 1) {
    const response = await fetch(url, init);
    if (response.status !== 429 || attempt >= maxRetries) return response;
    const retryAfter = Number(response.headers.get('Retry-After') ?? '1');
    await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
  }
}

Cursor pagination

List endpoints (/api/v1/trades, /api/v1/filings) use keyset pagination ordered by (disclosedAt desc, id desc) — newest disclosures first.

  • limit — rows per page, 1–100, default 25.
  • cursor — the nextCursor from the previous response, passed back verbatim. It is an opaque token; don't parse or construct it.
  • nextCursor: null — you're on the last page.
curl "https://www.wolfofcapitolhill.com/api/v1/trades?limit=100"
# → { "data": [...], "nextCursor": "MjAyNi0wNi0zMFQ..." }

curl "https://www.wolfofcapitolhill.com/api/v1/trades?limit=100&cursor=MjAyNi0wNi0zMFQ..."

Rules that keep a walk correct:

  • Keep the filters identical across pages. The cursor only encodes the keyset position, not your query — changing ticker/chamber/since mid-walk changes what the cursor pages over.
  • A malformed or truncated cursor is 400 invalid_cursor — pass the exact nextCursor string.
  • Keyset pagination is stable under concurrent inserts: rows published while you paginate never shift your pages (new rows appear before your first page, not inside your walk).

Incremental sync with since

For "give me everything new since my last poll," filter with since (ISO 8601 instant, offset required — Z works) instead of diffing full walks:

curl "https://www.wolfofcapitolhill.com/api/v1/trades?since=2026-07-01T00:00:00Z&limit=100"

Record the newest disclosedAt you've seen, use it as the next poll's since, and page through with cursor if a poll returns a full page. If you would rather not poll at all, webhooks push trade.published events to your server the moment filings publish.