Guide

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 Requests under 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 429 with a Retry-After header — no custom error envelope.
  • The defensive patterns above will continue to work without changes.

See also

  • Errors429 shape and where it fits in the status-code taxonomy.
  • Paginationper_page strategy for bulk walks.
  • Webhooks — replace polling loops.