Permalink to Asynchronous jobsAsynchronous jobs

Images, video and music are asynchronous. The create endpoint does not wait for the artifact: it validates your request, reserves credits, writes a durable job and answers 202 Accepted immediately. You then poll for the job's status and, once it succeeds, ask for signed download links.

This is the part of the API most likely to surprise you, so it is worth reading before you write any integration code.

text
POST /v1/images/generations        ─┐
POST /v1/images/edits               │
POST /v1/videos                     ├─► 202 { "id": "job_…", "status": "queued" }
POST /v1/audio/music               ─┘

                     GET /v1/jobs/{job_id}          ← poll until terminal

                     GET /v1/jobs/{job_id}/assets   ← signed URLs, 15 minutes

Chat completions, speech synthesis and transcription are not jobs — they return their result in the response body. Only the three generative-media surfaces above use this ladder.

Permalink to the-job-envelopeThe job envelope

Every create endpoint and every status read returns the same object:

json
{
  "id": "job_2f81c0a4d3b57e9016ab24cf",
  "status": "queued",
  "kind": "video",
  "model": "bytedance/seedance-2",
  "progress": 0,
  "created_at": 1787824589,
  "updated_at": 1787824589,
  "finished_at": 0,
  "dispatch_deadline_at": 1787825189,
  "error_code": "",
  "error": ""
}
FieldNotes
idopaque job id, prefixed job_. The only handle you need
statussee the state table below
kindimage, video or audio
modelthe model you asked for, not the upstream one we routed to
progress0100. Advisory: many upstreams only ever report 0 then 100
created_atUnix seconds
updated_atomitted while unset
finished_atUnix seconds, present once terminal
dispatch_deadline_atthe moment the job must have reached an upstream by
error_code, errorpresent on a non-success terminal, omitted otherwise

Permalink to statesStates

text
queued ──► running ──► succeeded
   │           └─────► failed
   └─► queue_expired
StatusTerminalMeaning
queuednoaccepted, credits reserved, waiting for upstream capacity
runningnoan upstream has accepted the job and is generating
succeededyesassets are stored and downloadable
failedyesgeneration failed, or succeeded upstream but produced nothing we could deliver
queue_expiredyeswe never got the job onto an upstream before dispatch_deadline_at

canceled exists in the schema for historical rows only. No new job can enter it.

There is no cancel endpoint

Once a job is created it runs to a terminal state. Closing your client, dropping the connection or stopping the poll only stops you observing — it does not stop the job, and it does not refund it. Any cancel-shaped URL you may find in older material returns a plain 404.

Permalink to the-dispatch-deadlineThe dispatch deadline

dispatch_deadline_at is ten minutes after creation. It bounds only the queueing phase: the time we have to find an upstream with free capacity and hand the job over. Generation itself is not bounded by it — a job that reaches an upstream at the 599th second is fine, and long clips routinely run well past the deadline in running.

If the deadline passes while the job is still queued, it terminates as queue_expired and every reserved credit is returned. That is the only timeout you can see from the outside.

Permalink to pollingPolling

There is no customer-facing webhook. Poll GET /v1/jobs/{job_id}:

bash
curl https://hypit.ai/v1/jobs/job_2f81c0a4d3b57e9016ab24cf \
  -H "Authorization: Bearer $HYPIT_API_KEY"

Reasonable cadence: first check after ~5 seconds, then every 3–10 seconds, backing off for long video. Images typically settle in tens of seconds, video in minutes.

python
import os, time, httpx

BASE = os.environ.get("HYPIT_BASE_URL", "https://hypit.ai/v1")
AUTH = {"Authorization": f"Bearer {os.environ['HYPIT_API_KEY']}"}
TERMINAL = {"succeeded", "failed", "queue_expired"}

def wait_for(job_id: str, timeout: float = 900.0) -> dict:
    deadline = time.monotonic() + timeout
    delay = 3.0
    while True:
        r = httpx.get(f"{BASE}/jobs/{job_id}", headers=AUTH, timeout=30)
        r.raise_for_status()
        job = r.json()
        if job["status"] in TERMINAL:
            return job
        if time.monotonic() > deadline:
            raise TimeoutError(f"{job_id} still {job['status']}")
        time.sleep(delay)
        delay = min(delay * 1.4, 15.0)
js
const BASE = process.env.HYPIT_BASE_URL ?? "https://hypit.ai/v1";
const AUTH = { Authorization: `Bearer ${process.env.HYPIT_API_KEY}` };
const TERMINAL = new Set(["succeeded", "failed", "queue_expired"]);

export async function waitFor(jobId, timeoutMs = 900_000) {
  const deadline = Date.now() + timeoutMs;
  let delay = 3_000;
  for (;;) {
    const r = await fetch(`${BASE}/jobs/${jobId}`, { headers: AUTH });
    if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
    const job = await r.json();
    if (TERMINAL.has(job.status)) return job;
    if (Date.now() > deadline) throw new Error(`${jobId} still ${job.status}`);
    await new Promise((res) => setTimeout(res, delay));
    delay = Math.min(delay * 1.4, 15_000);
  }
}

Permalink to collecting-the-resultCollecting the result

A succeeded job's artifacts are fetched from the assets route. Nothing is public: we sign a short-lived URL for you, on demand, against the exact stored object.

bash
curl https://hypit.ai/v1/jobs/job_2f81c0a4d3b57e9016ab24cf/assets \
  -H "Authorization: Bearer $HYPIT_API_KEY"
json
{
  "items": [
    {
      "id": "ast_9c3d1e7a44b0",
      "job_id": "job_2f81c0a4d3b57e9016ab24cf",
      "ordinal": 0,
      "kind": "video",
      "storage": "s3",
      "mime_type": "video/mp4",
      "bytes": 4718592,
      "width": 1920,
      "height": 1080,
      "seconds": 6,
      "url": "https://…?X-Amz-Signature=…",
      "expires_at": 1787825489,
      "created_at": 1787825189
    }
  ]
}
FieldNotes
idstable asset id
ordinalposition within the job; n: 3 on an image job yields ordinals 0, 1, 2
kindimage, video, audio or thumbnail
storagealways s3
width, height, secondspresent when known for the medium
urla freshly signed link, valid for 15 minutes
expires_atUnix seconds when url stops working

The response carries Cache-Control: private, no-store. Do not persist a signed url — persist the job_id and re-sign by calling this route again. Signed links are minted at read time from the stored bucket and object key, so calling it again always works while the artifact is retained.

Successful outputs are retained for 30 days from the moment the job reaches its terminal state. Download or copy anything you need to keep.

python
job = wait_for(job_id)
if job["status"] != "succeeded":
    raise RuntimeError(f"{job['status']}: {job.get('error_code')} {job.get('error')}")

assets = httpx.get(f"{BASE}/jobs/{job_id}/assets", headers=AUTH, timeout=30).json()["items"]
for a in assets:
    with httpx.stream("GET", a["url"], timeout=None, follow_redirects=True) as src:
        src.raise_for_status()
        with open(f"out-{a['ordinal']}.{a['mime_type'].split('/')[-1]}", "wb") as f:
            for chunk in src.iter_bytes():
                f.write(chunk)

Permalink to modality-aliasesModality aliases

For readability, three pairs of aliases point at exactly the same handlers:

AliasSame as
GET /v1/videos/{job_id}GET /v1/jobs/{job_id}
GET /v1/audio/music/{job_id}GET /v1/jobs/{job_id}
GET /v1/audio/music/{job_id}/contentthe same

The audio /content route is a convenience for curl -L. Video downloads use the owner-scoped assets API, which returns fresh signed URLs and answers 409 job_asset_not_ready when the job has no deliverable asset yet.

bash
curl https://hypit.ai/v1/jobs/job_2f81c0a4d3b57e9016ab24cf/assets \
  -H "Authorization: Bearer $HYPIT_API_KEY"

Permalink to idempotencyIdempotency

Send an Idempotency-Key header on any create call and a retry becomes safe:

bash
curl https://hypit.ai/v1/videos \
  -H "Authorization: Bearer $HYPIT_API_KEY" \
  -H "Idempotency-Key: order-4471-clip-1" \
  -H "Content-Type: application/json" \
  -d '{"model": "bytedance/seedance-2", "prompt": "a paper boat in a gutter", "seconds": 5}'

The key is scoped to your account and credential, and is at most 512 bytes.

SituationResult
same key, same requestthe original job is returned; nothing is charged twice
same key, different request409 idempotency_conflict
same key, first request still preparing425 admission_in_progress — retry shortly
no keyevery call creates a new job, even for identical bodies

Replay is decided from a durable fingerprint of the request, so a model that is later withdrawn does not change the answer you get back for a key you already used.

Permalink to concurrency-limitsConcurrency limits

One account may hold at most 64 queued or running jobs at a time, counted across every API key, OAuth grant and console session it owns. Exceeding it is a 429:

json
{
  "error": {
    "message": "too many queued or running media Jobs for this account; retry after one finishes",
    "type": "rate_limit_error",
    "code": "media_open_job_capacity",
    "param": null
  }
}

The response carries Retry-After: 30. This is separate from the per-key request rate limit described in Errors and limits.

Permalink to what-happens-to-your-creditsWhat happens to your credits

Submitting a job reserves credits from an estimate. When the job reaches a terminal state:

  • succeeded — the reservation is settled against the real usage. Under the estimate, the difference is refunded; over it, the difference is charged.
  • failed — everything that was actually taken is refunded, in full, to the same grants it came from. The job's cost columns go to zero.
  • queue_expired — the same full refund.

That includes the case where an upstream generated something but we could not deliver it: the job fails, and you are not charged for an artifact you cannot download. See Credits and billing for the mechanism.

Permalink to job-read-errorsJob read errors

StatuscodeMeaning
404job_not_foundunknown id — or a job belonging to another account. The two are deliberately indistinguishable
409job_asset_not_readyon /content only: no deliverable asset yet
503job_service_unavailablestorage or signing is briefly unavailable; retry