Skip to content

Getting Started

Rate limits

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

TODO — verify before release

OnCore does not currently publish numeric rate limits for the Core API, and the OpenAPI spec declares no 429 responses outside the WhatsApp onboarding endpoints. The behavior described below is the contract you should build against; the specific numbers must be confirmed with the API team before this page ships.

What is documented today#

Two endpoints return 429 with a documented cooldown. 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.

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 — 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#