Rate Limiting Policy

Rate Limiting Policy

Requests to the Candis API are subject to two independent limits: a request limit, which
applies to every endpoint, and a record limit, which applies to core data import endpoints.
On import endpoints both are evaluated and a request is rejected when either is exceeded.

Limits are applied at the organization level rather than per client or per API key. Where
several integrations import into the same organization, a single allowance is shared between them.
Organizations belonging to a holding are additionally subject to a shared holding allowance.

Request limit

ScopeLimit
Organization500 requests per minute
Holding2,500 requests per minute

This limit counts requests irrespective of their contents and applies to every Candis API
endpoint
— exports, files, invoices, purchase requests and reimbursement items as well as core
data imports.

Every successful response reports the request-limit state in the X-RateLimit-Limit-organization,
X-RateLimit-Remaining-organization and X-RateLimit-Reset-organization headers, with -holding
counterparts when the organization belongs to a holding. X-RateLimit-Reset is the number of
seconds until the window resets, not a Unix timestamp.

Record limit

Import endpoints are additionally metered by the number of records contained in the request
body, as each record generates one unit of downstream processing. A maximum of 100 records may
be submitted per request.

EndpointPer organizationPer holding
Contacts600 records per minute (burst 1,000)900 records per minute (burst 1,500)
All other core data types3,000 records per minute (burst 5,000)4,800 records per minute (burst 8,000)

Contacts are subject to a lower allowance because contact records require more downstream
processing than other core data types.

Allowance replenishment

The record allowance is replenished continuously rather than reset at fixed intervals. Submitted
records reduce the available balance; elapsed time restores it at the sustained rate, up to the
burst ceiling stated above.

Two properties follow. No window boundary applies, so there is no fixed time at which the allowance
resets. The balance does not accumulate beyond the ceiling, so an extended idle period confers no
greater allowance than a short one.

Example — contacts, 100 records per request

ConditionResult
Balance at ceiling following an idle period1,000 records available
10 consecutive requests submittedAll accepted
An 11th request submitted immediatelyRejected, with Retry-After: 10
A request submitted after 10 secondsAccepted; 100 records have been replenished
Submission continued at that rateSustained throughput of one request per 10 seconds
Submission suspended for 100 secondsBalance restored to the 1,000-record ceiling

Effective limits by batch size

The record limit is the binding constraint for batch imports. The request limit becomes binding only
where requests contain few records.

Records per requestContactsAll other types
100 (maximum)10 requests, then 1 per 10 seconds50 requests, then 1 per 2 seconds
10100 requests, then 1 per second500 requests, then the request limit binds
1Request limit applies: 500 per minuteRequest limit applies: 500 per minute

Each value indicates the number of requests accepted immediately from a full balance, followed by
the sustained rate thereafter.

*At 10 records per request the initial 500-request burst also reaches the 500-per-minute request
limit, so continuing before that minute has elapsed triggers its 60-second lockout. Submitting at
the sustained 5 per second stays below the request limit, and is bound only by the record limit.

Note that the two limits interact. Where the record allowance would permit more than 500 requests
in a minute, the request limit binds first and no further requests are accepted until its lockout
(described below) expires, regardless of remaining record allowance.

Exceeded limits

A request exceeding either limit receives a 429 Too Many Requests response with a
Retry-After header giving the number of whole seconds to wait. What that interval
represents differs by limit:

Limit exceededRetry-After represents
Record limitThe time needed to replenish enough allowance for this request
Request limitThe time remaining on the lockout

The response body's limit field (request or record) identifies which limit rejected the
request, and its scope field (organization or holding) identifies whose allowance was
exhausted, so neither needs to be inferred from the message text.

The two behave differently once exceeded. The record allowance replenishes continuously, so
capacity returns gradually. Exceeding the request limit locks the caller out for a full
60-second window (the standard window length), measured from the moment the limit was exceeded

— not merely the remainder of the current window — rather than admitting requests as capacity
frees up, so retrying early does not succeed (though it does not extend the lockout either). In
both cases waiting the stated interval is the fastest route to a successful retry.

Rejected requests leave no import state behind in either case. No records are imported, no partial
import is recorded, and no import history entry is created. The identical request body may be
resubmitted once the Retry-After interval has elapsed; no deduplication or reconciliation is
required.

Rejected by the record limit:

HTTP/1.1 429 Too Many Requests
Retry-After: 10

{
  "errorCode": "TOO_MANY_REQUEST",
  "message": "Import record budget exhausted. Retry after 10 seconds.",
  "limit": "record",
  "scope": "organization",
  "requestId": "..."
}

Rejected by the request limit:

HTTP/1.1 429 Too Many Requests
Retry-After: 42

{
  "errorCode": "TOO_MANY_REQUEST",
  "message": "Too Many Requests",
  "limit": "request",
  "scope": "organization",
  "requestId": "..."
}

Implementation guidance

  1. Observe the Retry-After header. It is returned on every 429 and is derived from the
    actual remaining interval rather than a fixed backoff schedule, so a retry submitted before it
    has elapsed is rejected again. Exponential backoff is neither required nor helpful for these
    responses and will delay completion unnecessarily.
  2. Base pacing on the header rather than on the published values. The limits stated above are
    current values and are subject to change. Integrations that pace on Retry-After remain
    compliant if they are adjusted.
  3. Submit full batches. A request containing 100 records consumes the allowance considerably
    more efficiently than 100 requests containing one record each.
  4. Account for concurrent importers. Where several processes import into the same organization
    the allowance is shared, and repeated pacing should be expected. Waiting the stated interval
    admits the retry unless another request consumes the replenished allowance first.
  5. Submit large migrations at a steady rate. A one-off import of several hundred thousand
    records will be paced. Steady submission completes in the same total time as submission in
    concentrated bursts, with fewer rejections.

Import status

Requests to the import status endpoint contain no records and are subject to the request limit only.


Did this page help you?