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 Type | Default Value | Limit By | Shared Across |
|---|---|---|---|
| Per-Second Rate | 1 request/sec | Per endpoint | Per API client |
| Burst Limit | 100 requests | Per endpoint | Per 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:
| Endpoint | Rate Limit | Burst Limit | Applies To |
|---|---|---|---|
/individuals/{id} | 1 request/sec | None | Per API client |
/scheduling/categories/{id}/schedules | 1 request / 2 sec | None | Per API client |
/search/individuals/results | 1 request / 5 sec | None | Per 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 Type | Value | Limit By | Shared Across |
|---|---|---|---|
| Daily Call Cap | 10,000 requests | Per api_user | Per API user |
| Per-Second Rate | 1–10 requests/sec | Per endpoint | Per organization |
| Burst Limit | 60–100 requests | Per endpoint | Per 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 be0when 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: 1740700800Handling 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: 1740700805The 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 times5. 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-afterheaders 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:
| Header | Description | Example |
|---|---|---|
x-total | Total number of records across all pages | 14994 |
x-total-pages | Total number of pages available | 600 |
x-page | Current page number (1-indexed) | 1 |
x-per-page | Number of records per page | 25 |
x-offset | Zero-based offset into the full result set | 0 |
x-next-page | Next page number (or empty if on last page) | 2 |
Basic Pagination with /individuals Endpoint
/individuals EndpointThe /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: 2This 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
- Always use the header values, not hardcoded assumptions about total pages
- Cache the
x-totalvalue at the start of a sync—it may change during long-running operations - Implement checkpoints to resume interrupted syncs using
x-offset - Respect the 1 request/sec rate limit when paginating sequentially
- Use modest
per_pagevalues (50-100) to avoid timeout on very large result sets - Log pagination metrics for debugging and monitoring:
- Pages processed
- Errors encountered per page
- Time per page
- 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.
