General

Rate Limiting

This section provides a technical overview of the rate limiting policies for Pushpay's Church Management System (ChMS) API products. Our rate limiting strategy ensures that all partners receive consistent, high-performance access to their data while protecting the stability of the ecosystem.


Core Concepts

The system uses two primary methods for managing request traffic:

  • Rate Limit (Sustained): The average number of requests allowed per second over a sustained period. Think of this as your "speed limit" for steady-state usage.
  • Burst Limit: The maximum number of concurrent requests that can be handled in a short spike. This allows for temporary bursts in traffic that exceed the sustained rate.

ChMS V2 API (JSON-REST)

The ChMS V2 API utilizes a per-endpoint rate and burst limit model. Unlike V1, V2 does not enforce a daily request limit, allowing for more flexibility in high-volume integrations. Limits are assigned per API client at the Campus or Parish level, if each campus/parish uses their own API credentials.

Default Configuration

Limit TypeDefault ValueLimit ByShared Across
Per-Second Rate1 request/secPer endpointPer API client
Burst Limit100 requestsPer endpointPer API client

Endpoint-Specific Overrides

Certain high-intensity or sensitive endpoints have more restrictive limits and do not allow for a burst bucket. These overrides protect against data exfiltration and performance degradation:

EndpointRate LimitBurst LimitApplies To
/individuals/{id}1 request/secNonePer API client
/scheduling/categories/{id}/schedules1 request / 2 secNonePer API client
/search/individuals/results1 request / 5 secNonePer API client

Note: These overrides are designed to prevent abuse of bulk-fetch operations and complex search queries.


ChMS V1 API (Legacy XML)

The V1 API combines a daily call cap with per-endpoint limits. It is scoped per api_user credential.

Limit TypeValueLimit ByShared Across
Daily Call Cap10,000 requestsPer api_userPer API user
Per-Second Rate1–10 requests/secPer endpointPer organization
Burst Limit60–100 requestsPer endpointPer organization

Legacy Status: V1 is maintained for backward compatibility. All new integrations should use V2.

Note: The daily call cap is 10,000 requests per api user. The per-second rate and burst limits are shared across the organization, across all api users. So, it is possible for one API user to consume the per-second rate limit and prevent another API user from consuming that endpoint.


Understanding Rate Limit Headers

To help you meter your usage programmatically, every API response includes HTTP headers indicating your current rate limit status.

Successful Requests (HTTP 200)

A successful call will return these headers to help you plan subsequent requests:

  • x-ratelimit-limit: The total burst capacity (number of calls) allowed in the current window.
  • x-ratelimit-remaining: The number of calls remaining in the current window before hitting your limit.
  • x-ratelimit-reset: The UTC epoch timestamp when the current window resets and your capacity refills.

Rate Limited Requests (HTTP 429)

If you exceed your limits, the API returns an HTTP 429 Too Many Requests error. The following headers are provided:

  • retry-after: The number of seconds to wait before retrying the request. Always respect this value.
  • x-ratelimit-limit: Your burst capacity (for reference).
  • x-ratelimit-remaining: Will be 0 when rate limited.
  • x-ratelimit-reset: The UTC epoch timestamp when your quota resets.

Implementation Example (ChMS V2)

Fetching Individual Profiles

Here's a practical example of working with the /individuals endpoint:

Initial Request:

curl -H "Authorization: Bearer [TOKEN]" \
  "https://api.ccbchurch.com/individuals?per_page=25"

Response Headers (HTTP 200):

HTTP/2 200
Content-Type: application/json; charset=utf-8
x-ratelimit-limit: 100
x-ratelimit-remaining: 99
x-ratelimit-reset: 1740700800

Handling Rapid Requests

If you exhaust the x-ratelimit-remaining count before the x-ratelimit-reset time, subsequent requests will receive:

HTTP/2 429
Content-Type: application/json; charset=utf-8
retry-after: 5
x-ratelimit-limit: 100
x-ratelimit-remaining: 0
x-ratelimit-reset: 1740700805

The response body typically includes:

{
  "error": "Rate limit exceeded",
  "retry_after": 5
}

Best Practices

1. Respect the Reset Headers

For predictable, scheduled workloads (like overnight data syncs), your application should ideally wait until the x-ratelimit-reset time before making the next request. This ensures maximum throughput without ever hitting a 429 error.

Example:

const resetTime = parseInt(response.headers['x-ratelimit-reset']) * 1000;
const waitMs = resetTime - Date.now();
setTimeout(() => retryRequest(), waitMs);

2. Implement Exponential Backoff

If you do receive an HTTP 429, do not immediately retry. Respect the retry-after header value. If multiple 429s occur in succession, implement exponential backoff to avoid overwhelming the API:

let retryCount = 0;
const maxRetries = 5;

async function requestWithBackoff() {
  try {
    return await makeRequest();
  } catch (error) {
    if (error.status === 429 && retryCount < maxRetries) {
      const waitTime = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s, 8s, 16s
      retryCount++;
      await new Promise(resolve => setTimeout(resolve, waitTime));
      return requestWithBackoff();
    }
    throw error;
  }
}

3. Cache Infrequently Changing Data

For data that changes infrequently (such as Campus lists, Custom Field definitions, or Membership Types), cache the JSON response locally. Only query the API when your local cache expires or is explicitly invalidated:

const cacheDuration = 3600000; // 1 hour
let cachedPromise = null;
let cacheTime = 0;

function getCampuses() {
  const now = Date.now();

  // 1. If we have a pending or resolved promise and it hasn't expired, return it
  if (cachedPromise && (now - cacheTime < cacheDuration)) {
    return cachedPromise;
  }

  // 2. Otherwise, initiate the API call and store the PROMISE, not just the data
  cachedPromise = api.get('/campuses').catch(error => {
    // 3. If the API call fails, clear the cache so the next call tries again
    cachedPromise = null;
    throw error; 
  });
  
  cacheTime = now;
  
  return cachedPromise;
}

4. Batch Operations When Possible

Use pagination parameters (per_page, page) to fetch data in controlled chunks rather than requesting a single record repeatedly. This is more efficient than making many sequential individual requests:

# Efficient: Paginate through results
curl "https://api.ccbchurch.com/individuals?page=1&per_page=100"
curl "https://api.ccbchurch.com/individuals?page=2&per_page=100"

# Inefficient: One-by-one requests
curl "https://api.ccbchurch.com/individuals/16688"
curl "https://api.ccbchurch.com/individuals/6741"
# ... repeat hundreds of times

5. Monitor Rate Limit Metrics

Log and monitor rate limit headers in your application. Set up alerts if you're approaching limits consistently:

function logRateLimitStatus(response) {
  // 1. Safely extract headers using .get() (assuming Fetch API)
  // If using Axios, revert to: response.headers['header-name']
  const remainingStr = response.headers.get('x-ratelimit-remaining');
  const limitStr = response.headers.get('x-ratelimit-limit');
  const resetStr = response.headers.get('x-ratelimit-reset');

  // 2. Bail out early if the headers don't exist on this response
  if (!remainingStr || !limitStr || !resetStr) {
    return; 
  }

  // 3. Explicitly parse strings to numbers
  const remaining = parseInt(remainingStr, 10);
  const limit = parseInt(limitStr, 10);
  
  // Note: Multiplying by 1000 is correct. The API sends the reset time in seconds (Unix Epoch).   
  const resetTime = new Date(parseInt(resetStr, 10) * 1000);
  
  console.log(`Rate limit: ${remaining}/${limit} remaining (resets at ${resetTime.toLocaleTimeString()})`);
  
  // Alert if we're using more than 80% of capacity
  if (remaining / limit < 0.2) {
    console.warn('Approaching rate limit threshold!');
  }
}

6. Avoid API Abuse

Excessive HTTP 429 responses are monitored. Applications that consistently ignore rate limit headers and "spam" the API may have their access temporarily or permanently revoked to protect the system. Never attempt to:

  • Bypass limits by requesting massive amounts of data in a single call (causes timeouts)
  • Hammer an endpoint with rapid-fire requests
  • Ignore retry-after headers and immediately re-request
  • Accumulate 429 errors without implementing backoff logic

Pagination

The ChMS V2 API returns large result sets in paginated chunks to maintain performance and respect rate limits. Proper pagination implementation is essential for efficiently syncing large datasets.

Understanding Pagination Headers

Every list endpoint response includes pagination metadata in the response headers:

HeaderDescriptionExample
x-totalTotal number of records across all pages14994
x-total-pagesTotal number of pages available600
x-pageCurrent page number (1-indexed)1
x-per-pageNumber of records per page25
x-offsetZero-based offset into the full result set0
x-next-pageNext page number (or empty if on last page)2

Basic Pagination with /individuals Endpoint

The /individuals endpoint supports page and per_page query parameters:

Request (Page 1, 25 items per page):

curl -H "Authorization: Bearer [TOKEN]" \
  "https://api.ccbchurch.com/individuals?page=1&per_page=25"

Response Headers:

HTTP/2 200
x-total: 14994
x-total-pages: 600
x-page: 1
x-per-page: 25
x-offset: 0
x-next-page: 2

This tells you there are 14,994 total individuals across 600 pages when paginating by 25 records per page.

Choosing Page Size

The per_page parameter accepts values of 25, 50, 75, or 100:

# Smaller chunks: Lower bandwidth per request, more total requests
curl "https://api.ccbchurch.com/individuals?per_page=25"

# Larger chunks: Higher bandwidth per request, fewer total requests
curl "https://api.ccbchurch.com/individuals?per_page=100"

Trade-offs:

  • Larger per_page (75-100): Fewer API calls, faster syncs, but each response takes longer to process
  • Smaller per_page (25-50): More API calls, slower overall, but smaller individual payloads and lighter processing

For most use cases, 50-75 items per page provides good balance.


Pagination Best Practices

  1. Always use the header values, not hardcoded assumptions about total pages
  2. Cache the x-total value at the start of a sync—it may change during long-running operations
  3. Implement checkpoints to resume interrupted syncs using x-offset
  4. Respect the 1 request/sec rate limit when paginating sequentially
  5. Use modest per_page values (50-100) to avoid timeout on very large result sets
  6. Log pagination metrics for debugging and monitoring:
    • Pages processed
    • Errors encountered per page
    • Time per page
  7. Filter when possible using query parameters (e.g., ?include_inactive=false) to reduce page counts

Pagination with Filters and Search

Pagination headers apply to filtered results too. When combining pagination with filters, the headers reflect the filtered dataset:

# Get only active individuals, paginated
curl "https://api.ccbchurch.com/individuals?include_inactive=false&page=1&per_page=50"

# Search for individuals by name, paginated
curl "https://api.ccbchurch.com/individuals?name=John&page=1&per_page=50"

The x-total will reflect only matching records, not the full database.