Skip to content

Rate Limits

The BenchKey API enforces rate limits to protect platform stability and ensure fair usage across tenants. Limits apply per API key.

The API uses a token-bucket model per API key: a burst capacity of 100 requests that refills at 10 requests per second. This allows short bursts up to 100 in-flight requests while sustaining no more than 10 req/s over time. Both live (bk_live_) and test (bk_test_) keys have independent limits.

Every API response includes headers that describe your current usage:

HeaderDescription
RateLimit-LimitBucket capacity (maximum burst size)
RateLimit-RemainingTokens remaining in the bucket right now
RateLimit-ResetSeconds until the bucket is full again
Retry-AfterSeconds to wait before retrying (only present on 429 responses)

Example response headers

HTTP/1.1 200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 87
RateLimit-Reset: 2

If you exceed the limit the API returns 429 Too Many Requests:

{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "API rate limit exceeded for this key.",
"request_id": "req_01J8X4zzzzzz"
}
}

The Retry-After header tells you exactly how many seconds to wait.

For any 429 or transient 5xx response, use exponential backoff with jitter. Do not hammer the API in a tight retry loop.

Terminal window
BENCHKEY_API_KEY="bk_live_acme_xxxxxxxxxxxx"
MAX_ATTEMPTS=5
ATTEMPT=0
BACKOFF=1
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
RESPONSE=$(curl -s -w "\n%{http_code}" \
https://app.benchkey.com/api/v1/customers \
-H "Authorization: Bearer $BENCHKEY_API_KEY" \
-H "BenchKey-Version: 2026-06-13")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | head -n-1)
if [ "$HTTP_CODE" -eq 200 ]; then
echo "$BODY"
break
elif [ "$HTTP_CODE" -eq 429 ] || [ "$HTTP_CODE" -ge 500 ]; then
RETRY_AFTER=$(echo "$BODY" | jq -r '.error.message' | grep -oE '[0-9]+' | head -1)
WAIT=${RETRY_AFTER:-$BACKOFF}
echo "Rate limited or server error. Waiting ${WAIT}s..." >&2
sleep "$WAIT"
BACKOFF=$((BACKOFF * 2))
ATTEMPT=$((ATTEMPT + 1))
else
echo "Fatal error $HTTP_CODE: $BODY" >&2
break
fi
done
async function fetchWithRetry(url, options, maxAttempts = 5) {
let backoff = 1000; // ms
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const res = await fetch(url, options);
if (res.ok) return res.json();
if (res.status === 429 || res.status >= 500) {
const retryAfter = res.headers.get("Retry-After");
const wait = retryAfter ? parseInt(retryAfter, 10) * 1000 : backoff;
const jitter = Math.random() * 200;
await new Promise((r) => setTimeout(r, wait + jitter));
backoff = Math.min(backoff * 2, 30_000);
continue;
}
const { error } = await res.json();
throw new Error(`[${error.code}] ${error.message}`);
}
throw new Error("Max retry attempts reached");
}
// Usage
const data = await fetchWithRetry(
"https://app.benchkey.com/api/v1/customers",
{
headers: {
Authorization: `Bearer ${process.env.BENCHKEY_API_KEY}`,
"BenchKey-Version": "2026-06-13",
},
}
);
console.log(data);
  • Batch reads — use limit=100 (the maximum) to reduce the number of list requests.
  • Cache aggressively — avoid re-fetching resources that haven’t changed. Store the id and re-read only when needed.
  • Parallelize carefully — fan-out requests in parallel, but cap concurrent in-flight requests to avoid burst spikes (aim for ≤ 10 concurrent).
  • Check headers proactively — if RateLimit-Remaining drops near zero, slow down before hitting 429.
  • Use idempotency keys on writes — if a write request times out and you retry, an Idempotency-Key prevents duplicate side effects.

There are no bulk endpoints — deliberately. A batch write is a loop of single, idempotent requests, which gives you per-record success/failure (no partial-batch ambiguity), free crash-restart safety, and honest backpressure:

  1. Send each record with an Idempotency-Key derived from your source ID (e.g. import-cust-8841). A crashed run can restart from the top — already-completed records replay their stored response instead of duplicating.
  2. Pace by the headers. Watch RateLimit-Remaining and ease off as it approaches zero; the default bucket sustains ~10 requests/second indefinitely, so a 5,000-record backfill completes in under 10 minutes.
  3. On 429, wait Retry-After seconds and resend the same request with the same key.

For a full migration from another system (RepairDesk, RepairShopr, CSV), use the in-app importer instead — it handles dedupe, staging review, and per-entity selection in ways a REST loop can’t.


GET /api/v1/usage

Returns a rollup of your tenant’s API request volume, error rate, and latency over a configurable time window. Useful for dashboards, alerting, and diagnosing whether you are approaching rate limits.

Auth required: any valid API key. No specific scope is needed — this endpoint is accessible even if your account is on a plan that gates other resources.

ParameterTypeDefaultDescription
window_daysinteger30Rolling lookback window in days
sinceinteger (epoch ms)Explicit window start (overrides window_days)
untilinteger (epoch ms)Explicit window end (overrides window_days)
by_keybooleanfalseAdd a per-API-key breakdown to the response
limitinteger50Max keys to include when by_key=true
Terminal window
# Last 7 days, whole-tenant rollup
curl "https://app.benchkey.com/api/v1/usage?window_days=7" \
-H "Authorization: Bearer bk_live_<tenantId>_<secret>"
# Explicit window with per-key breakdown
curl "https://app.benchkey.com/api/v1/usage?since=1748822400000&until=1749081600000&by_key=true&limit=20" \
-H "Authorization: Bearer bk_live_<tenantId>_<secret>"
const res = await fetch("https://app.benchkey.com/api/v1/usage?window_days=7", {
headers: { Authorization: `Bearer ${process.env.BENCHKEY_API_KEY}` },
});
const usage = await res.json();
console.log(`${usage.total_requests} requests, error rate ${usage.error_rate}`);
{
"object": "api_usage",
"tenant_id": "acme",
"window": {
"since": "2026-06-07T00:00:00.000Z",
"until": "2026-06-14T00:00:00.000Z"
},
"total_requests": 4821,
"by_status_class": {
"2xx": 4710,
"4xx": 108,
"5xx": 3,
"other": 0
},
"error_count": 111,
"error_rate": 0.023,
"duration_ms": {
"total": 721500,
"avg": 149,
"max": 2340
},
"distinct_keys": 2
}

With by_key=true the response also includes a by_key array — one entry per API key, with the same fields scoped to requests made by that key.

FieldTypeDescription
window.since / window.untilstring (ISO 8601)Resolved window boundaries
total_requestsintegerAll requests in the window
by_status_classobjectCounts broken out by HTTP status class
error_countinteger4xx + 5xx total
error_ratenumbererror_count / total_requests (0–1)
duration_ms.avgnumberMean response time in milliseconds
duration_ms.maxnumberSlowest response in the window
distinct_keysintegerNumber of distinct API keys that made requests

Fresh tenant / no traffic: If no request log data exists yet, the endpoint returns all-zero values rather than an error — safe to poll from dashboards immediately after provisioning.