Skip to content

Get started

Rate limits

What to expect under load, how to detect throttling, and how to back off correctly.

No published global limit

OnCore does not publish numeric global limits for the Core API. The public OpenAPI contract declares 429 responses only for the WhatsApp onboarding cases below, and one further limit is documented on this page without a figure. Do not design around an assumed requests-per-minute figure; use the retry behavior on this page.

What is documented today#

Two endpoints return 429 with a documented cooldown inside the error envelope. Both come from Meta's WhatsApp platform rather than OnCore itself:

CodeEndpointCooldown field
WHATSAPP_RESEND_TOO_SOONResend the verification calldetails.retryAfterSeconds
WHATSAPP_OTP_REQUEST_LIMITStart or resume a signupdetails.retryAfterHours

See WhatsApp onboarding for the full lifecycle.

One OnCore-side limit is also documented, and its shape is different. Minting a live-call monitor session is rate limited per tenant, with no published figure. POST /calls/{id}/monitor-session refuses with 429 when a tenant has exhausted it, and that response is not in the error envelope: the cooldown is a top-level retryAfterSeconds, repeated in a standard Retry-After header. Read the header or the top-level field, never error.details, which is absent on that route. See The monitoring API.

Build for throttling anyway#

An API without published limits is not an API without limits. Write the client that survives one:

Detect it#

Treat any 429 as throttling, and treat 5xx as potentially transient. Branch on error.code, never on the message:

Detecting a throttle
const res = await fetch(url, { headers });
const body = await res.json();

if (res.status === 429) {
  // Cooldown, when the endpoint supplies one.
  const seconds =
    body.error?.details?.retryAfterSeconds ??
    (body.error?.details?.retryAfterHours ?? 0) * 3600;
  return { retryAfter: seconds || null };
}

Back off exponentially, with jitter#

Retrying on a fixed interval synchronizes every client you run into a thundering herd. Randomize the delay:

Exponential backoff with full jitter
async function withRetry<T>(
  call: () => Promise<Response>,
  { attempts = 5, baseMs = 500 } = {},
): Promise<Response> {
  let lastError: unknown;

  for (let attempt = 0; attempt < attempts; attempt += 1) {
    try {
      const res = await call();
      if (res.status !== 429 && res.status < 500) return res;
      lastError = new Error(`HTTP ${res.status}`);
    } catch (error) {
      lastError = error;
    }

    // Full jitter: sleep a random point in the whole backoff window.
    const ceiling = baseMs * 2 ** attempt;
    await new Promise((r) => setTimeout(r, Math.random() * ceiling));
  }

  throw lastError;
}

Honor an explicit cooldown#

When the response carries retryAfterSeconds or retryAfterHours, wait at least that long; backoff is a floor, not a substitute.

Never retry a POST blindly#

POST is not idempotent here, so a retry after a timeout can create a duplicate. Reconcile before retrying. See Idempotency.

Reduce the load instead#

The cheapest request is the one you never send.

  • Page large, not often. limit accepts up to 500; one request beats ten.
  • Cache reference data. Voices, models and transcribers change rarely, so read them at startup, not per call.
  • Let webhooks push. Do not poll Calls for completion; take the end-of-call report instead. This is the single biggest reduction available to most integrations.
  • Filter server-side. Use search rather than listing everything and filtering locally.

Next steps#