Справочник API
Защита от двойных списаний
Идемпотентность означает, что повторный запрос выполняется один раз: передайте при повторе тот же Idempotency-Key, и списание будет только одно, даже если первая попытка лишь выглядела потерянной.
Запрос с тайм-аутом — не то же самое, что неудачный запрос. Генерация может уже идти, а повтор вслепую запустит — и оплатит — вторую. Передайте Idempotency-Key, и шлюз выполнит запрос один раз, а затем 24 часа будет возвращать тот же ответ.
#Когда передавать ключ
Передавайте
- При каждой отправке асинхронной задачи — видео, изображения, музыка, 3D. Это самые дорогие запросы.
- В любом непотоковом вызове, который ваш воркер повторяет автоматически.
- В вызовах из очереди, которая может повторно выдать задание после сбоя.
Не нужен
- Потоковые запросы — заголовок отклоняется с 400, поток нельзя воспроизвести повторно.
- GET-опрос и чтение метаданных — их повтор и так бесплатен и безопасен.
- Интерактивный чат, где лишний ответ обходится дешевле, чем учёт ключей.
curl https://www.aisplice.store/v1/videos \
-H "Authorization: Bearer sk-YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"model": "doubao-seedance-2.5",
"prompt": "A drone shot of a lighthouse at dawn"
}'Формируйте ключ из своей записи
#Как сопоставляются ключи
Включается явно, не автоматически
Без заголовка ничего не меняется. Повтор без ключа — это второй платный запрос.
Только методы записи
GET, HEAD и OPTIONS игнорируют заголовок — опрос задачи и так можно безопасно повторять.
Область действия: пользователь + ключ + метод + маршрут
То же значение с другим API-ключом или на другом эндпоинте — это другая операция.
Тело входит в идентификацию запроса
Путь, query-параметры и тело хешируются вместе. Повторное использование ключа с изменёнными данными вернёт 409, а не перезапишет запрос.
Несовместимо с потоковой передачей
Потоковый ответ нельзя сохранить для воспроизведения, поэтому stream:true вместе с ключом отклоняется с кодом 400.
При сбое — отказ
Если журнал недоступен, запрос отклоняется с кодом 503, а не выполняется без защиты.
Длина ключа — не более 255 символов. Более длинный ключ отклоняется с 400 до выполнения запроса.
#Результаты и коды статуса
Повторно возвращённый ответ содержит X-Idempotent-Replayed: true и исходный статус — считайте его ответом, а не дубликатом, требующим сверки. Все отказы используют плоский формат предварительной проверки.
| HTTP | Код | Значение | Что делать |
|---|---|---|---|
| 400 | IDEMPOTENCY_STREAM_UNSUPPORTED | Idempotency-Key передан в потоковом запросе. | Уберите заголовок из потоковых вызовов или задайте stream:false, если нужна защита от повторов. |
| 400 | IDEMPOTENCY_BODY_UNREADABLE | Не удалось вычислить хеш тела для журнала идемпотентности. | Отправляйте обычное тело JSON или multipart — chunked-тела, которые нельзя буферизовать, отклоняются. |
| 409 | IDEMPOTENCY_PAYLOAD_CONFLICT | Этот ключ уже использовался с другим телом, путём или query-параметрами. | Сгенерируйте новый ключ. Ключ обозначает один конкретный запрос, а не цикл повторов. |
| 409 | IDEMPOTENCY_IN_PROGRESS | Первый запрос с этим ключом ещё выполняется. | Подождите Retry-After (2 с) и повторите запрос с тем же ключом — вы получите исходный ответ. |
| 409 | IDEMPOTENCY_RESULT_UNREPLAYABLE | Исходный запрос завершён, но его ответ закрыт и не может быть воспроизведён. | Не отправляйте запрос повторно вслепую. Сначала сверьтесь с эндпоинтом задачи или своими записями. |
| 503 | IDEMPOTENCY_UNAVAILABLE | Журнал идемпотентности недоступен, поэтому проверка отклоняет запрос, чтобы не рисковать двойным списанием. | Повторите с увеличивающейся задержкой — запрос не выполнялся. |
#Цикл повторов
Один ключ на задание, одинаковый во всех попытках; 409 «ещё выполняется» обрабатывается как «подождать», а не как «ошибка».
import time, uuid, requests
def submit_once(job_id: str, body: dict, api_key: str, tries: int = 5):
# Same key for every attempt of this job — that is what makes it safe.
key = str(uuid.uuid5(uuid.NAMESPACE_URL, f"video-job:{job_id}"))
for attempt in range(tries):
response = requests.post(
f"https://www.aisplice.store/v1/videos",
headers={
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": key,
},
json=body,
timeout=60,
)
if response.status_code == 409:
code = response.json().get("code", "")
if code == "IDEMPOTENCY_IN_PROGRESS":
time.sleep(int(response.headers.get("Retry-After", 2)))
continue
# PAYLOAD_CONFLICT or RESULT_UNREPLAYABLE: a human decision,
# never a silent resubmit.
raise RuntimeError(response.text)
if response.status_code >= 500:
time.sleep(2 ** attempt)
continue
response.raise_for_status()
replayed = response.headers.get("X-Idempotent-Replayed") == "true"
return response.json(), replayed
raise TimeoutError(f"submit for {job_id} did not settle")Сохраните ID задачи, прежде чем что-либо повторять
Проблема не решена?
Пришлите нам ID запроса или текст ошибки, и мы разберёмся.