For AI agents: markdown of this page — /docs-content-en/errors/handling.md documentation index — /llms.txt

Retries and error handling in code

A summary of retry strategies for temporary-refusal codes, plus ready-made response handlers in JavaScript, Python and PHP.

The summary table of all Vibecode API codes — Error codes.

Retries and pauses

Summary of the codes that signal a temporary problem and warrant a retry:

Code HTTP Retry strategy
RATE_LIMITED, QUEUE_OVERFLOW, QUEUE_TIMEOUT 429 Retry after Retry-After, then with a growing pause and random jitter
LARGE_BODY_BACKEND_BUSY 429 Retry after Retry-After with random jitter added to the pause. The request was not executed and changed no state — the retry is safe for writes too
OPERATION_TIME_LIMIT 429 Retry strictly after Retry-After: before that moment the same call will not go through. scope: "apiKey" — the pause applies only to the calling key
TIMEOUT_QUARANTINE 429 Retry after Retry-After with random jitter added to the pause, and without shortening the interval: an early retry extends the pause. scope: "portal" — the pause is shared by all keys of the portal
ERROR_LOOP_DETECTED 429 Retry after Retry-After. The body carries no retryAfter field. First find the cause: the code means a series of identical refusals on one key. The platform lets every Nth retry through as a recovery probe
BITRIX_TIMEOUT 503 A longer pause before the retry than for 429. For a write — re-read the entity first: the change may already have been applied
BITRIX_UNAVAILABLE 502 This is a 5xx from Bitrix24 itself or a network problem between Vibecode and the portal, not a queue overload: the response carries no Retry-After header, so retry with exponential backoff. For a write — first check whether it was applied
POOL_EXHAUSTED, DB_TRANSIENT, SERVICE_UNAVAILABLE 503 Retry after retryAfter (the same value as the Retry-After header) — a few seconds. For DB_TRANSIENT the transaction is rolled back entirely, so the retry is safe for writes too. For SERVICE_UNAVAILABLE the outcome may remain unknown — for non-idempotent operations check the entity state before retrying
INTERNAL_ERROR 500 Retry the request. An error that reproduces consistently is not fixed by a retry — submit a ticket with the request time and the X-Request-Id header

The table covers the refusals that arrive on any endpoint. The remaining temporary states — installing an application through the connector module, waiting for a Bitrix24 update, freezing a key while an account is being deleted — are described by the code's row in the summary table, which says whether a retry changes anything.

Error handling in code

The handlers below limit the number of attempts. The limit is mandatory: a refusal that outlasts your patience becomes a series of identical requests if you retry endlessly, and the platform shuts that series down with the ERROR_LOOP_DETECTED code.

JavaScript

javascript
const MAX_ATTEMPTS = 5;

async function vibeRequest(url, options = {}, attempt = 1) {
  const response = await fetch(url, {
    ...options,
    headers: {
      'X-Api-Key': process.env.VIBE_API_KEY,
      'Content-Type': 'application/json',
      ...options.headers,
    },
  });

  const data = await response.json();

  if (!data.success) {
    const { code, message, retryAfter } = data.error;
    const canRetry = attempt < MAX_ATTEMPTS;

    switch (code) {
      case 'RATE_LIMITED':
      case 'QUEUE_TIMEOUT': {
        if (!canRetry) throw new Error(`${code}: the refusal persists for ${MAX_ATTEMPTS} attempts in a row`);
        const wait = retryAfter ?? Number(response.headers.get('Retry-After') ?? 1);
        await new Promise(r => setTimeout(r, wait * 1000));
        return vibeRequest(url, options, attempt + 1);
      }

      case 'BITRIX_UNAVAILABLE':
        if (!canRetry) throw new Error(`${code}: the refusal persists for ${MAX_ATTEMPTS} attempts in a row`);
        await new Promise(r => setTimeout(r, 5000));
        return vibeRequest(url, options, attempt + 1);

      case 'MISSING_API_KEY':
      case 'INVALID_API_KEY':
        throw new Error('Check the API key');

      default:
        throw new Error(`${code}: ${message}`);
    }
  }

  return data;
}

Python

Python
import os
import time
import requests

MAX_ATTEMPTS = 5

def vibe_request(url, method="GET", json_data=None, attempt=1):
    headers = {
        "X-Api-Key": os.environ["VIBE_API_KEY"],
        "Content-Type": "application/json",
    }

    response = requests.request(method, url, headers=headers, json=json_data)
    data = response.json()

    if not data.get("success"):
        err = data.get("error", {})
        code = err.get("code")
        message = err.get("message")
        retry_after = err.get("retryAfter") or int(response.headers.get("Retry-After", 1))
        can_retry = attempt < MAX_ATTEMPTS

        if code in ("RATE_LIMITED", "QUEUE_TIMEOUT"):
            if not can_retry:
                raise Exception(f"{code}: the refusal persists for {MAX_ATTEMPTS} attempts in a row")
            time.sleep(retry_after)
            return vibe_request(url, method, json_data, attempt + 1)

        if code == "BITRIX_UNAVAILABLE":
            if not can_retry:
                raise Exception(f"{code}: the refusal persists for {MAX_ATTEMPTS} attempts in a row")
            time.sleep(5)
            return vibe_request(url, method, json_data, attempt + 1)

        raise Exception(f"{code}: {message}")

    return data

PHP

php
const MAX_ATTEMPTS = 5;

function vibeRequest(string $url, string $method = 'GET', ?array $data = null, int $attempt = 1): array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => [
            'X-Api-Key: ' . getenv('VIBE_API_KEY'),
            'Content-Type: application/json',
        ],
    ]);
    if ($data !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    }
    $body = json_decode(curl_exec($ch), true);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($body === null) {
        throw new Exception("HTTP error: $httpCode");
    }

    if (empty($body['success'])) {
        $errCode = $body['error']['code'] ?? 'UNKNOWN';
        $errMsg = $body['error']['message'] ?? 'Unknown error';
        $retryAfter = $body['error']['retryAfter'] ?? 1;
        $canRetry = $attempt < MAX_ATTEMPTS;

        if (in_array($errCode, ['RATE_LIMITED', 'QUEUE_TIMEOUT'], true)) {
            if (!$canRetry) {
                throw new Exception("$errCode: the refusal persists for " . MAX_ATTEMPTS . ' attempts in a row');
            }
            sleep((int) $retryAfter);
            return vibeRequest($url, $method, $data, $attempt + 1);
        }

        if ($errCode === 'BITRIX_UNAVAILABLE') {
            if (!$canRetry) {
                throw new Exception("$errCode: the refusal persists for " . MAX_ATTEMPTS . ' attempts in a row');
            }
            sleep(5);
            return vibeRequest($url, $method, $data, $attempt + 1);
        }

        throw new Exception("$errCode: $errMsg");
    }

    return $body;
}

See also