Idempotency-Key: как ретраить запросы и не платить дважды
Таймаут на клиенте не значит, что запрос не выполнился на сервере. Заголовок Idempotency-Key решает это без ручной проверки истории.
4 мин · · Обновлено · API · надёжность
Сеть обрывается посередине запроса, HTTP-клиент падает по таймауту раньше, чем модель успела ответить, — а на сервере запрос при этом мог полностью выполниться и списаться. Без защиты от повторов единственный безопасный вариант — идти в /v1/usage и вручную проверять, было ли списание, прежде чем ретраить руками. Idempotency-Key снимает эту проблему на уровне протокола.
Как это работает
Добавляете заголовок Idempotency-Key с произвольной уникальной строкой к запросу. Если в течение 24 часов придёт ещё один запрос с тем же значением заголовка и тем же телом — сервер вернёт тот же самый ответ, не выполняя запрос повторно и не списывая деньги второй раз. Работает это для POST /v1/chat/completions, /v1/messages и /v1/responses — не для пополнений баланса.
curl https://api.zukko.pro/v1/chat/completions \
-H "Authorization: Bearer fetch_ваш_ключ" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-48213-email-draft" \
-d '{
"model": "gpt-5.6-sol",
"messages": [{"role": "user", "content": "Сформируй письмо клиенту"}]
}'Если этот же curl повторить с тем же значением Idempotency-Key и тем же JSON-телом — ответ придёт из уже выполненного запроса, без повторного обращения к модели и без повторного списания.
Когда это реально нужно
Обрыв соединения на середине ответа. В разделе про ошибки это отдельно оговорено: обрыв ответа почти всегда таймаут клиента или сети, а не лимит по токенам. Даже при обрыве шлюз дочитывает ответ провайдера и списывает всю генерацию, а клиент об этом просто не узнал. Повторный запрос с тем же Idempotency-Key не создаст второе списание.
Автоматические ретраи в очередях задач. Если воркер падает после отправки запроса, но до записи результата, при перезапуске он повторит вызов. Без идемпотентности это двойная оплата за одну и ту же генерацию — особенно ощутимо на запросах с большим контекстом.
Флаки-сети на мобильных клиентах. Плохое соединение — обычная причина повторных нажатий кнопки «отправить» пользователем. Idempotency-Key на уровне бэкенда защищает от того, что оба нажатия дойдут до модели как два разных запроса.
Как выбирать значение ключа
Ключ должен быть уникальным для каждого логического запроса и одинаковым при всех его повторах. Задавайте его в конкретном вызове, а не в заголовках клиента по умолчанию (например, default_headers в OpenAI SDK) — иначе следующий, уже другой запрос получит 422 idempotency_conflict. Проще всего сгенерировать UUID один раз перед первой попыткой и переиспользовать его при ретраях того же самого запроса:
import { randomUUID } from "crypto";
async function sendWithRetry(body, maxAttempts = 3) {
const idempotencyKey = randomUUID();
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const resp = await fetch("https://api.zukko.pro/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": "Bearer fetch_ваш_ключ",
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
});
if (resp.ok) return resp.json();
} catch (e) {
if (attempt === maxAttempts) throw e;
}
}
}Важно: idempotencyKey создаётся один раз до цикла ретраев, а не внутри него — иначе каждая попытка снова станет обычным новым запросом.
Ошибка 422 idempotency_conflict
Если отправить тот же Idempotency-Key, но с другим телом запроса — сервер ответит 422 с кодом idempotency_conflict (поля metadata, user и stream_options на сравнение не влияют):
{
"error": {
"message": "Idempotency-Key already used with a different request body",
"code": "idempotency_conflict"
}
}Это защита от бага в другую сторону: если код случайно переиспользует один и тот же ключ для двух разных по смыслу запросов (например, из-за коллизии счётчика вместо UUID), сервер не станет молча возвращать не относящийся к делу ответ, а явно откажет.
Практический вывод: не стройте Idempotency-Key на инкрементном счётчике вроде request-1, request-2 в рамках сессии — если счётчик где-то сбросится, вы получите коллизию с чужим телом запроса и 422 вместо нормального ответа.
Что Idempotency-Key не делает
Он не заменяет кэш ответов и не экономит деньги на разных по содержанию запросах — это буквально защита от повторной оплаты одного и того же запроса при ретрае. Если вам нужно не платить за повторяющиеся, но разные по времени одинаковые промпты — это отдельная функция кэширования по заголовку X-Zukko-Cache: true (или temperature: 0 — у моделей, которые его принимают, не у Claude).
Также ключ не продлевает время жизни ответа бесконечно — гарантия дедупликации действует 24 часа. Если ретрай происходит спустя сутки после первой попытки, это уже новый запрос со своим списанием.
Ключ, привязанный к бизнес-действию, а не к попытке
Для пользовательских сценариев (например, кнопка «сгенерировать» в интерфейсе) удобнее строить Idempotency-Key не из случайного UUID, а из id самого действия: generate-report-48213 или invoice-9911-email-draft. Тогда повторное нажатие той же кнопки на том же документе гарантированно не приведёт к двойной генерации, даже если пользователь обновит страницу и код заново создаст «случайный» UUID с нуля. Единственное условие — тело запроса при этом должно оставаться идентичным, иначе вместо дедупликации вы получите 422 idempotency_conflict.
Итог
Idempotency-Key — это один заголовок, который убирает целый класс проблем «а вдруг запрос уже выполнился, просто ответ не дошёл». Генерируйте ключ один раз на логический запрос, переиспользуйте его при ретраях того же тела запроса, и не переиспользуйте между разными по содержанию запросами — иначе получите 422 вместо результата.
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- Агенты opencode: как настроить роли, модели и инструментыСвой агент в opencode — это роль, модель и список разрешений. Показываем конфиг и пример, где рутину делает дешёвая модель, а код правит сильная.
- Ключи OpenAI API в открытом доступе: почему ими нельзя пользоватьсяОткуда берутся «бесплатные» ключи на GitHub, почему пользоваться ими опасно и для вас, и для владельца, и как защитить собственный ключ от такой же утечки.
- Ошибки GLM API: почему не работает запрос и как это исправитьНеверный id модели, переполненный контекст 128K, ключ, баланс, лимиты: типичные причины ошибок GLM API и быстрый способ найти свою.
- Plan mode в Claude Code: сначала план, потом правки в кодеВ plan mode Claude Code читает код и предлагает план, но ничего не правит, пока вы не согласитесь. Как включить, использовать и во сколько это обходится.