Rate limits
Trybe does not currently enforce a published rate limit on API requests. The API will serve as many requests as your token can generate, subject to the usual constraints — server capacity, database contention, and fair-use review if a single integration causes degradation for others. We may introduce enforced limits in future; this page tells you how to build now so that change is a no-op for your code.
What this means today
- There is no fixed per-second or per-minute cap published against any endpoint.
- The API does not currently return
429 Too Many Requestsunder normal operation. - There are no
X-RateLimit-*headers on responses.
That's not licence to hammer the API. Sustained high-volume traffic against shared infrastructure causes real customer impact, and we reserve the right to throttle individual tokens — informally first, then with enforced limits — if a single integration is starving the rest. Build defensively from day one and the limits, when they arrive, won't break you.
Patterns to build now
These are the patterns every well-behaved API consumer ships regardless of whether limits are enforced. Adopting them now is cheap; retrofitting them under fire is not.
Retry on 429 with Retry-After
If you do see a 429 response, honour the Retry-After header before retrying.
The header is either an integer number of seconds or an HTTP date.
async function withRateLimitRetry(fn, maxAttempts = 5) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const res = await fn()
if (res.status !== 429) return res
const retryAfter = res.headers.get('Retry-After')
const waitSec = retryAfter
? Number.isFinite(+retryAfter)
? +retryAfter
: Math.max(0, (new Date(retryAfter).getTime() - Date.now()) / 1000)
: 2 ** attempt // 2, 4, 8, 16…
await new Promise(r => setTimeout(r, waitSec * 1000))
}
throw new Error('Rate-limit retries exhausted')
}
If Retry-After is absent (or you're retrying a non-429 transient error), fall
back to exponential backoff with jitter.
Exponential backoff with jitter
A sleep that doubles per attempt, with a random component to avoid the thundering-herd problem when multiple workers retry in lockstep.
function backoffMs(attempt) {
const base = 500 // start at 0.5s
const cap = 30_000 // never wait more than 30s
const exp = Math.min(cap, base * 2 ** (attempt - 1))
return Math.random() * exp // full jitter
}
Apply this to 5xx responses and network errors too — see
errors.
Limit concurrency
A single HTTP client with unbounded concurrency will happily open a thousand sockets if your code asks for it. Cap concurrency at the client level — eight or sixteen in-flight requests is usually plenty for bulk jobs, and keeps you inside whatever limits we ship later.
import pLimit from 'p-limit'
const limit = pLimit(8) // at most 8 in-flight calls
const sites = await Promise.all(
siteIds.map(id => limit(() => client.get(`/sites/${id}`)))
)
For PHP, Guzzle's pool helper does the same job:
$pool = new GuzzleHttp\Pool($client, $requests, [
'concurrency' => 8,
'fulfilled' => fn ($response, $index) => $results[$index] = $response,
]);
$pool->promise()->wait();
Batch instead of looping
Where the API exposes a batch or list endpoint, use it. One request that returns
fifty rows is cheaper for both sides than fifty individual GET /sites/{id}
calls. The endpoint reference flags batch and bulk endpoints explicitly.
When walking a list, pick the largest per_page the endpoint allows (see
pagination) — fewer pages, fewer round-trips.
Use webhooks instead of polling
The fastest way to overspend on API requests is to poll for state changes that the API will tell you about for free. Anything that goes through an order, appointment, customer or membership lifecycle has webhook events — register an endpoint, react to the event, skip the poll loop entirely.
A common anti-pattern: poll GET /shop/appointments?status=settled every 30
seconds to learn when an appointment is checked out. Replace it with the
basket.settled webhook and your request volume drops to zero between actual
events.
Cache reference data
Sites, brands, currencies, locales, treatment types — anything that changes rarely — should be cached locally, not refetched on every request. A short TTL (a few minutes) gives you near-real-time freshness without hammering the same endpoints.
What we promise
- We will announce enforced limits before turning them on, with a window long enough to adjust integrations.
- When limits do arrive, the response shape will be the conventional
429with aRetry-Afterheader — no custom error envelope. - The defensive patterns above will continue to work without changes.
See also
- Errors —
429shape and where it fits in the status-code taxonomy. - Pagination —
per_pagestrategy for bulk walks. - Webhooks — replace polling loops.