每一个字段,
都与实现完全一致。
/api/v1 下有三条路由,分别负责提交、轮询与上传。下面写的是接口今天真正执行的约定——没有规划中的参数,也没有取整过的限额。
这些路由只服务本站自己的页面。没有 API key、没有 bearer token、没有 webhook,也没有回调地址;请求以匿名浏览器会话为作用域,从你自己的服务器或其他域名调用不会成功。本页是把现有约定写清楚以便审阅,不是一份可以直接对接的集成文档。
没有密钥,只有会话 Cookie。
路径相对当前站点,不带 Authorization 头。服务端在首次调用时下发 httpOnly 会话 Cookie,并以它界定每个任务的归属。请求体超过 100 KB 会在解析之前被拒绝。
| 请求头 | 方向 | 说明 |
|---|---|---|
| Content-Type: application/json | 请求 | 两条 POST 路由都必须携带。请求体解析不出 JSON 会在任何校验之前被拒。 |
| Idempotency-Key | 请求 | 可选,16–100 个字符,取值范围为 A–Z、a–z、0–9、下划线与连字符。同一个 key 配相同请求体重放会返回原任务;配不同请求体则返回 idempotency_conflict。不带这个头时服务端会自动生成一个,也就等于没有重放保护。 |
| X-Request-Id | 请求 | 可选的关联 id,最长 64 个字符,只允许字母、数字、点、下划线与连字符。不符合的会被丢弃并重新生成。 |
| Cookie: vv_anon | 请求 | 首次调用时由服务端下发,保留一年。它标识拥有任务的匿名会话;换一个会话去轮询会得到 task_not_found。它不是账号,也不携带任何余额。 |
| x-request-id | 响应 | 每个响应都会回带,成功失败都有。反馈问题时请附上它。 |
| Cache-Control: no-store | 响应 | 所有响应都不可缓存——轮询期间任务状态会在你脚下变化。 |
| Retry-After | 响应 | 需要等待的秒数,随 429 rate_limited 一起返回,与响应体里的 retryAfterSeconds 对应。 |
三条路由,其中两条可用
只实现了下表列出的方法。没有列表接口,没有取消接口,也没有回调注册。
| 端点 | 方法 | 作用 | 状态 |
|---|---|---|---|
| /api/v1/videos | POST | 校验请求体、预留匿名额度、提交给视频供应商,并返回供应商任务 id。返回 202 而不是 200——此时视频还不存在。 | 可用 · 同源 |
| /api/v1/videos/{taskId} | GET | 返回任务的当前状态,且只对创建它的会话返回。持续轮询直到状态变成 completed 或 failed。 | 可用 · 同源 |
| /api/v1/uploads | POST | 对象存储尚未开通,这条路由固定返回 503 not_configured,reason 为 upload_storage。不接收任何字节,也不做任何转发。正因如此,创建接口同样会拒绝任何带 url 的附件条目。 | 尚未接通 |
请求体就是一份 composer 状态
要生成什么、用哪个模型、多大尺寸。服务端会重跑浏览器里那套校验,面板拒绝的参数在这里同样会被拒绝。
| 字段 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
| prompt | string | 必填 | 画面描述。哪怕内容为空,这个键也必须存在且为字符串;服务端会做 trim,长度上限 2,000 个字符。空提示词是否允许取决于当前模式。 |
| model | "vivid-1" | "seedance-2-5" | "kling-3" | "veo-3-1" | "minimax-h3-max" | "wan-3" | "kling-motion" | 必填 | 模型 key。传未知的 key 会立刻失败;kling-motion 边界层接受,但面板里不提供。旧版的数字索引写法仍然兼容。 |
| mode | "text" | "image" | "reference" | "extend" | "edit" | "motion" | 必填 | 必须是所选模型真正支持的模式,否则在预留任何额度之前就会被拒。 |
| resolution | "480P" | "720P" | "768P" | "1080P" | "4K" | 必填 | 必须是该模型列出的取值,严格匹配,P 为大写。 |
| aspect | "auto" | "16:9" | "9:16" | "1:1" | "4:3" | "3:4" | "21:9" | null | 必填 | 只有在该模式会按输入自适应画幅时才允许传 null,此时服务端会替换成 16:9;否则必须是列出的取值之一。 |
| duration | number | "auto" | null | 必填 | 整数秒,且落在模型允许的区间内。只有模型允许时才能用 "auto",匿名调用一律不允许。只有不提供时长控制的模型才接受 null,服务端会替换成 5。 |
| audio | boolean | 必填 | 必填布尔值,即使模型没有音频开关也要传。音频影响单价时,费用估算会随之变化。 |
| media | object[] | 可选 | 附件引用,最多 50 条,纯文本模式下会被整体忽略。每条需要 id、kind、role、url、name 与 bytes,可以再带 mime、width、height 与 seconds;role 取值为 ref, start, end, motion, sound, source, character。由于上传尚未接通,任何 url 不为 null 的条目都会被拒:未配置上传域时,边界校验直接以 400 validation_failed 拒绝;只有在上传域已配置、而对象存储仍未接通时,才会返回 503 not_configured。 |
| advanced | object | 可选 | 供应商侧的高级参数,白名单为 watermark, watermarkText, webSearch, seed, quality, translate, orientation。取值只能是字符串、数字或布尔值,出现任何其他键都会导致整个请求被拒。 |
| source | object | null | 按需 | 对上一个任务的续作:一个 token 加一个 kind,kind 为 "lastFrame" 或 "extend"。token 是任务完成时轮询接口签发的签名声明——裸的供应商任务 id 一律不收。lastFrame 需要 image 模式,extend 需要 Veo 3.1 的 extend 模式,同时服务端必须配置好续作签名密钥。 |
# The response cookie scopes the task to this anonymous session.
curl -X POST https://vgent.com/api/v1/videos \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
--cookie-jar vgent.cookies \
-d '{"model":"vivid-1","mode":"text","prompt":"Neon koi swimming through a flooded Tokyo alley","resolution":"720P","aspect":"16:9","duration":5,"audio":true,"media":[],"advanced":{},"source":null}'
# Poll with the same cookie; any other session gets task_not_found.
curl https://vgent.com/api/v1/videos/b7f3c0a4-51d2-4c8e-9a17-2f6d0e5b3c91 --cookie vgent.cookiesconst created = await fetch('/api/v1/videos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
model: 'vivid-1',
mode: 'text',
prompt: 'Neon koi swimming through a flooded Tokyo alley',
resolution: '720P',
aspect: '16:9',
duration: 5,
audio: true,
media: [],
advanced: {},
source: null,
}),
});
const { data, error } = await created.json();
if (error) throw new Error(`${error.code}: ${error.message}`);
// 202 Accepted returns { taskId, model }; the video does not exist yet.
let task;
do {
await new Promise((resolve) => setTimeout(resolve, 3000));
const polled = await fetch(`/api/v1/videos/${data.taskId}`, { cache: 'no-store' });
task = (await polled.json()).data;
} while (task.status === 'pending' || task.status === 'processing');
// A failed generation still arrives as HTTP 200 with task.error set.
console.log(task.status === 'completed' ? task.videoUrl : task.error);所有状态共用一种结构
每个阶段返回的都是同一个对象,字段随任务推进逐步填上。工作台每 3 秒轮询一次,超过 20 分钟放弃。
| 字段 | 类型 | 说明 |
|---|---|---|
| taskId | string | 供应商任务 id:8 到 64 个字符,只含字母、数字与连字符。格式不对会在调用供应商之前就被拒。 |
| status | "pending" | "processing" | "completed" | "failed" | pending 与 processing 是非终态,completed 与 failed 是终态。到终态就停止轮询。 |
| model | string | 任务真正运行时使用的供应商模型 id,例如 doubao-seedance-2.0——不是你传进来的模型 key。 |
| videoUrl | string | null | 只有状态变成 completed 后才有值。它指向供应商托管的产物,会过期,需要的内容请尽快取走。 |
| creditsConsumed | number | null | 任务实际消耗的供应商积分,由供应商上报。它不是界面上显示的 Vgent 积分;后者是这个数字乘以 0.75 再向上取整。 |
| providerCredits | number | null | 同一个供应商数值的另一个显式命名。两个字段都是供应商口径,不是两种货币。 |
| lastFrameUrl | string | null | 尾帧地址,前提是模型返回了尾帧且任务已完成。lastFrame 续作就是从它接着往下生成的。 |
| ratio | string | null | 供应商上报的输出画幅,若它有上报。 |
| resolution | string | null | 供应商标注的输出分辨率,可能是小写,也可能与你请求的不一致。 |
| duration | number | null | 供应商测得的输出时长(秒),可能与请求的时长有出入。 |
| outputFormat | string | null | 供应商产出的封装格式,例如 mp4。 |
| sourceToken | string | null | 签名续作 token。只有任务完成、且服务端持有签名密钥时才会出现;完成后 24 小时过期。 |
| progress | number | null | 供应商上报进度时为 0 到 100,会被夹在这个区间内。null 表示供应商没有给出进度。 |
| estimatedSeconds | number | null | 供应商自己对剩余耗时的估计,若它有提供。 |
| createdAt | number | Unix 秒。 |
| completedAt | number | null | Unix 秒;供应商未上报完成时间之前为 null。 |
| error | object | null | 生成失败时由供应商给出的 code 与 message。它解释的是生成本身而不是这次 HTTP 调用:任务失败时 HTTP 依然是 200。 |
{
"data": {
"taskId": "b7f3c0a4-51d2-4c8e-9a17-2f6d0e5b3c91",
"status": "completed",
"model": "doubao-seedance-2.0",
"videoUrl": "https://cdn.aivideoapi.ai/outputs/b7f3c0a4.mp4",
"creditsConsumed": 190,
"providerCredits": 190,
"lastFrameUrl": "https://cdn.aivideoapi.ai/outputs/b7f3c0a4-last.jpg",
"ratio": "16:9",
"resolution": "720p",
"duration": 5,
"outputFormat": "mp4",
"sourceToken": "eyJ0YXNrSWQiOiJiN2YzYzBhNCIsImV4cCI6MTc4OTE3MTgxMn0.dGFza190b2tlbl9zaWduYXR1cmU",
"progress": 100,
"estimatedSeconds": 0,
"createdAt": 1789084800,
"completedAt": 1789085412,
"error": null
},
"error": null
}成功失败共用一个信封
成功时数据放在 data,error 为 null;失败时反过来:data 为 null,error 里是一个稳定的机器可读 code。每个响应还会带上请求 id,并标记为 no-store。
错误 message 是写给日志看的英文句子。工作台按 code 映射到自己的多语言文案,从不把原始 message 展示给访客。基于这些接口做开发也应该照此处理。
| 字段 | 类型 | 说明 |
|---|---|---|
| code | string | 稳定标识,完整清单见下表。真正值得据以分支的就是它。 |
| message | string | 英文、面向日志,可能随时调整,不属于约定的一部分。 |
| reason | string? | 部分失败会带上,用来指出是哪一部分出了问题:哪个依赖没配置、续作 token 因何被拒,或者触发了哪条校验规则。 |
| retryAfterSeconds | number? | 只在 429 rate_limited 时出现,与 Retry-After 响应头一致。 |
{
"data": {
"taskId": "b7f3c0a4-51d2-4c8e-9a17-2f6d0e5b3c91",
"model": "doubao-seedance-2.0"
},
"error": null
}{
"data": null,
"error": {
"code": "rate_limited",
"message": "Too many requests.",
"retryAfterSeconds": 47
}
}每个状态码在这里的含义
其中两个含义比通常更重要:创建返回的是 202 而不是 200;生成失败时依然返回 200,失败信息放在 data 里。
| 状态码 | 含义 |
|---|---|
| 200 | 任务读取成功。这也包括生成失败的任务,所以取用产物前请先看 data.status。 |
| 202 | 任务已被接受并提交给供应商,响应体是任务 id 与供应商模型。重放同一个 Idempotency-Key 会再次返回原来的 202。 |
| 400 | 请求体没通过边界校验、任务 id 格式不对、同一个幂等 key 配了不同参数,或者续作 token 被拒。 |
| 402 | 为本站供资的供应商账户积分耗尽或触及消费上限。没有向你收取任何费用。 |
| 403 | 请求本身合法,但超过了匿名单条 300 供应商积分的上限,或使用了匿名调用不允许的附件。 |
| 404 | 该 id 的任务不属于你的会话,或者供应商没有这条记录。 |
| 409 | 同一个 Idempotency-Key 的上一次请求已经打到供应商,但始终没有对账成功,因此没有任务 id 可以返回。等那次提交被记录后即会恢复;换新 key 意味着再付一次生成费用。 |
| 429 | 十分钟滚动窗口内提交次数过多,或视频供应商正在对本站限流。只有前一种情况会带 Retry-After 与 retryAfterSeconds。 |
| 500 | 未处理的服务端错误。不会暴露任何内部细节;请求 id 在响应头和日志里都有。 |
| 502 | 供应商有响应,但内容不可用:格式错误的载荷、被拒的凭证,或映射不到更具体分类的错误。 |
| 503 | 有依赖未配置——供应商密钥、上传、续作签名或持久化限流存储——也可能是共享的当日额度已经用完。 |
| 504 | 供应商在 30 秒内没有响应,或者完全无法连通。 |
接口会返回的全部错误码
下面就是 error.code 里会出现的确切字符串。这个清单是封闭的:出现别的值就是 bug。
| 错误码 | HTTP | 触发场景 |
|---|---|---|
| validation_failed | 400 | 某个字段没通过校验。reason 会指出触发的规则,例如缺少提示词、时长超出区间,或所选模型不支持该画幅。 |
| invalid_request | 400 | URL 里的任务 id 不是 8 到 64 个字母、数字与连字符,或者供应商认为提交本身不合法。 |
| idempotency_conflict | 400 | 同一个 Idempotency-Key 配了不同的请求体。换新参数就换新 key。 |
| source_invalid | 400 | 续作 token 格式错误、已过期、签给了别的会话或模型,或者指向的任务已不再符合条件。reason 区分具体情形。 |
| insufficient_credits | 402 | 为本站供资的供应商账户已无积分。与你这条请求无关。 |
| spend_limit_exceeded | 402 | 本站配置的供应商消费上限已经触顶。 |
| free_limit_exceeded | 403 | 这条请求的费用超过匿名单条上限,或附带了匿名调用不能发送的素材。自动时长也会落到这里,因为它的费用无法事先封顶。 |
| task_not_found | 404 | 任务不属于你的会话,或者供应商没有这条记录。两种情形被刻意做成无法区分。 |
| submission_pending | 409 | 使用这个幂等 key 的上一次提交已经打到供应商,但响应丢失了,因此记录被刻意保留且没有任务 id。用同一个 key 重试仍会得到这个错误,而不会重复扣费。 |
| rate_limited | 429 | 匿名窗口按客户端地址限制为每十分钟 5 次提交。retryAfterSeconds 会告诉你要等多久。 |
| provider_rate_limited | 429 | 是视频供应商在对本站限流,而不是在限你。稍后重试即可。 |
| internal_error | 500 | 接口内部出现意料之外的失败。请附上响应头里的请求 id。 |
| provider_error | 502 | 供应商返回了映射不到更具体分类的错误。 |
| provider_bad_response | 502 | 供应商返回了非 JSON 响应、没有任务 id,或任务载荷缺少 id / status。 |
| provider_auth | 502 | 供应商拒绝了本服务器的凭证或来源地址。这是我们这边的配置问题,不是你的问题。 |
| not_configured | 503 | 有依赖缺失。reason 区分上传、续作签名、持久化限流存储与供应商密钥。 |
| free_budget_exhausted | 503 | 匿名生成的共享当日额度已用完,跨过 UTC 日界后重置。 |
| model_offline | 503 | 供应商当前不提供所请求的模型。 |
| provider_unreachable | 504 | 完全连不上供应商。 |
| provider_timeout | 504 | 供应商在 30 秒内没有响应。上游可能已经建好了任务,因此幂等记录会被保留而不是释放,用同一个 key 重试会得到 submission_pending。 |
匿名调用能拿到什么
目前还没有账号体系,所有调用都是匿名的,共用同一套限额。接口内部按供应商积分计费,只在展示时才做换算。
| 限制项 | 取值 | 说明 |
|---|---|---|
| 免费额度 | 200 Vgent credits | 面向新访客展示的额度,单位是 Vgent 积分。它是展示口径,服务端并不为你保存余额。 |
| 单条上限 | 300 | 供应商积分。估算超过这个数的请求会在提交之前以 free_limit_exceeded 被拒。 |
| 每 10 分钟提交数 | 5 | 按客户端地址计算的滚动窗口。超出会返回 rate_limited,并带上 Retry-After。 |
| 共享当日预算 | 20,000 | 所有匿名调用共享的供应商积分,按 UTC 日计。用完后提交会返回 free_budget_exhausted,直到跨日重置。部署时可以配置成别的数值。 |
| 单次图片数 | 2 | 匿名调用最多能附带这么多张图片。 |
| 视频与音频附件 | 0 | 匿名调用完全不能附带视频或音频,出现一条就会拒绝整个请求。 |
| 提示词长度 | 2,000 | 字符数,当前列出的所有模型都是这个值。请求会按所选模型自己的上限来校验。 |
| 请求体大小 | 100 KB | 字节数,在解析之前就会测量。超过的请求根本不会被读取。 |
| 幂等记录 | 24 h | 幂等 key、请求指纹与任务归属会被记住这么久,重放才能拿回原来的任务。 |
| 积分换算 | × 0.75 | Vgent 积分 = 供应商数值乘以它再向上取整。 |
限流、当日预算与幂等记录都存放在兼容 Redis 的 REST 存储里。没有它,生产部署会直接拒绝付费提交;开发模式会退回进程内存,重启后所有记录都会丢失。
接下来看什么
参考文档的其余部分会讲清楚这些字段在实际使用中意味着什么。