图片、视频和音乐都是异步的。创建端点不会等产物:它校验请求、预扣额度、写入一条持久化的
Job,然后立刻返回 202 Accepted。之后由你轮询任务状态,成功后再去要带签名的下载链接。
这是整套 API 里最容易让接入方意外的一块,动手写代码之前值得先读完。
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} ← 轮询到终态
│
GET /v1/jobs/{job_id}/assets ← 签名 URL,15 分钟有效Chat completions、语音合成和转写不是 Job,结果直接在响应体里返回。只有上面这三类生成式媒体走 这条链路。
Permalink to job-信封Job 信封
每个创建端点和每次状态查询返回的都是同一个对象:
{
"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": ""
}| 字段 | 说明 |
|---|---|
id | 不透明任务 id,前缀 job_。你只需要拿住这一个句柄 |
status | 见下面的状态表 |
kind | image、video 或 audio |
model | 你请求的模型名,不是我们实际路由到的上游模型名 |
progress | 0–100。仅供参考:很多上游只会报 0 然后 100 |
created_at | Unix 秒 |
updated_at | 未设置时省略 |
finished_at | Unix 秒,进入终态后出现 |
dispatch_deadline_at | 任务必须在此之前被投递到上游 |
error_code、error | 非成功终态时出现,其余情况省略 |
Permalink to 状态机状态机
queued ──► running ──► succeeded
│ └─────► failed
└─► queue_expired| 状态 | 终态 | 含义 |
|---|---|---|
queued | 否 | 已受理、额度已预扣,正在等上游产能 |
running | 否 | 上游已接收,正在生成 |
succeeded | 是 | 产物已落盘,可以下载 |
failed | 是 | 生成失败,或上游成功但没有任何我们能交付的产物 |
queue_expired | 是 | 在 dispatch_deadline_at 之前始终没能投递到上游 |
canceled 只为解码历史数据保留,新任务不会进入这个状态。
没有取消接口
Job 一旦创建就会跑到终态。关掉客户端、断开连接或停止轮询只会让你不再观察它,既不会停止任务,
也不会因此退款。你在旧资料里看到的任何 cancel 形状的 URL,都只会返回一个普通的 404。
Permalink to 投递期限投递期限
dispatch_deadline_at 是创建后十分钟。它只约束排队这一段:我们必须在这段时间内找到有空闲
产能的上游并把任务交出去。生成本身不受它约束——第 599 秒才进入上游也没问题,长视频在 running
状态下跑过这个时间点是常态。
如果期限到了任务还停在 queued,它会以 queue_expired 终结,并且预扣的额度全额返还。这是从
外部唯一能看到的超时。
Permalink to 轮询轮询
面向客户没有 webhook,请轮询 GET /v1/jobs/{job_id}:
curl https://hypit.ai/v1/jobs/job_2f81c0a4d3b57e9016ab24cf \
-H "Authorization: Bearer $HYPIT_API_KEY"合理的节奏:首次约 5 秒后查,之后每 3–10 秒一次,视频再拉长退避。图片通常几十秒出结果,视频以 分钟计。
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} 仍处于 {job['status']}")
time.sleep(delay)
delay = min(delay * 1.4, 15.0)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} 仍处于 ${job.status}`);
await new Promise((res) => setTimeout(res, delay));
delay = Math.min(delay * 1.4, 15_000);
}
}Permalink to 取产物取产物
succeeded 的任务从 assets 路由取产物。产物一律不公开:我们按需为你在当次请求里对确切的存储
对象签一个短期 URL。
curl https://hypit.ai/v1/jobs/job_2f81c0a4d3b57e9016ab24cf/assets \
-H "Authorization: Bearer $HYPIT_API_KEY"{
"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
}
]
}| 字段 | 说明 |
|---|---|
id | 稳定的产物 id |
ordinal | 该任务内的序号;图片任务 n: 3 会得到序号 0、1、2 |
kind | image、video、audio 或 thumbnail |
storage | 恒为 s3 |
width、height、seconds | 该媒介已知时出现 |
url | 现签的链接,有效期 15 分钟 |
expires_at | url 失效的 Unix 秒 |
响应带 Cache-Control: private, no-store。不要持久化 url,请持久化 job_id,需要时重新调用
本路由重签。签名从存储的 bucket 与 object key 现场生成,只要产物还在保留期内,重签总是成功。
成功产物从任务进入终态起保留 30 天。需要长期保存的请自行下载或转存。
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 模态别名模态别名
为了可读性,有三对别名指向完全相同的处理逻辑:
| 别名 | 等价于 |
|---|---|
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}/content | 同上 |
音频 /content 是给 curl -L 用的便利路由。视频下载统一使用按归属校验的 assets 接口,
返回新的签名 URL;任务还没有可交付产物时返回 409 job_asset_not_ready。
curl https://hypit.ai/v1/jobs/job_2f81c0a4d3b57e9016ab24cf/assets \
-H "Authorization: Bearer $HYPIT_API_KEY"Permalink to 幂等幂等
在任意创建请求上带 Idempotency-Key,重试就变安全了:
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": "水沟里漂着一只纸船", "seconds": 5}'这个键的作用域是你的账号加当前凭据,最长 512 字节。
| 情况 | 结果 |
|---|---|
| 同键、同请求 | 返回原来那个 Job,不会重复计费 |
| 同键、不同请求 | 409 idempotency_conflict |
| 同键、上一次仍在准备中 | 425 admission_in_progress,稍后重试 |
| 不带键 | 即使请求体完全相同,每次调用也都创建新任务 |
是否重放由请求的持久化指纹判定,因此某个模型后来下线,也不会改变你用同一个键拿回的结果。
Permalink to 并发上限并发上限
一个账号同时最多持有 64 个 queued 或 running 的任务,跨所有 API Key、OAuth 授权和控制台
会话合并计算。超过就是 429:
{
"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
}
}响应带 Retry-After: 30。这条限制与错误与限流里那条按 Key 的请求速率
限制是两回事。
Permalink to 额度怎么走额度怎么走
提交任务会按估算预扣额度。进入终态时:
succeeded——按真实用量结算。低于估算的部分退回,高于估算的部分补扣。failed——已经实际扣掉的全额退回,退到原来那几笔额度上;任务的费用列归零。queue_expired——同样全额退回。
这也覆盖了那个尴尬的情况:上游确实生成了东西,但我们拿不到可交付的产物时,任务判为失败并退款, 你不会为一个下载不到的产物付钱。机制细节见额度与计费。
Permalink to 查询类错误查询类错误
| 状态 | code | 含义 |
|---|---|---|
404 | job_not_found | id 不存在——或者这个任务属于别的账号。两者刻意不可区分 |
409 | job_asset_not_ready | 仅 /content:还没有可交付的产物 |
503 | job_service_unavailable | 存储或签名短暂不可用,请重试 |