API reference
Avoid double charges
Idempotency means a retried request is carried out once: send the same Idempotency-Key with the retry and you are charged once, even if the first attempt only looked lost.
A timed-out request is not a failed request. The generation may already be running, and a blind retry starts — and bills — a second one. Send an Idempotency-Key and the gateway executes the request once, then replays the same response for 24 hours.
#When to send a key
Send one
- Every async submit — video, image task, music, 3D. These are the expensive ones.
- Any non-streaming call your worker retries automatically.
- Calls behind a queue that can redeliver a job after a crash.
Skip it
- Streaming requests — the header is rejected with 400, a stream cannot be replayed.
- GET polling and metadata reads — repeating them is already free and safe.
- Interactive chat where a duplicate answer costs less than the bookkeeping.
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"
}'Derive the key from your own record
#How keys are matched
Opt-in, never automatic
Without the header nothing changes. A retry with no key is a second billable request.
Write methods only
GET, HEAD and OPTIONS ignore the header — polling a task is already safe to repeat.
Scoped to user + key + method + route
The same value under a different API key, or on a different endpoint, is a different operation.
The body is part of the identity
Path, query and body are hashed together. Reusing a key with a changed payload returns 409, it does not overwrite.
Not compatible with streaming
A streamed response cannot be stored for replay, so stream:true plus a key is rejected with 400.
Fails closed
If the ledger is unavailable the request is refused with 503 rather than executed unprotected.
Keys are at most 255 characters. Anything longer is rejected with 400 before the request runs.
#Outcomes and status codes
A replayed response carries X-Idempotent-Replayed: true and the original status — treat it as the answer, not as a duplicate to reconcile. Every rejection uses the flat guard envelope.
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 400 | IDEMPOTENCY_STREAM_UNSUPPORTED | Idempotency-Key was sent on a streaming request. | Drop the header for streaming calls, or set stream:false when you need replay protection. |
| 400 | IDEMPOTENCY_BODY_UNREADABLE | The body could not be hashed for the idempotency ledger. | Send a normal JSON or multipart body — chunked bodies that cannot be buffered are rejected. |
| 409 | IDEMPOTENCY_PAYLOAD_CONFLICT | This key was already used with a different body, path or query. | Generate a new key. A key identifies one exact request, not one retry loop. |
| 409 | IDEMPOTENCY_IN_PROGRESS | The first request with this key is still running. | Wait for Retry-After (2s) and retry the same key — you will get the original response. |
| 409 | IDEMPOTENCY_RESULT_UNREPLAYABLE | The original request finished but its response was sealed and cannot be replayed. | Do not resubmit blindly. Reconcile through the task endpoint or your own records first. |
| 503 | IDEMPOTENCY_UNAVAILABLE | The idempotency ledger is unavailable, so the guard fails closed rather than risk a double charge. | Retry with backoff — the request was not executed. |
#Retry loop
One key per job, reused across every attempt, with 409 in-progress handled as "wait", not as "fail".
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")Persist the task id before you retry anything
Still stuck?
Send us the request ID or the error message and we will look into it.