Ошибки и лимиты
Конверт ошибок, все коды API со статусами, семантика лимитов запросов, квоты тарифов и поведение кошелька при превышении.
Конверт ошибок
Любая ошибка — валидация, аутентификация, квота, движок — приходит в одном формате:
{
"error": {
"code": "QUOTA_EXCEEDED",
"message": "Monthly searches quota exhausted (500000/500000 for 2026-10)",
"request_id": "req_3c5e7a9b1d3f5a7c9e1b3d5f",
"details": {
"metric": "searches",
"limit": 500000,
"used": 500000,
"period": "2026-10",
"resetsAt": "2026-11-01T00:00:00.000Z"
}
}
}code — стабильный идентификатор, по нему можно ветвиться; message предназначен людям и может меняться; request_id дублируется в заголовке x-request-id. details присутствует только тогда, когда есть что сообщить.
curl -sS -i "$AAC_API/indexes/documentation/search?q=тарифы&num_typos=9" \
-H "Authorization: Bearer $AAC_KEY"{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request payload is invalid",
"request_id": "req_7d9f1b3c5e7a9b1d3f5c7a9b",
"details": {
"issues": [
{
"code": "too_big",
"path": ["num_typos"],
"message": "Number must be less than or equal to 3"
}
]
}
}
}Коды по статусам
400
| Код | Где возникает |
|---|---|
INVALID_QUERY | Неверный синтаксис фильтра, сортировки, списка полей или vector_query; упоминание organization_id в filter_by; отсутствие filter_by при массовом удалении; индекс без поля автоэмбеддинга при semantic=true; больше 20 запросов в multi_search |
FIELD_NOT_IN_SCHEMA | filter_by, sort_by, facet_by, query_by или group_by ссылается на поле, которого нет в схеме индекса (в документах оно может быть). details.field — поле из вашего запроса, details.param — параметр. Добавьте поле в разделе «Схема» и переиндексируйте |
INVALID_JSON | Тело вебхука не является JSON |
PRIVATE_FIELD | query_by, filter_by, sort_by, facet_by, include_fields или group_by называет приватное поле, а у ключа нет documents:read_private (также /embed/search) |
PRIVATE_FIELDS_PRESENT | POST /indexes/{slug}/scoped-key для индекса с приватными полями ключом без documents:read_private. details.fields |
401
| Код | Где возникает |
|---|---|
UNAUTHORIZED | Нет заголовка Authorization, он некорректен, ключ неизвестен или отозван, нет заголовков подписи вебхука |
KEY_ROTATED | Ключ был ротирован, льготный период истёк. Используйте ключ, выданный при ротации |
EMBED_SESSION_REQUIRED / EMBED_SESSION_EXPIRED / EMBED_SESSION_INVALID | /embed/search, /embed/events, /embed/config, /embed/recommendations вызваны без токена сессии, с истёкшим или недействительным токеном. Выпустите новый через POST /embed/session |
EMBED_SESSION_REVOKED | API-ключ или сохранённый виджет, выпустивший сессию встраивания, отозван, истёк или отключён; токен перестаёт работать примерно через 30 секунд после изменения |
STALE_SIGNATURE | Отметка времени вебхука вне окна 300 секунд |
INVALID_SIGNATURE | Несовпадение HMAC вебхука |
402
| Код | Где возникает |
|---|---|
PAYMENT_REQUIRED | Функция не входит в тариф (details.feature, details.upgradeTo) или на кошельке не хватает на следующий блок превышения (details.blockCents). Оба случая означают «заплатите или смените тариф», а не «повторите позже» |
PLAN_LIMIT_REACHED | Создание ещё одного индекса или API-ключа либо импорт документов превысит лимит тарифа (индексы, ключи или документы) |
FEATURE_NOT_AVAILABLE | Организация suspended после неоплаты подписки: работает как Free, платная функция отклоняется с details.reason = "past_due" — оплатите счёт, а не повышайте тариф |
403
| Код | Где возникает |
|---|---|
FORBIDDEN | У ключа нет нужного права или он привязан к другому индексу; сессия встраивания использована не для search:read |
ORIGIN_NOT_ALLOWED | Запрос с заголовком Origin, которого нет в allowedOrigins ключа. Проверяется на сервере для всех маршрутов с API-ключом (и POST /embed/session), а не только через CORS-заголовки; запросы без Origin (сервер-сервер) не затрагиваются |
INDEX_MISMATCH | Сессия встраивания привязана к другому индексу, чем запрошенный |
KEY_IN_GRACE | Вызывающий ключ был ротирован и находится в льготном периоде: он не может ротировать ключи и выпускать ограниченные ключи. Используйте преемника |
KEY_LIFETIME_EXCEEDS_CALLER | POST /api-keys/{keyId}/rotate для ключа, который пережил бы вызывающий ключ |
FORBIDDEN (на всю организацию) | GET /organization/export, DELETE /organization/data или /webhooks/* ключом, привязанным к индексу |
Утверждение acl в запросе поиска игнорируется (а не отклоняется), если у ключа есть список allowedOrigins или запрос пришёл с заголовком Origin: ключ, видимый в браузере, не может выбирать, чьи документы он видит. Такой поиск видит только документы без ACL.
404
| Код | Где возникает |
|---|---|
INDEX_NOT_FOUND | Неизвестный слаг индекса в организации ключа; сессия встраивания без доступного индекса. «Коллекция не найдена» на уровне движка отдаётся под этим публичным кодом |
DOCUMENT_NOT_FOUND | GET/DELETE по неизвестному external_id |
PROFILE_NOT_FOUND | ?profile= указывает на профиль, которого нет у индекса |
CONVERSATION_NOT_FOUND | Идентификатор диалога, не принадлежащий индексу |
NOT_FOUND | /metrics без валидного x-metrics-token |
JOB_NOT_FOUND | Задача другого тенанта или (для ключа с привязкой) другого индекса |
SNAPSHOT_NOT_FOUND | Неизвестный снимок или снимок другого индекса/тенанта |
409
| Код | Где возникает |
|---|---|
INDEX_ALREADY_EXISTS | Дубликат на уровне движка при создании коллекции (переиндексация, консоль); физическое имя коллекции не раскрывается |
CONFLICT | Конфликтующая запись, обнаруженная сервисным слоем |
IDEMPOTENCY_IN_FLIGHT | Импорт документов с тем же Idempotency-Key ещё выполняется — повторите после его завершения |
JOB_CONFLICT | Запрошен reindex или restore (в том числе с ?sync=true), пока reindex или restore в очереди или выполняется, либо переэмбеддинг всего индекса при уже активном. details.job_id называет активную задачу |
JOB_NOT_CANCELLABLE | Отмена завершённой задачи или задачи на этапе swapping |
SNAPSHOT_LIMIT_REACHED | Достигнут лимит снимков на индекс; повторите с ?prune=true |
KEY_ALREADY_ROTATED / KEY_NOT_ACTIVE | Повторная ротация / ротация отозванного или истёкшего ключа |
413
| Код | Где возникает |
|---|---|
AUDIO_TOO_LARGE | Голосовой фрагмент превышает допустимый размер |
422
| Код | Где возникает |
|---|---|
VALIDATION_ERROR | Отказ схемы Zod, включает details.issues |
IDEMPOTENCY_KEY_REUSED | Idempotency-Key уже использован для другого запроса (отличается метод, путь или тело) |
INVALID_DOCUMENT | Нет content, неверное значение поля, больше 500 документов на импорт или 1000 на вебхук |
INVALID_AUDIO | Аудио не удалось декодировать |
EMPTY_TRANSCRIPTION | В голосовом запросе не распознана речь |
CLIENT_ID_REQUIRED | POST /recent без x-aac-client-id |
QUERY_TOO_SHORT | POST /recent с запросом короче 2 символов |
INVALID_SNAPSHOT_NAME | DELETE /admin/snapshots с небезопасным именем файла |
429
| Код | Где возникает |
|---|---|
RATE_LIMITED | Превышен лимит запросов на ключ |
QUOTA_EXCEEDED | Исчерпана квота тарифа по метрике |
5xx
| Код | Статус | Где возникает |
|---|---|---|
INTERNAL_ERROR | 500 | Необработанное исключение — сообщите request_id |
FEATURE_UNSUPPORTED | 501 | Функции нужен более новый поисковый движок, чем подключён (FEATURE_UNAVAILABLE — функция ещё не включена в этом развёртывании) |
SEARCH_UNAVAILABLE | 502 | Движок недоступен или вернул неотображённую ошибку |
AI_UNAVAILABLE | 502 | Сбой провайдера модели |
AI_EMPTY_RESULT | 502 | Модель вернула пустой результат |
SNAPSHOT_FAILED | 502 | Не удался снимок движка |
SNAPSHOT_NOT_CONFIGURED | 503 | POST /admin/snapshot без SEARCH_SNAPSHOT_DIR |
SEARCH_UNAVAILABLE | 503 | В развёртывании нет учётных данных поискового движка |
AI_NOT_CONFIGURED | 503 | Нет OPENAI_API_KEY для ответов, изображений или транскрипции |
NL_MODEL_NOT_CONFIGURED | 503 | Модель для этой функции не настроена |
VOICE_MODEL_NOT_CONFIGURED | 503 | Модель для этой функции не настроена |
CONVERSATION_MODEL_NOT_CONFIGURED | 503 | Модель для этой функции не настроена |
WEBHOOK_NOT_CONFIGURED | 503 | Нет секрета вебхука у индекса или на сервере |
SEARCH_TIMEOUT | 504 | Движок не ответил вовремя |
Ошибки движка и провайдеров приводятся к этому публичному контракту до выхода из API, внутренние имена в ответе не появляются: 404 движка становится INDEX_NOT_FOUND, 409 — INDEX_ALREADY_EXISTS, 400/422 — INVALID_QUERY (неизвестное поле в filter_by/sort_by/facet_by/query_by/group_by — FIELD_NOT_IN_SCHEMA), таймауты — SEARCH_TIMEOUT, всё остальное (включая отсутствие конфигурации движка) — SEARCH_UNAVAILABLE. Другие алиасы: AI_PROVIDER_ERROR → AI_UNAVAILABLE, ENGINE_FEATURE_UNSUPPORTED → FEATURE_UNSUPPORTED, INDEX_HAS_NO_EMBEDDING_FIELD → INDEX_NOT_VECTOR_ENABLED. Исходное сообщение движка пишется в лог на сервере и связывается через request_id; назовите этот id в поддержке.
Лимиты запросов
Запросы ограничиваются на API-ключ в скользящем окне:
| Настройка | По умолчанию | Переменная |
|---|---|---|
| Окно | 60 секунд | SEARCH_RATE_WINDOW_SECONDS |
| Запросов в окне | 600 | SEARCH_RATE_MAX_REQUESTS |
Каждый аутентифицированный ответ несёт x-ratelimit-limit, x-ratelimit-remaining и x-ratelimit-reset (Unix-время в секундах), включая успешные, — так клиент может сам выдерживать темп. Превышение — 429 с временем сброса:
curl -sS -i "$AAC_API/indexes/documentation/search?q=тарифы" \
-H "Authorization: Bearer $AAC_KEY"{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded",
"request_id": "req_9b1d3f5a7c9e1b3d5f7a9c1e",
"details": { "limit": 600, "reset_at": "2026-10-23T10:42:00.000Z" }
}
}По умолчанию лимитер работает в процессе, а при заданном REDIS_URL — общий через Redis. На нескольких инстансах без Redis каждый инстанс держит свои 600 — учитывайте это при планировании.
RATE_LIMITED и QUOTA_EXCEEDED — оба 429, но означают разное: первый снимается через секунды, второй — только с началом нового периода или после смены тарифа. Смотрите error.code, а не только статус.
Квоты тарифов
Метрики учёта: searches, documents, ai_tokens и reindex. Включённые в тариф объёмы за период:
| Free | Pro ($29/мес) | Enterprise | |
|---|---|---|---|
| Поиски | 10 000 | 500 000 | 5 000 000 |
| Документы | 1 000 | 50 000 | 1 000 000 |
| AI-токены | 10 000 | 100 000 | 2 000 000 |
| Переиндексации | 2 | 50 | 500 |
| Индексы | 5 | 10 | 100 |
| API-ключи | 3 | 10 | 100 |
| Коннекторы | 5 | 10 | 100 |
| Результатов на страницу | 50 | 250 | 500 |
| Хранение журнала запросов | 7 дней | 90 дней | 365 дней |
Цены превышения за блок:
| Метрика | Блок | Free | Pro | Enterprise |
|---|---|---|---|---|
| Поиски | 1 000 | — | 50¢ | 25¢ |
| Документы | 1 000 | — | 20¢ | 10¢ |
| AI-токены | 1 000 | — | 2¢ | 1¢ |
| Переиндексация | 1 запуск | — | 5¢ | 2¢ |
У Free цен превышения нет вообще: при достижении лимита возвращается 429 QUOTA_EXCEEDED, и ничего не списывается. На платных тарифах превышение списывается с предоплаченного кошелька.
Функции, зависящие от тарифа: Free включает поиск по ключевым словам, синонимы, стоп-слова, aiAnswer (RAG, в пределах 10 000 AI-токенов) и scheduledSync, но не vectorSearch, hybridSearch, curations, grouping, geoSearch, multiSearch, reindex, exportDocuments, customSort и queryAnalytics. Использование закрытой функции даёт 402 PAYMENT_REQUIRED от слоя учёта; запрос падает, а не деградирует молча. Недостающий scope API-ключа по-прежнему даёт 403 FORBIDDEN.
Кошелёк и превышение
- Расход считается по периоду (
YYYY-MM) и сбрасывается в00:00 UTCпервого числа. - В пределах включённого объёма вызов ничего не стоит.
- Сверх него каждый блок списывается с кошелька:
searchesблоками по 1 000,documentsиai_tokensтак же,reindex— за запуск. - При нулевом балансе вызов падает с
402 PAYMENT_REQUIREDи инструкцией по пополнению; функция тарифа, требующая оплаты, падает так же. - Пополнения — предоплаченные пакеты на $10, $50 и $200, они покупаются через платёжного провайдера и попадают в журнал операций.
Смотрите состояние заранее:
curl -sS "$AAC_API/billing/wallet" -H "Authorization: Bearer $AAC_KEY"{
"wallet": { "balanceCents": 4200, "currency": "USD" },
"plan": "pro",
"period": "2026-10",
"resets_at": "2026-11-01T00:00:00.000Z",
"transactions": [
{
"id": "txn_01HZYA",
"type": "credit",
"amount_cents": 5000,
"balance_after_cents": 5000,
"description": "Top-up credits_50",
"metric": null,
"created_at": "2026-10-02T09:12:00.000Z"
},
{
"id": "txn_01HZYB",
"type": "debit",
"amount_cents": -800,
"balance_after_cents": 4200,
"description": "search overage: 16 blocks",
"metric": "searches",
"created_at": "2026-10-21T18:03:11.000Z"
}
],
"request_id": "req_4f6a8c0e2b4d6f8a0c2e4b6d"
}curl -sS "$AAC_API/billing/usage" -H "Authorization: Bearer $AAC_KEY"{
"usage": {
"plan": "pro",
"planName": "Pro",
"period": "2026-10",
"resetsAt": "2026-11-01T00:00:00.000Z",
"searches": { "used": 500000, "limit": 500000 },
"documents": { "used": 51204, "limit": 50000 },
"aiTokens": { "used": 24800, "limit": 100000 },
"indexes": { "used": 3, "limit": 10 },
"apiKeys": { "used": 2, "limit": 10 },
"connectors": { "used": 1, "limit": 10 },
"wallet": { "balanceCents": 4200, "currency": "USD" }
},
"request_id": "req_2b4d6f8a0c2e4b6d8f0a2c4e"
}documents считается по зеркалу документов, а не по счётчику, поэтому отражает фактическое содержимое индекса. wallet равен null, если у тенанта никогда не было пополнений.
Практические рекомендации
- Повторяйте только на 429. Заголовок
Retry-Afterне отправляется; используйтеdetails.reset_atилиx-ratelimit-reset. - Не повторяйте 4xx кроме 429. 403 не станет 200 от повторения, а тело с 422 нужно исправить.
- Повторяйте 5xx с джиттером. SDK по умолчанию делают две попытки с экспоненциальной задержкой.
- Следите за
x-aac-quota-remaining. Падения на границе квоты можно избежать, если алертить на 10% остатка. - Алертите по доле пустых выдач, а не по 200. Поиск без результатов — успех по HTTP и провал по продукту.