aisplice

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.
submit.sh
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

A UUID generated inside the retry loop defeats the purpose — each attempt gets a new identity. Derive it from the business row you are fulfilling (order id, job id) so every retry of that job carries the same key.

#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.

HTTPCodeMeaningWhat to do
400IDEMPOTENCY_STREAM_UNSUPPORTEDIdempotency-Key was sent on a streaming request.Drop the header for streaming calls, or set stream:false when you need replay protection.
400IDEMPOTENCY_BODY_UNREADABLEThe body could not be hashed for the idempotency ledger.Send a normal JSON or multipart body — chunked bodies that cannot be buffered are rejected.
409IDEMPOTENCY_PAYLOAD_CONFLICTThis 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.
409IDEMPOTENCY_IN_PROGRESSThe first request with this key is still running.Wait for Retry-After (2s) and retry the same key — you will get the original response.
409IDEMPOTENCY_RESULT_UNREPLAYABLEThe 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.
503IDEMPOTENCY_UNAVAILABLEThe 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".

submit_once.py
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

Write task_id to your database in the same transaction that marks the job submitted. The key protects the submit; only your own record protects you after the reply is lost.

Async task lifecycle →

Still stuck?

Send us the request ID or the error message and we will look into it.