Skip to main content
A slow integration is a bad integration. Reports that take ten minutes to load, syncs that lag hours behind, dashboards that fall over under modest customer growth — all are symptoms of integration code that doesn’t respect the API’s grain. This page covers the practical techniques that keep Raise integrations fast, efficient with rate-limit budget, and pleasant to operate. The audience is integration engineers building or optimizing Raise-backed integrations. The techniques apply equally to small integrations and large ones; the larger the integration, the more important they become.

The performance principles

Six principles, in rough order of impact: The next sections drill into each.

Principle 1: webhooks over polling

For any change-detection workflow — syncing new gifts, reacting to donor updates, processing recurring failures — webhooks deliver the change within seconds and consume zero rate-limit budget for the detection itself. Polling at any reasonable cadence is strictly worse.

The polling trap

A typical naive polling pattern:
JavaScript
Problems with this:
  • 288 requests per day per customer just for change detection. For a partner with 100 customers, that’s 28,800 daily requests producing no user-facing value.
  • 5-minute lag between gift creation and detection.
  • Edge cases at the boundaries — gifts created exactly between polls can be missed or double-counted.
  • No insight into deletions or updates unless you also poll modification timestamps.

The webhook alternative

JavaScript
Now change detection is push-based:
  • One subscription per customer, zero ongoing rate-limit cost for the detection.
  • Seconds-level latency instead of minutes.
  • All event types covered — creates, updates, deletes — with no extra work.
  • Edge cases handled by Raise — retries, ordering, etc.

When polling is still appropriate

Three legitimate cases for polling: For these cases, throttle aggressively and minimize request count. See Reconcile with CRM+: pattern 3 for the reconciliation pattern.

Principle 2: use the largest page size

For paginated reads, each request consumes one rate-limit slot regardless of how many records it returns. A Take=1000 request returns 1,000 records for the cost of one request; a Take=25 request returns 25.

When this matters most

For partner integrations doing bulk reads, default to Take=1000 and only reduce if a specific endpoint enforces a lower maximum. The pagination loop is otherwise identical.

A bulk read pattern

JavaScript
100,000 gifts read with Take=1000 produces 100 requests. The same read with Take=100 produces 1,000 requests. The same data, 10× the rate-limit cost.

Principle 3: cache reference data

Some Raise resources change rarely — Campaigns, Projects, MotivationCodes, Premiums, CustomFields. Caching these eliminates the majority of read traffic for analytics and reporting integrations.

What to cache

A reference-data cache pattern

JavaScript
For an integration that processes 1,000 gifts and needs to look up the Campaign name for each, this is the difference between 1,000 Campaign fetches and 1 (cached for the rest of the hour).

Cache invalidation strategies

For most reference data, time-based with a 1-hour TTL is the right default. The occasional stale lookup is acceptable; the rate-limit savings are substantial.

Query options caching

The QueryOptions discovery endpoint (GET /api/Query/options/{queryType}) returns the integer-to-label mapping for query operators and filter parameters. This effectively never changes — cache it at integration startup and refresh once a day at most:
JavaScript
See Pagination and Filtering: Discovering query options.

Principle 4: push filters into the API

A query that returns 500 records after filtering is much cheaper than one that returns 50,000 records the integration then filters client-side.

Filter at the source

Bad pattern — pull everything, filter in code:
JavaScript
Good pattern — filter at the API:
JavaScript
The second pattern reads only the records that match. For a customer with 50,000 historical gifts and 5,000 recent ones, the second pattern is 10× cheaper.

Combine multiple filters

Filters compose with AND semantics within a condition group. Combining narrows the result set further:
JavaScript
Three filters compose to identify recent ($100+) production gifts — one query that returns only the matches.

Use /list + Filter for free-text matching

For interactive search where the user types a query string, the simple Filter parameter on /list is cheap and fast:
JavaScript
Don’t fall back to POST /api/Donor/query for what’s essentially a search-as-you-type lookup — /list with Filter handles it with less overhead.

Principle 5: use IncludeDetails=false for bulk reads

IncludeDetails=true includes related entities (addresses, contact methods, embedded donor on gifts, etc.) in each response item. For bulk reads, this can multiply the payload size 5–10×. The Raise spec calls this out explicitly on POST /api/Donor/query:
When includeDetails=true, the response includes all related entities (DonorAddresses, DonorContactMethods) similar to the GET by ID endpoint. This may impact performance for large result sets.

Use it sparingly

Default to IncludeDetails=false. Switch to true only when the workflow demonstrably needs the related entities and the result set is small (typically a few hundred records at most).

When you do need details on many records

For workflows that genuinely need full records across many donors, fetch them individually rather than as a bulk read:
JavaScript
This is more requests than one bulk query with IncludeDetails=true, but each is small and the workload is more controlled. For very large workloads, prefer the controlled-concurrency pattern over the bulk-with-details one.

Principle 6: choose the right endpoint

Different endpoints have different costs for the same logical question. Some examples:

Donor’s gifts: scoped endpoint vs. filtered query

The scoped endpoint is cheaper because the filter happens at the source rather than requiring full-table filter evaluation.

Donor lookup by email: search vs. query

/search is optimized for free-text lookups and is faster than the structured query for single-record matches.

Reading reference data: list vs. query

For unfiltered bulk reads, /list is cheaper than /query.

Reading a single record: get-by-ID vs. filtered query

Always prefer get-by-ID when you have the ID. The general principle: when multiple endpoints could answer the same question, choose the most specific one. Specificity translates to less work at the API and faster responses.

Avoiding common performance traps

A few specific anti-patterns that cause performance issues:

Sequential fetches when parallel would do

JavaScript
JavaScript
For tens or hundreds of fetches, parallelism reduces total wall-clock time significantly. Cap concurrency to stay within rate limits — see Rate Limits for the broader pattern.

Repeated lookups of the same data

JavaScript
JavaScript
Cache reference data once per workload (or for the integration’s lifetime with TTL) and look up from memory.

Synchronous webhook processing

JavaScript
JavaScript
A slow webhook handler causes Raise timeouts and triggers retries — producing duplicate deliveries the integration then has to deduplicate. Acknowledge first, process after.

Fetching unnecessary fields with selectedColumns

When you don’t need every field, request a subset:
JavaScript
For workflows that only need a handful of fields, selectedColumns reduces response payload by an order of magnitude. The set of valid columns is discovered via QueryOptions.

Measuring performance in production

For partner integrations operating at scale, instrument for visibility:

Track per-endpoint latency

JavaScript
Per-endpoint latency reveals which endpoints are slow under your customer’s data shape, and helps catch regressions.

Track request count per customer

JavaScript
A customer with a spike in request volume may indicate either a new high-traffic workflow or an integration bug producing extra requests. Both are worth investigating.

Track cache hit rate

JavaScript
A reference-data cache with a 99% hit rate is doing its job. A 30% hit rate suggests the cache’s TTL is too short or the cache key isn’t matching what the code requests.

Alert on rate-limit responses

If 429 responses occur (the spec doesn’t formally document them but they happen), alert immediately:
JavaScript
A sustained rate-limit pattern indicates the integration’s traffic exceeds the customer’s budget — investigate which workflow is the culprit and apply the techniques on this page.

A performance checklist

Run through this when building or auditing an integration:
  • Change-detection uses webhooks, not polling, wherever possible
  • Bulk reads use Take=1000
  • Reference data (Campaigns, Projects, MotivationCodes, CustomFields, QueryOptions) is cached with appropriate TTLs
  • Filters are applied in the API request, not in client-side filtering of large result sets
  • Bulk reads use IncludeDetails=false
  • selectedColumns reduces payload when only a few fields are needed
  • Donor-scoped endpoints are used for donor-scoped reads (not filtered queries)
  • Get-by-ID is used when the ID is known (not filtered queries)
  • Concurrent fetches use a controlled-concurrency pool
  • Webhook handlers acknowledge fast and process async
  • Latency, request count, cache hit rate, and rate-limit responses are instrumented
Most of these are small individually; together they make the difference between an integration that scales gracefully and one that hits walls.

Where to go next

Error Recovery Patterns

What to do when the techniques on this page meet the reality of transient failures.

Rate Limits

The throttling patterns that pair with the performance practices.

Pagination and Filtering

The reference for the pagination, filtering, and selectedColumns mechanics used here.

Sync Architecture Patterns

The broader architectural patterns these performance techniques fit into.
Last modified on May 21, 2026