09Основы API
Повтор без двойного списания
Если вы не уверены, дошёл ли запрос, отправьте его снова с тем же ключом повтора: картинка и списание будут одни.
Эта модель рисует не все разрешения и форматы кадра: в примерах стоят те, что ей доступны.
Зачем нужен ключ повтора
Связь может оборваться после того, как запрос дошёл до нас, но до того, как вы получили ответ. Вы не знаете, принят ли запрос. Отправить его ещё раз без ключа — значит купить вторую картинку.
Без ключа
- Отправили запрос — связь оборвалась, ответа нет.
- Отправили ещё раз.
- Получили две картинки и два списания.
С ключом повтора
- Отправили запрос с ключом — связь оборвалась.
- Отправили тот же запрос с тем же ключом.
- Получили тот же
idи одно списание.
Как пользоваться
- Передайте заголовок
X-Idempotency-Keyв запросе генерации. Подойдёт UUID или номер операции в вашей системе: непустая строка не длиннее 128 знаков, иначе400 ERR_INVALID_REQUEST. - Новая картинка — новый ключ. Повтор того же запроса — прежний ключ.
- Сохраните ключ и точное тело запроса до отправки: они нужны для повтора.
- Ключ действует в пределах одного API-ключа. Другой API-ключ, даже того же аккаунта, считает операцию новой.
- Ключ не сверяет тело запроса. Если отправить другой промпт с прежним ключом, вернётся прежний ответ, а промпт не изменит ничего. Для новой операции берите новый ключ.
Без заголовка защиты от повторного выполнения нет.
Что вернёт повтор
Если первый запрос уже принят, повтор с тем же ключом вернёт тот же ответ 202: те же id, status, task_id и cost. Новой картинки и нового списания не будет.
{
"id": "5b0214ae-9e39-460a-820a-f6ae3e24400b",
"status": "queued",
"task_id": "5b0214ae-9e39-460a-820a-f6ae3e24400b",
"cost": 0.04
}Это сохранённый ответ на отправку, а не текущее состояние: в нём может остаться queued, даже когда картинка уже готова. Актуальный статус всегда берите у GET /creations/.
Пример с повторами
Код ниже делает не больше трёх попыток, ставит тайм-ауты, ждёт всё дольше и слушается Retry-After. Ключ повтора создан один раз на операцию и не меняется между попытками.
# Нужны bash, curl и uuidgen. Ключ создаётся один раз на операцию. IDEMPOTENCY_KEY="$(uuidgen)" || exit 1 curl --fail-with-body --max-time 30 \ -X POST https://api.rendergrid.io/ api/ public/ v1/ images/ generate \ -H "Authorization: Bearer $RENDERGRID_API_KEY" \ -H "Content-Type: application/json" \ -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \ -d '{ "model": "nano-banana-pro", "prompt": "Керамическая кружка на мраморе, мягкий свет окна" }'
import os import random import time import requests API = "https://api.rendergrid.io/ api/ public/ v1" KEY = os.environ["RENDERGRID_API_KEY"] def submit( payload: dict, operation_key: str, max_attempts: int = 3 ) -> dict: headers = { "Authorization": f"Bearer {KEY}", "X-Idempotency-Key": operation_key, } for attempt in range(max_attempts): last = attempt + 1 == max_attempts delay = 2 ** (attempt + 1) + random.random() try: resp = requests.post( f"{API}/images/generate", headers=headers, json=payload, timeout=(5, 30), ) except (requests.Timeout, requests.ConnectionError): if last: raise else: if resp.status_code == 202: return resp.json() try: details = resp.json()["error"].get("details") or {} except (ValueError, KeyError, AttributeError): details = {} retryable = ( resp.status_code == 429 or resp.status_code >= 500 or details.get("retryable") is True ) blocked = details.get("reason") == "burst_block" if blocked or not retryable or last: raise RuntimeError(resp.status_code, resp.text) wait = resp.headers.get("Retry-After", "") if wait.isascii() and wait.isdigit(): delay = max(delay, int(wait)) time.sleep(delay) payload = { "model": "nano-banana-pro", "prompt": "Керамическая кружка на мраморе, мягкий свет окна", } creation = submit(payload, operation_key="order-8412-cover-v1") print(creation["id"])
// Node.js 22+, файл .mjs const API = "https://api.rendergrid.io/ api/ public/ v1"; const KEY = process.env.RENDERGRID_API_KEY; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function submit(payload, operationKey, maxAttempts = 3) { const body = JSON.stringify(payload); for (let attempt = 0; attempt < maxAttempts; attempt++) { const last = attempt + 1 === maxAttempts; let delay = (2 ** (attempt + 1) + Math.random()) * 1_000; let resp; try { resp = await fetch(`${API}/images/generate`, { method: "POST", headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json", "X-Idempotency-Key": operationKey, }, body, signal: AbortSignal.timeout(30_000), }); } catch (error) { const network = error instanceof TypeError; if (!network && error.name !== "TimeoutError") throw error; if (last) throw error; } if (resp) { if (resp.status === 202) return resp.json(); const text = await resp.text(); let details; try { details = JSON.parse(text)?.error?.details; } catch { details = undefined; } const retryable = resp.status === 429 || resp.status >= 500 || details?.retryable === true; const blocked = details?.reason === "burst_block"; if (blocked || !retryable || last) { throw new Error(`HTTP ${resp.status}: ${text}`); } const wait = resp.headers.get("Retry-After"); if (wait && /^[0-9]+$/.test(wait)) { delay = Math.max(delay, Number(wait) * 1_000); } } await sleep(delay); } } const payload = { model: "nano-banana-pro", prompt: "Керамическая кружка на мраморе, мягкий свет окна", }; const creation = await submit(payload, "order-8412-cover-v1"); console.log(creation.id);
Пример curl отправляет запрос один раз. Если результат неизвестен, повторите его вручную с теми же ключом и телом.
Что делать, если…
| Ситуация | Что делать |
|---|---|
| Тайм-аут или обрыв связи, ответа нет | Повторите запрос с тем же ключом и тем же телом |
Ответ 429: первый запрос ещё принимается | В ответе details.reason: idempotency_in_flight. Подождите Retry-After и повторите с тем же ключом |
Ответ 400, 402 или 404 | Запрос не принят. Устраните причину и повторите с тем же ключом |
Ответ 5xx | Повторите с тем же ключом, ограничив число попыток. 500 не говорит, принят ли запрос |
Пришёл 202, а потом задание стало failed | Прежний ключ задание не перезапустит. Для новой попытки нужен новый ключ: это новая платная операция, а прежняя вернётся автоматически |
| Нужна вторая картинка | Новый запрос с новым ключом |
Неудачный запрос бесплатен: сумма сразу возвращается на баланс.
Сколько хранится ключ
Сохранённые ответы живут 30 дней. После этого запись может быть удалена, и тот же ключ запустит новую платную генерацию. Храните у себя связь «ваша операция → id задания» и не используйте старые ключи как способ поиска.
Ключ повтора не даёт безусловной гарантии при сбоях сервиса. Отправляйте запросы одной операции по очереди, ограничивайте число попыток и ведите свой учёт. Если после повторов всё ещё неясно, принят ли запрос, загляните в историю запросов или напишите в поддержку — до того, как отправлять его с новым ключом.
Частые вопросы
Нужен ли ключ повтора для каждого запроса?
Можно ли использовать один ключ для разных картинок?
Что будет, если ключ длиннее 128 знаков?
400 ERR_INVALID_REQUEST. Возьмите UUID или короткий номер операции.