Skip to main content
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.
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 below for partner-integration implications.

Rate limit headers

Every API response includes three headers that report your current limit status:
HeaderTypeDescription
X-RateLimit-LimitintegerTotal request limit for the current window (always 5000).
X-RateLimit-RemainingintegerNumber of requests remaining in the current window.
X-RateLimit-ResetintegerUnix 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.
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.
A 429 response includes a Retry-After header indicating how many seconds to wait before retrying:
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.
# 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

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. 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.
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 for the iteration pattern.

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 and Volunteer API 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

Error Handling

The error response shape and how to branch on status codes — including the retryable vs. non-retryable matrix.

Pagination and Filtering

Use take=1000 and filtered queries to minimize requests when fetching large result sets.

Webhooks Overview

Replace polling with real-time webhook deliveries — they do not count against the rate limit.

API Performance Tips

Patterns for keeping latency low and request counts down at scale.
Last modified on May 27, 2026