> ## Documentation Index
> Fetch the complete documentation index at: https://docs.virtuous.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate Limits

> CRM+ API rate limit thresholds, response headers, and how to handle 429 responses gracefully in production integrations.

The CRM+ API enforces a rate limit of **5,000 requests per hour** per Virtuous organization. The limit applies to all endpoints equally and is primarily a safeguard against runaway loops and misconfigured sync jobs — most partner integrations stay well within it under normal operation.

This page covers how to read the rate limit headers, what to do when you hit the limit, and patterns for keeping high-volume integrations well below the threshold.

<Warning>
  The rate limit is scoped to the **Virtuous organization**, not to individual API Keys or OAuth tokens. Every credential issued inside an organization draws from the same 5,000-per-hour bucket. If a nonprofit has multiple integrations (yours plus a marketing tool, plus a finance sync, plus internal scripts) all using their Virtuous organization, all of those callers share a single limit. Plan for less than the full 5,000/hour when sizing a partner integration's request budget.

  See [Rate limit scope](#rate-limit-scope) below for partner-integration implications.
</Warning>

## Rate limit headers

Every API response includes three headers that report your current limit status:

| Header                  | Type      | Description                                                                                            |
| ----------------------- | --------- | ------------------------------------------------------------------------------------------------------ |
| `X-RateLimit-Limit`     | `integer` | Total request limit for the current window (always `5000`).                                            |
| `X-RateLimit-Remaining` | `integer` | Number of requests remaining in the current window.                                                    |
| `X-RateLimit-Reset`     | `integer` | Unix timestamp (seconds) when the current window resets and `X-RateLimit-Remaining` returns to `5000`. |

Monitor `X-RateLimit-Remaining` during high-volume operations so you can slow down before hitting the limit rather than recovering from a `429`.

***

## When you hit the limit

When you exceed 5,000 requests in the current hour, the API returns `429 Too Many Requests`.

<Warning>
  The CRM+ OpenAPI spec documents the rate limit in its description prose but does not declare a `429` response on any individual endpoint. The `429` response can occur on any endpoint. Build your integration to handle `429` on every request, not just on endpoints where it is explicitly documented.
</Warning>

A `429` response includes a `Retry-After` header indicating how many seconds to wait before retrying:

```http theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 847
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1713914400

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "You have exceeded the request rate limit. Retry after 847 seconds.",
    "details": []
  }
}
```

***

## Handling 429 with exponential backoff

When you receive a `429`, respect the `Retry-After` header and wait the indicated number of seconds before retrying. For high-volume integrations, add exponential backoff with jitter on top of the `Retry-After` value to avoid a burst of synchronized retries if multiple workers hit the limit simultaneously.

<CodeGroup>
  ```bash cURL theme={null}
  # cURL-based retry loop. Reads Retry-After from the response headers
  # and sleeps before retrying. Production integrations should use a
  # proper HTTP client with backoff support.
  URL="https://api.virtuoussoftware.com/api/Organization/Current"
  TOKEN="$VIRTUOUS_API_TOKEN"
  MAX_RETRIES=5

  for attempt in $(seq 0 $MAX_RETRIES); do
    RESPONSE=$(curl -s -o /tmp/body.json -D /tmp/headers.txt -w "%{http_code}" \
      "$URL" -H "Authorization: Bearer $TOKEN")

    if [ "$RESPONSE" != "429" ]; then
      echo "Status: $RESPONSE"
      cat /tmp/body.json
      break
    fi

    RETRY_AFTER=$(grep -i "^retry-after:" /tmp/headers.txt | awk '{print $2}' | tr -d '\r')
    RETRY_AFTER=${RETRY_AFTER:-60}
    echo "Rate limited. Waiting ${RETRY_AFTER}s (attempt $attempt)..."
    sleep "$RETRY_AFTER"
  done
  ```

  ```javascript JavaScript theme={null}
  async function fetchWithRetry(url, options = {}, maxRetries = 5) {
    const BASE_DELAY_MS = 1000;
    const MAX_DELAY_MS = 60000;

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      const response = await fetch(url, {
        ...options,
        headers: {
          Authorization: `Bearer ${process.env.VIRTUOUS_API_TOKEN}`,
          'Content-Type': 'application/json',
          ...options.headers,
        },
      });

      if (response.status !== 429) {
        if (!response.ok) {
          throw new Error(`API error: ${response.status}`);
        }
        return await response.json();
      }

      if (attempt === maxRetries) {
        throw new Error('Max retries reached — rate limit persists.');
      }

      // Respect Retry-After header; fall back to exponential backoff
      const retryAfterHeader = response.headers.get('Retry-After');
      const retryAfterMs = retryAfterHeader
        ? parseInt(retryAfterHeader, 10) * 1000
        : Math.min(BASE_DELAY_MS * Math.pow(2, attempt), MAX_DELAY_MS);

      // Add jitter (±20%) to avoid synchronized retries across parallel workers
      const jitter = retryAfterMs * (0.8 + Math.random() * 0.4);

      console.warn(
        `Rate limited. Retrying in ${Math.round(jitter / 1000)}s (attempt ${attempt + 1}/${maxRetries})`
      );
      await new Promise((resolve) => setTimeout(resolve, jitter));
    }
  }
  ```
</CodeGroup>

***

## Staying within limits

**Use bulk and query endpoints.** When you need to fetch or update multiple records, use bulk endpoints — `POST /api/Contact/Query` for Contacts, `POST /api/Gift/Query` for Gifts, `POST /api/Gift/Bulk` for bulk gift writes — rather than calling `GET /api/Contact/{id}` in a loop. Bulk endpoints return many records per request and dramatically reduce your request count.

**Use webhooks for change detection.** Polling for new or updated records burns requests quickly. Subscribe to webhooks instead — Virtuous pushes change events to your endpoint in real time, and webhook deliveries do not count against your hourly limit. See [Webhooks Overview](/crm/webhooks/overview).

**Avoid high-frequency polling.** Do not poll the API more than once per minute for any given resource. If you need near-real-time updates, use webhooks.

**Spread batch jobs across the hour.** If you have a sync job that needs to make 4,000+ requests, spread it over multiple rate-limit windows rather than running all requests in a single burst. This leaves headroom for webhook deliveries and UI-triggered requests that share the same rate limit.

**Monitor remaining capacity.** Read `X-RateLimit-Remaining` from each response and reduce your request rate as you approach zero. Do not wait for a `429` to start slowing down.

<Tip>
  A single `POST /api/Contact/Query` request with `take=1000` returns up to 1,000 Contact records. If your integration needs to sync all Contacts, you can retrieve up to 1.5 million records per hour using a paginated query loop — well above the scale of any typical nonprofit donor database. See [Pagination and Filtering](/crm/pagination) for the iteration pattern.
</Tip>

***

## Rate limit scope

The 5,000-requests-per-hour limit is enforced at the **Virtuous organization** level. Every API Key and every OAuth token issued inside an organization shares the same hourly bucket. The `X-RateLimit-Remaining` header reflects the remaining capacity for the entire organization — not for the specific credential making the call.

This has three implications for partner integrations:

* **A nonprofit's total request budget is shared across all integrations they run.** If a customer uses your integration plus three other tools that call CRM+, all four integrations draw from the same 5,000/hour bucket on that organization. Plan your integration's typical request rate well below the full limit to leave headroom for the customer's other callers.
* **Multiple credentials in your integration don't expand the budget.** Generating a second API Key inside the same organization gives you a second credential, not a second rate-limit bucket. The two keys share the limit. The right reason to use multiple keys is permission-group separation or credential isolation per environment — not capacity scaling.
* **Each nonprofit customer has its own independent budget.** Because each customer is a separate Virtuous organization with separate credentials, a burst in one customer's sync does not consume budget from another customer's calls. This is what makes a partner integration serving many customers viable — the 5,000/hour limit applies per customer, not across your entire customer base.

### Requesting a higher limit

The Virtuous engineering team can grant per-organization rate limit exceptions for legitimate high-volume use cases — for example, an initial historical-data migration or a partner with consistent high-throughput requirements. Exceptions are handled case-by-case and typically scoped to a specific organization or source IP. If your integration's expected request rate exceeds the default budget for a specific customer, route the customer through Virtuous support to discuss an exception.

***

## Cross-API note

The [Raise API](/raise/rate-limits) and [Volunteer API](/volunteer/rate-limits) enforce their own rate limits independently of CRM+. A `429` on one product does not affect your ability to call the others. If your integration spans multiple products, track remaining capacity per product separately.

***

## Next steps

<CardGroup cols={2}>
  <Card title="Error Handling" icon="circle-exclamation" href="/crm/error-handling">
    The error response shape and how to branch on status codes — including the retryable vs. non-retryable matrix.
  </Card>

  <Card title="Pagination and Filtering" icon="list" href="/crm/pagination">
    Use `take=1000` and filtered queries to minimize requests when fetching large result sets.
  </Card>

  <Card title="Webhooks Overview" icon="bell" href="/crm/webhooks/overview">
    Replace polling with real-time webhook deliveries — they do not count against the rate limit.
  </Card>

  <Card title="API Performance Tips" icon="bolt" href="/crm/best-practices/api-performance">
    Patterns for keeping latency low and request counts down at scale.
  </Card>
</CardGroup>
