Referencia de la API
Evitar cobros dobles
La idempotencia significa que una solicitud reintentada se ejecuta una sola vez: envía la misma Idempotency-Key en el reintento y se te cobrará una sola vez, aunque el primer intento solo pareciera perdido.
Una solicitud con timeout no es una solicitud fallida. Puede que la generación ya esté en marcha, y un reintento a ciegas inicia — y factura — una segunda. Envía una Idempotency-Key y el gateway ejecuta la solicitud una sola vez y luego reproduce la misma respuesta durante 24 horas.
#Cuándo enviar una clave
Envíala
- Todo envío asíncrono — video, tarea de imagen, música, 3D. Son los más caros.
- Cualquier llamada sin streaming que tu worker reintente automáticamente.
- Llamadas detrás de una cola que puede volver a entregar un trabajo tras una caída.
Omítela
- Solicitudes con streaming — el encabezado se rechaza con 400; un stream no se puede reproducir.
- Consultas GET y lecturas de metadatos — repetirlas ya es gratis y seguro.
- Chat interactivo en el que una respuesta duplicada cuesta menos que la gestión adicional.
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"
}'Deriva la clave de tu propio registro
#Cómo se comparan las claves
Opcional, nunca automático
Sin el encabezado no cambia nada. Un reintento sin clave es una segunda solicitud facturable.
Solo métodos de escritura
GET, HEAD y OPTIONS ignoran el encabezado — consultar una tarea ya es seguro de repetir.
Ámbito: usuario + clave + método + ruta
El mismo valor con otra clave de API, o en otro endpoint, es una operación distinta.
El cuerpo forma parte de la identidad
La ruta, la query y el cuerpo se hashean juntos. Reutilizar una clave con un payload modificado devuelve 409; no lo sobrescribe.
No compatible con streaming
Una respuesta en streaming no se puede almacenar para reproducirla, así que stream:true junto con una clave se rechaza con 400.
Falla en modo cerrado
Si el registro no está disponible, la solicitud se rechaza con 503 en lugar de ejecutarse sin protección.
Las claves tienen como máximo 255 caracteres. Cualquier clave más larga se rechaza con 400 antes de ejecutar la solicitud.
#Resultados y códigos de estado
Una respuesta reproducida incluye X-Idempotent-Replayed: true y el estado original — trátala como la respuesta, no como un duplicado que conciliar. Todos los rechazos usan el envoltorio plano del control previo.
| HTTP | Código | Significado | Qué hacer |
|---|---|---|---|
| 400 | IDEMPOTENCY_STREAM_UNSUPPORTED | Se envió Idempotency-Key en una solicitud con streaming. | Quita el encabezado en las llamadas con streaming, o usa stream:false cuando necesites protección contra repeticiones. |
| 400 | IDEMPOTENCY_BODY_UNREADABLE | No se pudo calcular el hash del cuerpo para el registro de idempotencia. | Envía un cuerpo JSON o multipart normal — se rechazan los cuerpos chunked que no se pueden almacenar en búfer. |
| 409 | IDEMPOTENCY_PAYLOAD_CONFLICT | Esta clave ya se usó con otro cuerpo, otra ruta u otra query. | Genera una clave nueva. Una clave identifica una solicitud exacta, no un bucle de reintentos. |
| 409 | IDEMPOTENCY_IN_PROGRESS | La primera solicitud con esta clave sigue en ejecución. | Espera el Retry-After (2s) y reintenta con la misma clave — obtendrás la respuesta original. |
| 409 | IDEMPOTENCY_RESULT_UNREPLAYABLE | La solicitud original terminó, pero su respuesta quedó sellada y no se puede reproducir. | No reenvíes a ciegas. Concilia primero desde el endpoint de la tarea o con tus propios registros. |
| 503 | IDEMPOTENCY_UNAVAILABLE | El registro de idempotencia no está disponible, así que el control falla en modo cerrado en lugar de arriesgarse a un cobro doble. | Reintenta con backoff — la solicitud no se ejecutó. |
#Bucle de reintentos
Una clave por trabajo, reutilizada en cada intento, y el 409 en curso tratado como "esperar", no como "fallo".
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")Guarda el id de la tarea antes de reintentar nada
¿Sigues con el problema?
Envíanos el ID de solicitud o el mensaje de error y lo revisaremos.