Rate Limiting
Per-tier rate limits applied to each API credential, response headers for monitoring, and recommended retry strategies.
Overview
The Simply360 API enforces rate limits to ensure fair usage and platform stability. Limits are applied per credential — per API key, per user, or per OAuth client — within each team, and are tracked in fixed time windows. Session tokens share the counters of the API key that created them, so minting many session tokens does not increase your quota.
Different kinds of operations have different limits:
| Tier | Applies to | Limit |
|---|---|---|
| Read | GET requests |
300 requests per minute |
| Write | POST, PUT, PATCH, DELETE requests |
60 requests per minute |
| Search | Search endpoints (paths containing /search) |
30 requests per minute |
| Bulk | Batch and bulk endpoints (paths containing /batch or /bulk) |
10 requests per minute |
| Export | Export endpoints (paths containing /export) |
5 requests per 5 minutes |
In addition, an IP-based limit of 600 requests per minute applies before authentication, and authentication endpoints (sign-up, password reset) have stricter per-IP limits to protect against abuse.
Response Headers
Authenticated API responses include rate-limit headers so you can monitor your usage in real time:
| Header | Description | Example |
|---|---|---|
X-RateLimit-Limit |
Maximum requests allowed in the current window for this tier. | 300 |
X-RateLimit-Remaining |
Requests remaining in the current window. | 287 |
X-RateLimit-Reset |
Unix timestamp (seconds) when the current window resets. | 1709726520 |
Reading Headers
const response = await fetch(
'https://api.simply360.app/v1/data-records?dataCollectionSimplyId=XXXX-XXXX-XXXX',
{ headers: { Authorization: `Bearer ${apiKey}` } },
);
const limit = response.headers.get('X-RateLimit-Limit');
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');
console.log(`${remaining}/${limit} requests remaining, resets at ${new Date(Number(reset) * 1000).toISOString()}`);
Handling 429 Responses
When you exceed a rate limit, the API returns 429 Too Many Requests with the standard error envelope. Use the X-RateLimit-Reset header to determine when the window resets.
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Maximum 300 requests per 60 seconds."
},
"meta": { "requestId": "req_abc123" }
}
Exponential Backoff with Jitter
The recommended strategy for handling rate limits is exponential backoff with jitter. This prevents thundering-herd retry storms when many clients are limited at once. The Simply360 SDK does not retry automatically — implement the strategy in your own code:
async function fetchWithBackoff(
url: string,
options: RequestInit,
maxRetries = 3,
): Promise<Response> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) return response;
if (attempt === maxRetries) throw new Error(`Rate limited after ${maxRetries} retries`);
// Prefer the reset timestamp when provided; fall back to exponential delay.
const resetHeader = response.headers.get('X-RateLimit-Reset');
const resetDelayMs = resetHeader ? Number(resetHeader) * 1000 - Date.now() : NaN;
const baseDelay = Number.isFinite(resetDelayMs) && resetDelayMs > 0
? resetDelayMs
: Math.pow(2, attempt) * 1000;
const jitter = Math.random() * baseDelay * 0.5;
await new Promise((resolve) => setTimeout(resolve, baseDelay + jitter));
}
throw new Error('Unreachable');
}
If you are using the SDK, catch the ApiError with statusCode === 429 and apply the same logic before retrying the SDK call. See Error Handling for the full error model.
Best Practices
Monitor Your Usage
Watch X-RateLimit-Remaining on every response. If it drops faster than expected, look for unnecessary calls in your code.
Use the Largest Reasonable Page Size
List endpoints support limit up to 100. Fewer requests — lower bill against your quota. See Pagination, Sorting & Field Selection.
// Efficient: 100 records per page, advancing manually with offset.
let offset = 0;
const limit = 100;
while (true) {
const response = await s360.dataRecords.list({ dataCollectionSimplyId, limit, offset });
// process response.data...
if (!response.meta.pagination.hasMore) break;
offset += limit;
}
Use Batch Endpoints for Bulk Writes
When creating or updating many records, use the batch endpoints instead of individual calls. Batch endpoints are limited to 10 requests per minute, but each request can carry up to 100 records — up to 1,000 record writes per minute, versus 60 individual write calls:
await s360.dataRecords.batchCreate({
dataCollectionSimplyId: 'XXXX-XXXX-XXXX',
records: items.map((item) => ({
fields: { 'FLDS-NAME': item.name, 'FLDS-EMAL': item.email },
})),
});
Cache Responses
Cache read responses on your side when the data doesn't change frequently. Collection metadata, field definitions, and other configuration data are good candidates.
Spread Requests Over Time
For batch jobs, spread requests evenly rather than bursting:
async function processInBatches<T>(
items: T[],
processFn: (item: T) => Promise<void>,
delayMs = 200,
): Promise<void> {
for (const item of items) {
await processFn(item);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
Use Field Selection
Request only the fields you need with the fields query parameter. Smaller responses are faster to process and reduce server work, helping you stay within limits.
Error Reference
| HTTP Status | Error Code | Description |
|---|---|---|
429 |
RATE_LIMIT_EXCEEDED |
You've exceeded the quota for this tier. Wait until the window resets (see X-RateLimit-Reset) before retrying. |