Seedance 视频 API

基址 https://__ORIGIN__。创建任务后轮询状态,再下载 MP4;纯文本即可生成,参考图可选。

字字动画专用接入指南

视频插件配置、模型参数、参考图和任务恢复,按下面的顺序接入。

下载本站通用视频插件,安装后填写地址和 API Key。插件自动读取分镜提示词与参考图片,完成创建、查询和 MP4 下载,支持断网后恢复原任务。

适用于支持 Python 自定义视频插件的字字动画版本。请在视频模型管理器中选择“Seedance 聚合 · 通用视频”。推荐安装包 1.4.2,修复原画下载校验,文件请求或传输中断会自动重试下载,不重新生成。继续支持同镜头批量并发,各条视频独立保存与恢复。模型与时长均为下拉选择。

解压后安装到字字动画的视频插件目录;无需单独安装 Python 依赖。

1. 安装与配置

  1. 在本站账户控制台的 API Key 页面创建并保存 Key,使用 sk- 开头的值。登录密码、登录令牌和充值卡密不能代替它。
  2. 退出字字动画,下载并解压 ZIP,将整个 video_plugin_seedance 文件夹放入安装目录的 _internal/plugins/video_plugins/。最终应能找到 _internal/plugins/video_plugins/video_plugin_seedance/main.py;不要多嵌套一层同名文件夹。
  3. 重启字字动画,在视频模型管理器 → 自定义插件中选择 Seedance 聚合 · 通用视频。菜单名可能随版本变化,设置页面依赖字字动画自带的 PluginSDK。
  4. 本站地址已默认填好,粘贴 API Key 即可。配置页会自动获取模型与时长,直接下拉选择,无需手写;切换模型后时长自动联动。首次选 Seedance 2.0 Fast → 5 秒,分辨率默认 720p;先用纯文本、数量 1,确认后再加参考图和批量任务。
  5. 通过带 Key 的 GET /v1/pricing 确认本人价格,通过 GET /v1/balance 确认余额。批量任务按实际任务数分别计费,具体以账户价格和账单为准。
本站基址(不含 /v1)
https://__ORIGIN__
创建视频的完整地址
https://__ORIGIN__/v1/videos

API Key 输入框只填写 Key 本身;插件发送请求时使用 Authorization: Bearer sk-your-key,JSON 请求使用 Content-Type: application/json。避免拼出重复的 /v1/v1/videos。本站使用 /v1/videos,不是 /v1/chat/completions

模型列表通过 GET /v1/catalog 自动获取,也可点击插件中的重新获取模型与时长;读取目录不扣费。获取失败时保留当前选项,刷新不会擅自改变已选参数。升级至 1.4.2 时先退出字字动画,覆盖安装包中的 main.pyui/index.htmlREADME.md 三个文件,保留原配置和 .seedance-tasks 任务记录,重启后确认配置页显示 1.4.2。

不满意或失败后想重新生成:使用“提交生成”模式,成功下载完成或确认失败后直接再次点击当前镜头的“重新生成”即可,无需改提示词或生成版本;新一版按本人价格计费,旧视频和失败记录保留。同一批量位置、相同参数在处理中或下载中断时仍会恢复原任务。

同镜头批量并发:每镜头可设置多条,两个镜头各 3 条对应 6 个独立任务,按本人价格逐条计费。插件已移除同镜头并发限制,同时执行数量由字字的插件任务设置控制。各条视频单独保存和恢复,请保留原配置及 .seedance-tasks

网站已成功,只补下载原画:选择“仅恢复/下载原视频”,保持原分镜、提示词、图片、模型参数、生成版本、API Key 及素材目录不变再次执行,不会新建一版或重复扣费;不要增加生成版本或点击“准备新版本”。原画保留生成结果的原始码率,当前分辨率仍为 720p,不做放大或二次转码。

2. 字字动画参数与本站字段

插件设置 / 分镜素材本站字段填写规则
视频提示词prompt必填,1–6000 字符;使用当前分镜的视频提示词。
模型modelseedance2.0 / seedance2.0fast:5、10、15 秒;seedance2.0mini:5、10 秒;seedance2.5:30 秒。
时长 / 画面比例duration / ratio时长传整数秒,不能传“15s”;竖屏用 9:16,横屏用 16:9,完整范围见模型与参数
分辨率resolution当前为 720p
本地参考图片image_ids先逐张上传,再提交返回的图片 ID 数组。2.5 最多 30 张,其他模型最多 9 张。
公开图片地址image_urls直接返回图片的 HTTP(S) 地址;不能传本地路径、素材网页地址或需要登录的图片链接。
首尾帧 / 参考视频无专用字段/v1/videos 没有专用首尾帧或参考视频字段;牛来系列的素材接入见全能参考
参考音频audio_b64s可选,按连接顺序提交最多 3 段;支持 MP3、M4A、WAV、OGG、FLAC,单段不超过 50 MiB,合计不超过 60 MiB;也兼容单段字段 audio_b64

3. 自定义视频插件请求协议

  1. 上传图片(可选)。POST /v1/files 发送 {"image_b64":"图片的 Base64"},读取响应的 image_id。使用 JSON,不是 multipart;PNG、JPEG、WebP 单张解码后最多 50 MiB,上传图片有效期一小时。外部图片地址单张最多 25 MiB。详见上传参考图
  2. 创建任务。为每个新生成保存独立操作编号和完整请求体,在 Idempotency-Key 请求头中传入该编号,然后 POST /v1/videos。下面是 15 秒竖屏文生视频的请求体;有参考图时再加入 image_ids
{
  "prompt": "雨后的城市街道,镜头缓慢向前移动,柔和电影光影",
  "model": "seedance2.0fast",
  "duration": 15,
  "ratio": "9:16",
  "resolution": "720p"
}
  1. 保存任务 ID 并轮询。创建返回 HTTP 202,从根字段 id 读取任务编号,不是 task_iddata.id。随后 GET /v1/videos/{id}queuedrunning 表示继续等待,succeeded 才能下载,failed 为本次任务失败。不要把排队或受理当作生成成功。
  2. 下载原画到字字动画素材目录。成功后用同一个 Key 调用 GET /v1/videos/{id}/signed_url?download=1,确认响应 qualityoriginal,再原样使用返回的本站 url 下载,无需附加 Key。链接中的签名、质量与文件标识必须保留,文件响应应为 X-Video-Quality: original。普通 /file、不带 download=1 的签名请求以及任务状态中的 file/download_url 是轻量预览链路。原画暂未就绪时保留已有任务,稍后重新获取原画链接;不要改用预览文件或重新提交生成。插件完整保存 MP4 后才向字字动画报告成功。

4. 批量、超时和积分恢复

  • 支持同镜头批量并发,数量由字字拆分提交,各条独立计费。同一批量位置、相同参数的未完成任务继续恢复原操作;不同位置或改变参数、版本可并行新建。默认“提交生成”模式下,成功下载完成后再次提交会生成新一版;仅补下载时选仅恢复/下载原视频
  • 超时后保持原参数和生成版本,选择“仅恢复/下载原视频”后再次执行。保留作品素材目录中的 .seedance-tasks、原参考图、原地址及原 Key;更换内容、版本或清理任务记录会影响原任务恢复。
  • 建议单次 API 请求等待 60 秒、图片上传 90 秒,任务查询间隔约 5 秒;任务整体等待与单次 HTTP 超时分开处理。插件等待结束不代表服务端任务已取消。
  • 提交超时、断网、空响应或 null 时,保留原 Key、操作编号和请求体,用同一 Idempotency-Key 恢复;收到 id 后只查询该任务。不要因不确定的响应而重新生成操作编号。
  • 确认 failed 后,如决定重做,保持“提交生成”模式,直接再次点击“重新生成”,无需增加生成版本。刚查到失败的本次执行会停止,不会自动连续下单。失败任务的退款金额和到账情况以本站账单为准,不能仅根据插件显示“失败”推断全额退款。

5. 常见接入问题

  • 插件不显示 / 配置页未加载:确认上述目录层级,重启字字动画,并确认版本支持 Python 自定义视频插件。已验证公开插件接口、模拟配置页面和隔离 API 故障恢复;各版本客户端界面仍可能存在差异。
  • 401:检查 Key 是否有效,以及请求头是否为 Authorization: Bearer …
  • 404 / 返回 HTML:核对基址和路径,确认调用了本站视频接口,保留 HTTP 状态和请求编号便于排查。
  • 400 / 422:核对模型对应的时长、整数类型和图片字段;seedance2.0mini 不接受 15 秒。
  • 402:查询本人余额;若原提交结果不明,先恢复原操作,再决定是否充值或创建新任务。
  • 429:根据响应的 error.type 区分并发或每日限制,按 Retry-After(如有)等待;409 idempotency_in_progress 继续恢复原操作。
  • 本站成功、字字动画没有素材或提示“未收到已确认的原画文件”:先升级至 1.4.2,再选择“仅恢复/下载原视频”,保留原参数、生成版本、配置和 .seedance-tasks 补下载。不要增加版本重新付费生成;若仍失败,保留提示中的插件版本和任务编号便于排查。

开始接入

从账户控制台的 API Key 页面创建并复制 sk- 开头的 Key,服务端通过请求头 Authorization: Bearer sk-your-key 传入。登录令牌、密码和充值码不能代替 API Key。

先查询余额确认鉴权,再用 Python 完整示例 跑一次纯文本生成。余额查询不扣积分,创建视频任务会按当前价格扣积分。Key 保存在服务端环境变量中。

$env:VIDEO_API_BASE = "https://__ORIGIN__"
$env:VIDEO_API_KEY = "sk-your-key"
$env:VIDEO_STATE = "order-001.json"
curl.exe --fail-with-body -sS --max-time 90 "$env:VIDEO_API_BASE/v1/balance" -H "Authorization: Bearer $env:VIDEO_API_KEY" -A "vinted-video-client/1.0"
# 将下方完整示例保存为 video_client.py 后运行(Python 3.10+)
python video_client.py

同一订单重试保持 VIDEO_STATE 不变;新订单换一个文件名,例如 order-002.json。状态文件保存操作编号、参数和任务 id,不保存 Key。每个订单同一时间只运行一个进程。

下方分步骤的 curl 示例适用于 Bash,需 curl 7.76+、jq、Python 3;PowerShell 可直接使用上面的环境设置与完整 Python 示例。基址只填域名根,不重复添加 /v1

方法与路径用途鉴权
GET /health · GET /ready服务存活 / 就绪探针
GET /v1/catalog模型、合法时长、比例、分辨率
GET /v1/pricing不带 Key 为公开价;带 Key 为该账户适用售价可选
GET /v1/balance积分余额与最近流水
POST /v1/files上传参考图,换取 image_id
GET /v1/files/{image_id}读取已上传的参考图
POST /v1/videos创建视频任务(返回 202 + 任务 id)
GET /v1/videos/{job_id}查询任务状态
GET /v1/videos/{job_id}/signed_url获取轻量版链接;加 ?download=1 获取原片下载链接
GET · HEAD /v1/videos/{job_id}/file下载 / 预览视频(支持 RangeKey 或有效签名
GET /v1/image-ai/settings彩绘图片单价与保留时间
POST /v1/image-ai/submit人物照片转彩绘,批量逐张受理
GET /v1/image-ai/batch/{batch_id}查询图片批次
GET /v1/image-ai/jobs/{job_id}查询图片状态与下载地址
GET /v1/image-ai/jobs/{job_id}/file下载彩绘图片Key 或有效签名

Python 完整示例

Python 3.10+,无需安装第三方包。先设置上方环境变量,再下载 video_client.py 运行。默认生成 5 秒竖屏视频,结果保存为 out.mp4

图片可选:Windows 设置 $env:VIDEO_IMAGE = "reference.png";macOS / Linux 设置 export VIDEO_IMAGE="reference.png"。仅在新订单首次运行时读取图片和参数;已有状态文件继续原订单。需要修改提示词时,先修改代码中的请求体并为新订单更换状态文件名。

video_client.py
import base64, hashlib, http.client, json, os, shutil, time, uuid
import urllib.error, urllib.request
from pathlib import Path
from urllib.parse import urljoin

base = os.environ.get("VIDEO_API_BASE", "https://__ORIGIN__").rstrip("/")
key = os.environ.get("VIDEO_API_KEY", "").strip()
state_path = Path(os.environ.get("VIDEO_STATE", "order-001.json"))
output = Path(os.environ.get("VIDEO_OUTPUT", "out.mp4"))
agent = "vinted-video-client/1.0"
if not key.startswith("sk-") or key == "sk-your-key":
    raise SystemExit("Set VIDEO_API_KEY to your own sk- key first.")

class ApiError(SystemExit):
    def __init__(self, status, message):
        super().__init__(message)
        self.status = status

def save_state():
    temporary = state_path.with_suffix(state_path.suffix + ".tmp")
    temporary.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
    temporary.replace(state_path)

def call(method, path, body=None, idem=None, field=None):
    data = None if body is None else json.dumps(body).encode("utf-8")
    headers = {"Authorization": "Bearer " + key, "User-Agent": agent}
    if data is not None:
        headers["Content-Type"] = "application/json"
    if idem:
        headers["Idempotency-Key"] = idem
    for attempt in range(3):
        delay = 5 * (attempt + 1)
        req = urllib.request.Request(base + path, data=data, headers=headers, method=method)
        try:
            with urllib.request.urlopen(req, timeout=90) as response:
                result = json.load(response)
            if not isinstance(result, dict) or (field and
                    (not isinstance(result.get(field), str) or not result[field])):
                raise ValueError("empty or incomplete JSON response")
            return result
        except urllib.error.HTTPError as exc:
            raw = exc.read().decode("utf-8", "replace")
            try:
                detail = json.loads(raw)
            except ValueError:
                detail = {}
            error = detail.get("error", {}) if isinstance(detail, dict) else {}
            if not isinstance(error, dict):
                error = {}
            message = "HTTP %s: %s; request_id=%s" % (
                exc.code, error.get("message", raw[:300]),
                error.get("request_id", exc.headers.get("X-Request-ID", "")))
            retryable = (exc.code in {408, 429, 500, 502, 503, 504} or
                         (exc.code == 409 and error.get("type") in
                          {"idempotency_in_progress", "submission_reclaimed"}))
            if not retryable:
                raise ApiError(exc.code, message + "\nState kept: " + str(state_path))
            retry = exc.headers.get("Retry-After")
            if retry is None and isinstance(detail, dict):
                retry = detail.get("retry_after")
            try:
                delay = max(delay, float(retry))
            except (TypeError, ValueError):
                pass
        except (OSError, ValueError, http.client.HTTPException) as exc:
            message = str(exc)
        if attempt == 2 or delay > 60:
            raise SystemExit(message + "\nWait and rerun with the SAME VIDEO_STATE and API Key: " + str(state_path))
        time.sleep(delay)

# Persist the operation before the first creation request; never persist the key.
key_hash = hashlib.sha256(key.encode()).hexdigest()
if state_path.exists():
    state = json.loads(state_path.read_text(encoding="utf-8"))
    if state.get("base") != base or state.get("key_hash") != key_hash:
        raise SystemExit("This state belongs to a different base URL or API Key.")
else:
    state = {"base": base, "key_hash": key_hash, "operation_id": uuid.uuid4().hex,
             "image_path": os.environ.get("VIDEO_IMAGE", ""),
             "body": {"prompt": "A clean product showcase", "model": "seedance2.0fast",
                      "duration": 5, "ratio": "9:16"}}
    save_state()

def upload_reference():
    image = Path(state["image_path"]).read_bytes()
    digest = hashlib.sha256(image).hexdigest()
    if state.get("image_hash", digest) != digest:
        raise SystemExit("The reference image changed. Restore the original image to resume.")
    uploaded = call("POST", "/v1/files", {
        "image_b64": base64.b64encode(image).decode("ascii")
    }, field="image_id")
    state.update(image_hash=digest, image_expires_at=uploaded["expires_at"])
    state["body"]["image_ids"] = [uploaded["image_id"]]
    save_state()

if not state.get("job_id"):
    if state["image_path"] and not state["body"].get("image_ids"):
        upload_reference()
    try:
        job = call("POST", "/v1/videos", state["body"],
                   idem=state["operation_id"], field="id")
    except ApiError as exc:
        # Reconcile the original submission first. Only refresh an expired
        # upload after an explicit rejection, retaining the same operation.
        if (exc.status != 400 or not state["image_path"] or
                state.get("image_expires_at", float("inf")) > time.time()):
            raise
        upload_reference()
        job = call("POST", "/v1/videos", state["body"],
                   idem=state["operation_id"], field="id")
    state["job_id"] = job["id"]
    save_state()

job_path = "/v1/videos/" + state["job_id"]
print("job_id=" + state["job_id"], flush=True)
deadline = time.monotonic() + 1800
while time.monotonic() < deadline:
    current = call("GET", job_path, field="status")
    if current["status"] in {"succeeded", "failed"}:
        break
    if current["status"] not in {"queued", "running"}:
        raise SystemExit("Unexpected status: " + str(current["status"]))
    time.sleep(5)
else:
    raise SystemExit("Still pending; rerun with the SAME VIDEO_STATE to continue polling.")
if current["status"] != "succeeded":
    raise SystemExit("Generation failed: " + json.dumps(current, ensure_ascii=False))

# A signed URL needs no Authorization header and expires; mint it when needed.
signed = call("GET", job_path + "/signed_url?download=1", field="url")
url = signed["url"]
download = urljoin(base.rstrip("/") + "/", url)
part = output.with_name(output.name + ".part")
request = urllib.request.Request(download, headers={"User-Agent": agent}, method="GET")
with urllib.request.urlopen(request, timeout=600) as response, part.open("wb") as handle:
    if response.status != 200:
        raise SystemExit("download failed: %d" % response.status)
    shutil.copyfileobj(response, handle)
    expected = response.headers.get("Content-Length")
    if expected is not None and handle.tell() != int(expected):
        raise SystemExit("Incomplete download; rerun with the SAME VIDEO_STATE.")
part.replace(output)
print("saved " + str(output))

请求超时、空响应、临时错误最多自动重试 3 次;等待过久或需要充值时会退出并保留状态。重新运行可继续提交确认、轮询或下载,不会因为本地等待结束而新建订单。视频失败时以任务返回的 error 和实际账单为准。

接入须知

使用自定义视频 REST 接口,接入工具需支持自定义路径、JSON 请求体与异步轮询。

  1. 基址 https://__ORIGIN__;业务接口都在 /v1 下。不要直连源站 IP。
  2. 鉴权 每个受保护请求带请求头 Authorization: Bearer sk-your-keyBearer 后有一个空格)。
  3. 格式 JSON 请求使用 Content-Type: application/json;图片与视频读取返回二进制。视频请求发往 POST /v1/videos,不能直接套用聊天接口或图片生成接口。
  4. 异步 POST /v1/videos 成功返回 HTTP 202 和任务 id不会等到视频生成完。必须轮询 GET /v1/videos/{job_id} 直到 succeededfailed。把 202 当失败是最常见的错。
  5. 超时 客户端读超时设 ≥ 60 秒;轮询间隔 3–5 秒;总等待预算 ≥ 10 分钟(seedance2.5/30 秒更久)。不要用一次同步请求等成品,也不要套 30 秒短超时。
  6. 幂等 创建前保存唯一 Idempotency-Key 与请求体;超时、空响应或重启后,使用同一个 API Key、操作编号和请求体恢复。新订单用新编号。收到任务 id 后只需查询;客户端等待超时不代表任务已取消。
  7. 计费 按视频计积分;余额不足返回 402;触发频率/额度限制返回 429(按 Retry-After 等待)。
  8. 下载 使用 GET /v1/videos/{job_id}/signed_url?download=1 获取原片下载链接,普通签名和列表中的 download_url 是轻量版。母版获取失败时返回固定的轻量版链接,以响应的 qualityoriginalpreview)为准。只用本站返回的地址,支持 Range 断点续传;接口选好版本后,续传不会跨版本。不要缓存或转存这些短期地址。

模型与参数

GET /v1/catalog 实时返回为准;下表为当前能力。所有模型都支持图生视频,分辨率均为 720p

model合法 duration(秒)参考图上限
seedance2.53030 张
seedance2.05 / 10 / 159 张
seedance2.0fast5 / 10 / 159 张
seedance2.0mini5 / 109 张

比例 ratio1:13:44:39:1616:921:9。不传或传空字符串时使用模型默认比例,需要竖屏请明确传 9:16运镜 camera_movement:空字符串(模型默认)、autofixed分辨率 resolution720p。提示词 prompt 最长 6000 字符。

创建前查询带 Key 的 GET /v1/pricing,确认模型和时长有可用价格。目录价格是公开价,可能与当前账户价格不同。

参数与响应约定
  1. 基址必须是 https://__ORIGIN__,不要打源站 IP。
  2. 鉴权头必须是 Authorization: Bearer sk-...Bearer 后有空格;不要只传 Key,也不要用 X-API-Key
  3. POST /v1/videos 成功是 HTTP 202,例如 {"id":"f84c6a1e9b234f6e8a94231450000001","status":"queued"}。幂等重放可能返回 running 或终态。
  4. model 只能用 GET /v1/catalog 里的 id:seedance2.5 / seedance2.0 / seedance2.0fast / seedance2.0mini。推荐用这四个展示 id。若你此前对接时使用的是带下划线的旧写法,服务端仍然兼容、计价与时长规则完全一致,不必改造;新接入请统一用上面的展示 id。
  5. 2.5 只能 duration: 30;2.0 / 2.0fast 为 5/10/15;mini 为 5/10。
  6. 图生视频先 POST /v1/filesimage_id 再传 image_ids;也可直接传公网图床直链 image_url(单张)或 image_urls(多张),两种方式可混用并合计张数(seedance2.5 最多 30 张,其余模型最多 9 张)。公开文档不走 multipart。
  7. 轮询时内部 polling 对外显示为 running。直到 succeededfailed
  8. signed_url 返回 urlexpires_atquality。不带 download=1 时获取轻量版预览链接;下载原片时重新请求 GET /v1/videos/{job_id}/signed_url?download=1,不要直接改写旧预览链接。返回的 url 可能是相对路径或绝对地址,使用 URL 解析函数处理,避免重复拼接域名,并保留签名中的版本参数。签名有效期为 600 秒,以返回的 expires_at 为准。该接口未就绪返回 409;直接访问 /file 未就绪返回 404。

余额查询

GET /v1/balance 返回当前积分余额与最近流水。响应字段为 balance(整数积分)与 ledger(最近流水数组)。

BASE="${VIDEO_API_BASE:-https://__ORIGIN__}"
KEY="${VIDEO_API_KEY:?Set VIDEO_API_KEY to the original sk- key}"
curl --fail-with-body -sS --max-time 90 "$BASE/v1/balance" \
  -H "Authorization: Bearer $KEY" -A "vinted-video-client/1.0"
# {"balance":12345,"ledger":[{"reason":"consume","delta":-20,"job_id":"example-job","balance_after":12345,"created_at":1756000000}]}

流水字段是 delta,负数为扣款、正数为入账。普通失败全额退款;适用内容审核扣费时可能部分退款或退款为零。退款结算可能晚于任务终态,以该任务的账单记录为准。

上传参考图

图生视频可选。POST /v1/files 每次上传一张 PNG、JPEG 或 WebP,以 image_b64 传 data URL 或纯 base64,解码后单图最大 50 MiB,编码字符串不超过 70,000,000 字符。成功返回 HTTP 201。多图逐张上传,再把所有 id 传给视频接口。

BASE="${VIDEO_API_BASE:-https://__ORIGIN__}"
KEY="${VIDEO_API_KEY:?Set VIDEO_API_KEY to the original sk- key}"

set -euo pipefail
UPLOAD=$(python3 -c 'import base64,json; from pathlib import Path; print(json.dumps({"image_b64":base64.b64encode(Path("reference.png").read_bytes()).decode()}))' |
  curl --fail-with-body -sS --max-time 90 -X POST "$BASE/v1/files" \
    -A "vinted-video-client/1.0" \
    -H "Authorization: Bearer $KEY" \
    -H "Content-Type: application/json" \
    --data-binary @-)
IMAGE_ID=$(printf '%s' "$UPLOAD" | jq -er '.image_id | select(type == "string" and length > 0)')

响应字段为 image_idurlformatsizeexpires_at,id 有效期一小时。同一账号的有效 Key 在过期前可重复使用该 id;已受理的视频任务不受上传过期影响。将 image_id 放进 image_ids,不要把上传响应的 url 当公网图床链接。

创建视频

POST /v1/videos 的请求字段:

字段类型必填说明
promptstring1–6000 字符
modelstring默认 seedance2.0fast,建议明确传入
durationinteger省略时 2.5 默认 30 秒,其他模型默认 5 秒;必须是该模型的合法且有价格的时长
ratiostring默认空字符串(模型默认),需要竖屏明确传 9:16
resolutionstring720p
camera_movementstringautofixed
image_idsstring[]/v1/files 返回的 id 列表
image_urlstring单张公网图床直链(png/jpg/webp)
image_urlsstring[]多张公网图床直链;与 image_ids 合并计数

不传图片就是文生视频。参考图可传 image_ids(先上传)、image_url/image_urls(公网直链),混用时合并计数。下面的 Bash 示例将参数和操作编号保存到文件;同一订单重试保留这两个文件,新订单在新目录运行。

set -euo pipefail
BASE="${VIDEO_API_BASE:-https://__ORIGIN__}"
KEY="${VIDEO_API_KEY:?Set VIDEO_API_KEY to the original sk- key}"
if [ ! -f order.id ]; then python3 -c 'import uuid; print(uuid.uuid4().hex)' > order.id; fi
if [ ! -f request.json ]; then
  jq -n --arg image "${IMAGE_ID:-}" \
    '{prompt:"A clean product showcase",model:"seedance2.0fast",duration:5,ratio:"9:16"}
     + (if $image == "" then {} else {image_ids:[$image]} end)' > request.json
fi
OPERATION_ID=$(tr -d '\r\n' < order.id)
test -n "$OPERATION_ID"
JOB=$(curl --fail-with-body -sS --max-time 90 -X POST "$BASE/v1/videos" \
  -H "Authorization: Bearer $KEY" \
  -H "Idempotency-Key: $OPERATION_ID" \
  -A "vinted-video-client/1.0" \
  -H "Content-Type: application/json" \
  --data-binary @request.json)
JOB_ID=$(printf '%s' "$JOB" | jq -er '.id | select(type == "string" and length > 0)')
printf '%s\n' "$JOB_ID" > job.id

外链必须直接返回图片,不是相册网页或需要登录的地址。image_url/image_urls 单图上限为 25 MiB,与上传接口的 50 MiB 不同。遇到图床防盗链或拉取失败,可改用上传接口。

查询与下载

轮询 GET /v1/videos/{job_id}。中间状态为 queuedrunning,终态为 succeededfailed

超时与轮询(避免“接口超时”):
  1. 出片是异步的:POST /v1/videos 完成校验和参考图处理后返回 202id,不会等到视频生成完。外链图片拉取可能延长提交等待时间。
  2. 生成通常几十秒到几分钟,seedance2.5 / 30 秒更久。请轮询 GET /v1/videos/{job_id},建议每 3–5 秒一次,总等待预算给到 10 分钟以上。
  3. 客户端超时建议:连接超时 10 秒、读取超时 ≥ 60 秒;不要给提交或轮询套 30 秒的短超时。
  4. POST 一定带唯一 Idempotency-Key:网络超时后可安全重试同一请求,不会重复建任务或重复扣费。
  5. 站点前有网关,超长的单条连接可能被断开;请依赖轮询拿终态,下载大文件用 Range 断点续传。
set -euo pipefail
BASE="${VIDEO_API_BASE:-https://__ORIGIN__}"
KEY="${VIDEO_API_KEY:?Set VIDEO_API_KEY to the original sk- key}"
JOB_ID=$(tr -d '\r\n' < job.id)
DEADLINE=$((SECONDS + 1800))
while [ "$SECONDS" -lt "$DEADLINE" ]; do
  CURRENT=$(curl --fail-with-body -sS --max-time 90 "$BASE/v1/videos/$JOB_ID" \
    -H "Authorization: Bearer $KEY" -A "vinted-video-client/1.0")
  STATUS=$(printf '%s' "$CURRENT" | jq -er '.status | select(type == "string")')
  case "$STATUS" in
    succeeded) break ;;
    failed) printf '%s\n' "$CURRENT"; exit 1 ;;
    queued|running) sleep 5 ;;
    *) printf '%s\n' "$CURRENT"; exit 1 ;;
  esac
done
if [ "${STATUS:-}" != succeeded ]; then
  echo "Still pending. Keep job.id and rerun this polling block."; exit 1
fi
curl --fail-with-body -sS --max-time 600 "$BASE/v1/videos/$JOB_ID/file" \
  -H "Authorization: Bearer $KEY" -A "vinted-video-client/1.0" -o result.mp4.part
mv result.mp4.part result.mp4

GET|HEAD /v1/videos/{job_id}/file 也可使用 Key 访问,并支持字节范围。

全能参考 API

沿用账户的 sk- API Key 和积分余额,对所有状态为 active 的正常账户开放,无须逐用户授权。GET /v1/niulai/catalog 返回 access_allowed(账户状态)、enabled(模块总开关)、available(服务可用性)和当前可选模型、时长价格。

模型API 标识整数时长参考素材与输出
牛来proniulaipro4–15 秒文生视频、首尾帧、多素材参考;自动画幅;768p 默认 10 积分/秒,2K 默认 15 积分/秒

牛来pro 的 768p 默认 10 积分/秒,2K 默认 15 积分/秒:4 秒分别为 40、60 积分,15 秒分别为 150、225 积分;管理员可随时修改两档价格,已受理订单保留原扣费。牛来pro 提交前以目录的 resolution_prices[resolution][duration] 为准,其他模型使用 prices;空价格表示该规格未开放。新任务扣积分,失败自动退款。模块总开关关闭后停止新上传和生成,已付费任务继续处理,历史查询、恢复和下载仍可使用。

上传、提交与下载

方法与路径用途
GET /v1/niulai/catalog权限、模型能力和积分价格
POST /v1/niulai/assets?kind=image&role=reference上传图片、视频或音频原始字节,返回素材 id
POST /v1/niulai/submit创建任务,必传 Idempotency-Key,成功为 HTTP 202
GET /v1/niulai/jobs?page=1&size=30本账户任务列表,每页最多 100 条
GET /v1/niulai/jobs/{job_id}查询状态、扣费、退款和恢复进度
GET /v1/niulai/jobs/{job_id}/signed_url生成本站下载地址,返回文件 sizesha256expires_at
GET /v1/niulai/jobs/{job_id}/file下载原始视频,支持 Range,使用 Key 或返回的完整签名地址
POST /v1/niulai/jobs/{job_id}/delivered完整保存并校验文件后,提交 {"size":字节数,"sha256":"文件摘要"} 确认交付

上传使用文件原始字节和对应 Content-Type,不使用 JSON 或 multipart;可带 X-Filename。图片最多 20 MiB、视频 100 MiB、音频 50 MiB,网关硬上限为 128 MiB。支持 JPEG/PNG/WebP 图片和服务能探测的 MP4/MOV/WebM、MP3/M4A/WAV/FLAC/OGG 等音视频格式;下载返回 MP4 二进制流。

牛来pro 的 mode=reference 最多 9 张图片、3 段视频、3 段音频,视频和音频各自合计不超过 15 秒;图片和视频宽高须为 256–5760 像素,音频必须配至少一张图片。mode=frames 只接受最多一张 first 和一张 last 图片,不能与参考素材混用;可只传一帧、两帧或不传素材。

正文必填 modelprompt 和 4–15 秒整数 duration。牛来pro 的 aspect 支持 16:9、9:16、4:3、3:4、1:1、21:9、adaptiveparameters 仅含 resolution,可选 768p2k。牛来1/2(niulai1niulai2)已停售,新提交返回 422 model_retired,历史牛来1/牛来2订单仍可查询和下载。

门户文本框支持 @图片N@视频N@音频N 选择已上传素材;提交前会编译为 Picture NVideo NAudio N 并绑定素材 ID。直接调用 API 时请传素材 ID 和已编译的提示词。

curl --fail-with-body -sS --max-time 90 "https://__ORIGIN__/v1/niulai/submit" \
  -H "Authorization: Bearer sk-your-key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: niulaipro-order-001" \
  --data '{"model":"niulaipro","prompt":"主体沿海边步道前行,镜头缓慢跟随","duration":8,"aspect":"16:9","mode":"reference","assets":[],"parameters":{"resolution":"768p"}}'

每个新视频使用独立 Idempotency-Key(1–200 字符),服务端按 API Key 和该头生成操作编号;保存返回的任务 ID。相同 Key 和正文重试返回原订单;更改正文返回 409 idempotency_conflict。超时或 425 operation_pending 后保留原 Key 和正文继续恢复,不能换 Key 重复下单。

queuedrunningrecovering 都应继续查询;约每 5 秒查询一次。succeeded 后获取签名地址并下载,文件大小与 SHA256 均通过后再确认交付;链接过期可以重新获取。failed 查看 errorrefunded,不要因下载失败再次创建视频。签名链接无需附带 Key。

Key 的每分钟请求、每日任务和并发限制仍有效。无效或禁用 Key 返回 401 invalid_key;未绑定客户或提交竞态中的账户失效可能返回 403 customer_not_allowed;余额不足返回 402 insufficient_balance;旧模型或未配置价格返回 422 model_retired/unpriced;限流或队列额度不足返回 429,根据 error.type 区分 rpmdaily_capconcurrencyqueue_full。错误响应可带 request_id。总开关关闭返回 503 channel_disabled,已付费任务仍可恢复、查询和下载。

人物照片转彩绘 API

把人物照片转成彩色铅笔手绘风格。沿用账户的 sk- API Key 与积分余额:提交图片、查询任务、下载结果,全部通过下面的接口完成。省略 prompt 即使用本站默认彩铅风格,也可以传入自己的绘图要求。

先查价再提交:GET /v1/image-ai/settings 返回当前 price_per_image,以实际返回值为准。每张受理成功的图片独立扣积分,生成失败自动退款;未受理的图片不扣费,可在 GET /v1/balance 核对流水。该功能不占视频日配额,当前月卡账户暂不支持。

接口说明
GET /v1/image-ai/settings查询当前图片单价与保留时间
POST /v1/image-ai/submit批量提交,返回 HTTP 202 和逐张受理结果
GET /v1/image-ai/batch/{batch_id}查询批次中的图片任务
GET /v1/image-ai/jobs/{job_id}查询单张状态,成功时返回新的短期签名下载地址
GET /v1/image-ai/jobs/{job_id}/file下载图片;使用 Key 或有效的 expsig 签名

请求与结果

  • images_b64:必填数组,1–30 张 PNG、JPEG 或 WebP,元素为纯 Base64 或 data URL。每张解码后最多 20 MiB,整次 JSON 默认最多 96 MiB(含 Base64 体积,服务可调整);大图请拆成小批次。
  • prompt:可选,最多 4,000 字符。留空使用默认彩铅风格;填写则整批使用这段绘图要求。接口不返回本站默认提示词。
  • Idempotency-Key:可选但强烈建议,1–200 字符。每个新批次用新编号;提交超时、响应丢失或重启时,保留原 Key、编号和请求体重试。相同请求重放不会重复扣费;相同编号对应不同正文时返回 409 idempotency_conflict。操作编号按 API Key 隔离。
  • 202 只表示收到批次。逐一检查 items[].oktrue 时保存 idfalse 时读取 error。余额不足或非法图片可能使部分乃至全部图片未受理,不能只看 HTTP 状态。
  • 图片任务状态为 queued(排队)、processing(生成中)、succeeded(成功)、failed(失败)。每 3–5 秒轮询一次;只有成功且完整下载图片,才算取得结果。
  • 成功任务返回 download_url,可能为相对地址;用 URL 解析函数与站点基址拼接,保留全部签名参数。链接过期后重新查询任务获取新地址;也可带 Key 直接访问文件接口。以实际 Content-Type 保存 PNG、JPEG 或 WebP。
  • 同账户的其他有效 Key 可以查询和下载该账户的图片;其他账户的任务一律返回 404。任务与文件默认保留 24 小时,具体以设置返回的 retention_hours 为准,请及时保存。

curl:本地图片提交与查询

下面示例使用 Bash、Python 3 和 curl 7.76+。把人物图放在当前目录,命名为 portrait.jpg。新订单使用新目录;超时重试保留已保存的 JSON 与操作编号。

export IMAGE_API_BASE="https://__ORIGIN__"
export IMAGE_API_KEY="sk-your-key"
# 每个订单使用单独目录;重试保留 request.json 和 operation.id,不重新生成。
python3 - <<'PY'
import base64, json, uuid
from pathlib import Path
request, operation = Path("image-request.json"), Path("image-operation.id")
if not request.exists():
    body = {"images_b64": [base64.b64encode(Path("portrait.jpg").read_bytes()).decode("ascii")]}
    request.write_text(json.dumps(body), encoding="utf-8")
if not operation.exists():
    operation.write_text(uuid.uuid4().hex, encoding="ascii")
PY
curl --fail-with-body -sS --max-time 30 "$IMAGE_API_BASE/v1/image-ai/settings" \
  -H "Authorization: Bearer $IMAGE_API_KEY"
curl --fail-with-body -sS --max-time 90 "$IMAGE_API_BASE/v1/image-ai/submit" \
  -H "Authorization: Bearer $IMAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(cat image-operation.id)" \
  --data-binary @image-request.json
# 从返回 items 中取 ok=true 的 id;每 5 秒查询一次,直到 succeeded 或 failed。
JOB_ID="替换为已受理的图片任务id"
curl --fail-with-body -sS --max-time 30 \
  "$IMAGE_API_BASE/v1/image-ai/jobs/$JOB_ID" \
  -H "Authorization: Bearer $IMAGE_API_KEY"
# succeeded 后可直接用 Key 下载;响应 Content-Type 决定真实图片格式。
curl --fail-with-body -sS --max-time 90 \
  "$IMAGE_API_BASE/v1/image-ai/jobs/$JOB_ID/file" \
  -H "Authorization: Bearer $IMAGE_API_KEY" -o portrait-result.image

Python:批量生成、轮询与下载

Python 3.10+,先执行 python -m pip install requests,把下方代码保存为 image_client.py。设置 IMAGE_API_BASEIMAGE_API_KEY 后运行 python image_client.py portrait.jpg second.png。PowerShell 设置环境变量时使用 $env:IMAGE_API_KEY = "sk-your-key"

可选 IMAGE_PROMPT 设置整批绘图要求,IMAGE_WAIT_SECONDS 设置本次最多等待秒数(默认 1800),IMAGE_OUTPUT 设置下载目录。状态文件默认 image-order-001.json:重跑会恢复已有任务;新订单须改用新的 IMAGE_STATE 文件名。状态文件含输入图片与请求体,应保存在自己的受控目录;每个订单同一时间只运行一个进程。

总等待到期不会取消生成,也不代表失败。保留状态文件和原 API Key 再次运行即可继续查询或下载;未受理的图片与已确认失败的图片不会在重跑时自动创建新任务。

"""Python 3.10+; install: python -m pip install requests"""
import base64
import json
import os
import sys
import time
import uuid
from pathlib import Path
from urllib.parse import quote, urljoin, urlsplit

import requests

BASE = os.environ.get("IMAGE_API_BASE", "https://__ORIGIN__").rstrip("/") + "/"
KEY = os.environ.get("IMAGE_API_KEY", "").strip()
STATE = Path(os.environ.get("IMAGE_STATE", "image-order-001.json"))
OUT = Path(os.environ.get("IMAGE_OUTPUT", "image-results"))
WAIT = float(os.environ.get("IMAGE_WAIT_SECONDS", "1800"))
if not KEY.startswith("sk-") or WAIT <= 0:
    raise SystemExit("Set IMAGE_API_KEY to your own sk- key and use a positive wait limit.")
origin = urlsplit(BASE)
if origin.scheme not in ("https", "http") or not origin.netloc:
    raise SystemExit("IMAGE_API_BASE must be an HTTP(S) origin, without /v1.")
if origin.path != "/" or origin.query or origin.fragment:
    raise SystemExit("IMAGE_API_BASE must contain only the site origin.")

def save():
    STATE.parent.mkdir(parents=True, exist_ok=True)
    temp = STATE.with_name(STATE.name + ".tmp")
    temp.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
    temp.replace(STATE)

if STATE.exists():
    state = json.loads(STATE.read_text(encoding="utf-8"))
    if state["base"] != BASE:
        raise SystemExit("Use the original base URL, API Key, and state file to resume.")
else:
    paths = [Path(name) for name in sys.argv[1:]]
    if not 1 <= len(paths) <= 30:
        raise SystemExit("Usage: python image_client.py photo.png [photo2.jpg ...]; 1-30 images.")
    images = []
    for path in paths:
        data = path.read_bytes()
        if not 0 < len(data) <= 20 * 1024 * 1024:
            raise SystemExit(str(path) + ": decoded image must be between 1 byte and 20 MiB.")
        images.append(base64.b64encode(data).decode("ascii"))
    body = {"images_b64": images}
    if os.environ.get("IMAGE_PROMPT", "").strip():
        body["prompt"] = os.environ["IMAGE_PROMPT"]
    if len(body.get("prompt", "")) > 4000:
        raise SystemExit("IMAGE_PROMPT must not exceed 4000 characters.")
    if len(json.dumps(body).encode("utf-8")) > 96 * 1024 * 1024:
        raise SystemExit("Split these images into smaller batches; JSON exceeds the default 96 MiB limit.")
    state = {"base": BASE, "operation": uuid.uuid4().hex,
             "body": body, "accepted": None, "done": {}}
    save()

deadline = time.monotonic() + WAIT
session = requests.Session()
session.headers.update({"Authorization": "Bearer " + KEY,
                        "User-Agent": "image-client/1.0"})

def remaining():
    seconds = deadline - time.monotonic()
    if seconds <= 0:
        raise TimeoutError("Total wait ended. Rerun with the SAME IMAGE_STATE and API Key.")
    return seconds

def safe_url(path):
    url = urljoin(BASE, path)
    target = urlsplit(url)
    if (target.scheme, target.netloc) != (origin.scheme, origin.netloc):
        raise RuntimeError("Refusing a result URL outside the configured site.")
    return url

def error_message(response):
    try:
        value = response.json()
        error = value.get("error", value.get("detail", {}))
        if isinstance(error, dict):
            return str(error.get("message") or error.get("type") or "Request failed")
        return str(error)
    except (ValueError, AttributeError):
        return "Non-JSON response; retain the HTTP status for support."

def call(method, path, **kwargs):
    budget = remaining()
    response = session.request(method, safe_url(path), timeout=(min(10, budget), min(90, budget)),
                               allow_redirects=False, **kwargs)
    if not 200 <= response.status_code < 300:
        raise RuntimeError("HTTP " + str(response.status_code) + ": " + error_message(response))
    return response

def get_json(method, path, **kwargs):
    with call(method, path, **kwargs) as response:
        value = response.json()
    if not isinstance(value, dict):
        raise RuntimeError("Unexpected JSON response. Keep the same state file to resume.")
    return value

def download(job):
    link = job.get("download_url")
    if not link:
        raise RuntimeError("Succeeded job has no download_url; query this job again.")
    with call("GET", link, stream=True) as response:
        kind = response.headers.get("Content-Type", "").split(";", 1)[0].lower()
        extension = {"image/png": ".png", "image/jpeg": ".jpg", "image/webp": ".webp"}.get(kind)
        if not extension:
            raise RuntimeError("Unexpected image Content-Type: " + kind)
        OUT.mkdir(parents=True, exist_ok=True)
        dest = OUT / (job["id"] + extension)
        partial = dest.with_suffix(dest.suffix + ".part")
        size = 0
        prefix = b""
        with partial.open("wb") as file:
            for chunk in response.iter_content(64 * 1024):
                remaining()
                if chunk:
                    prefix = (prefix + chunk)[:12]
                    file.write(chunk)
                    size += len(chunk)
        valid = ((kind == "image/png" and prefix.startswith(b"\x89PNG\r\n\x1a\n"))
                 or (kind == "image/jpeg" and prefix.startswith(b"\xff\xd8\xff"))
                 or (kind == "image/webp" and prefix[:4] == b"RIFF" and prefix[8:12] == b"WEBP"))
        if not size or not valid:
            raise RuntimeError("Downloaded bytes do not match the announced image format.")
        expected = response.headers.get("Content-Length")
        if expected and response.headers.get("Content-Encoding", "identity") == "identity":
            if size != int(expected):
                raise RuntimeError("Incomplete download. Rerun with the same state file.")
        partial.replace(dest)
    return str(dest.resolve())

try:
    if state["accepted"] is None:
        settings = get_json("GET", "/v1/image-ai/settings")
        print("Current credits per image:", settings["price_per_image"])
        accepted = get_json("POST", "/v1/image-ai/submit", json=state["body"],
                            headers={"Idempotency-Key": state["operation"]})
        if not isinstance(accepted.get("items"), list) or not accepted.get("batch_id"):
            raise RuntimeError("Missing batch receipt. Keep the same state file to resume.")
        state["accepted"] = accepted
        save()
    items = state["accepted"]["items"]
    pending = set()
    failed = any(not item.get("ok") for item in items)
    for index, item in enumerate(items, 1):
        if not item.get("ok"):
            print("NOT ACCEPTED", index, item.get("error", "Unknown error"))
        elif not item.get("id"):
            raise RuntimeError("Accepted item has no id.")
        else:
            job_id = str(item["id"])
            result = state["done"].get(job_id)
            if result and result["status"] == "failed":
                failed = True
                print("FAILED", job_id, result.get("error", "Generation failed"))
            elif result and Path(result.get("file", "")).is_file():
                print("SAVED", result["file"])
            else:
                pending.add(job_id)
    while pending:
        for job_id in list(pending):
            job = get_json("GET", "/v1/image-ai/jobs/" + quote(job_id, safe=""))
            status = job.get("status")
            if status in ("queued", "processing"):
                continue
            if status == "failed":
                failed = True
                state["done"][job_id] = {"status": status, "error": job.get("error", "")}
                print("FAILED", job_id, job.get("error", "Generation failed"))
            elif status == "succeeded":
                path = download(job)
                state["done"][job_id] = {"status": status, "file": path}
                print("SAVED", path)
            else:
                raise RuntimeError("Unknown job status: " + str(status))
            pending.remove(job_id)
            save()
        if pending:
            time.sleep(min(5, remaining()))
    raise SystemExit(1 if failed else 0)
except (requests.RequestException, RuntimeError, TimeoutError, ValueError, OSError) as exc:
    raise SystemExit(str(exc) + "\nKeep IMAGE_STATE and the original API Key to resume; do not create a new order.")
finally:
    session.close()

号池状态

需单独申请只读 Key。

GET /v1/pool/status 返回实时号池规模与可用量,用于向你的客户证明号池真实。

这个接口不用出片用的 sk- Key,而是单独申请的号池只读 Key(pk- 开头)。两套凭据物理隔离:pk- 调不动 /v1/videos,不会消耗任何额度;sk- 也打不开本接口。需要接入请联系我们签发。
curl --fail-with-body -sS "$BASE/v1/pool/status" \
  -H "Authorization: Bearer $POOL_KEY"
{
  "generated_at": 1756531200,
  "total": 48,
  "available": 31,
  "status_counts": {"active": 31, "busy": 2, "cooling": 5, "inactive": 10},
  "availability_counts": {"active": 31, "busy": 2, "cooling": 5, "inactive": 3,
                          "recovery_pending": 4, "quota_exhausted": 1, "quota_frozen": 1, "unavailable": 1},
  "quota": {"total": 192, "remaining": 118, "remaining_available": 84}
}
字段含义
generated_at快照时间(Unix 秒)
total号池账号总数
available当前可调度账号数
status_counts兼容原有四类状态:active、busy、cooling、inactive;inactive 包含所有当前不可调度账号,不代表永久失效
availability_counts当前可用性原因的详细数量(见下表)
quota.total账号配置额度之和,不代表上游已确认的可用额度
quota.remaining本地估算剩余额度;等待恢复的账号不会仅因重置时间到期而加满
quota.remaining_availableactive 账号的本地估算剩余额度,可能包含获准尝试恢复的账号

available 与调度器当前可派单账号数一致,可能包含已分配恢复尝试名额的账号,不保证上游受理成功。两组统计各自互斥,分别合计为 total。新增的四类不可用原因在原有 status 中统一归入 inactive

可用性原因含义
active当前可派单,包括获准尝试恢复的账号
busy任务、账号租约或内部占用状态尚未释放
cooling临时限流或退避中
inactive账号被停用、暂停或登录状态不可用
recovery_pending额度恢复尚待验证,当前未获派单资格;不代表账号丢失或永久失效
quota_exhausted本地记录额度已耗尽,尚未进入恢复窗口
quota_frozen额度暂时冻结,等待恢复窗口
unavailable暂不满足其他派单条件

逐账号明细

accounts=true 可拿到每个账号一行的脱敏明细,用 limit(1–500,默认 100)和 offset(默认 0)分页。

curl --fail-with-body -sS "$BASE/v1/pool/status?accounts=true&limit=50" \
  -H "Authorization: Bearer $POOL_KEY"
{
  "generated_at": 1756531200,
  "total": 48, "available": 31,
  "status_counts": {"active": 31, "busy": 2, "cooling": 5, "inactive": 10},
  "availability_counts": {"active": 31, "busy": 2, "cooling": 5, "inactive": 3,
                          "recovery_pending": 4, "quota_exhausted": 1, "quota_frozen": 1, "unavailable": 1},
  "quota": {"total": 192, "remaining": 118, "remaining_available": 84},
  "accounts": [
    {"id": "acc_7f3a91c4", "status": "active", "availability_reason": "active", "quota_total": 4, "quota_remaining": 3, "completed": 214},
    {"id": "acc_1b8d05e6", "status": "cooling", "availability_reason": "cooling", "quota_total": 4, "quota_remaining": 0, "completed": 189}
  ],
  "page": {"limit": 50, "offset": 0, "returned": 48, "total": 48}
}
字段含义
accounts[].id稳定且不可反推的账号标识
accounts[].status原有四类状态之一:active、busy、cooling、inactive
accounts[].availability_reason上表可用性原因之一,区分实际占用、等待恢复及其他不可用原因
accounts[].quota_total该账号配置额度
accounts[].quota_remaining该账号本地估算剩余额度,不是上游余额承诺
accounts[].completed该账号累计成功出片数
page.returned / page.total本次返回行数 / 号池总数
id 是截断的 HMAC:跨多次轮询保持稳定,可以长期跟踪同一个账号,但无法反推出我们这边的任何信息。账号凭据、地区、线路一律不会出现在任何响应里。

号池 Key 可以签发成“仅汇总”权限。这类 Key 请求 accounts=true 会返回 403 accounts_not_allowed,但聚合查询照常可用。

状态type含义
401invalid_pool_key号池 Key 缺失、无效、已禁用或已撤销
403accounts_not_allowed该 Key 只能读聚合数字
429rate_limited号池 Key 超过每分钟调用上限

号池 Key 有独立的 RPM 限制,与出片 Key 互不影响。Key 可以禁用后再启用;已撤销的 Key 永久失效。

错误与限制

状态含义怎么办
202任务已入队(创建成功)开始轮询,别当失败
400图片编码/格式无效,或图床直链拉取失败检查 base64 或 URL(仅 png/jpg/webp)
401Key 缺失、无效或已删除检查 Authorization
402余额不足充值后重试
403账号访问受限,或下载签名无效/过期签名问题重新获取 /signed_url;账号问题联系支持
404任务/图片不存在、已过期或不属于当前账号;/file 也可能尚未就绪先查询任务状态,再确认 id 与归属
409相同幂等请求仍在处理,或视频尚未就绪稍后重试 / 继续轮询
410原提交已处理,但任务记录已删除不再自动重建;确需生成新视频时创建新订单
413 / 422请求过大或字段不合法按错误 message 修正字段
429触发 concurrencydaily_caprpm 限制Retry-After 等待后重试
502 / 504视频下载临时不可用短暂等待后重试下载
{"error":{"code":"401","type":"invalid_key","message":"invalid api key","request_id":"req-example"}}
上面是业务错误体示例;任务查询 HTTP 200 时也可能是 status: failed,此时 error 是字符串。收到 429Retry-After 头或 JSON 顶层 retry_after 等待。反馈时提供请求方法、路径、HTTP 状态码、error.typerequest_id 和任务 id,隐藏 Key。

返回 HTML、403 页面或空响应

先检查完整请求 URL,设置 User-Agent: vinted-video-client/1.0,用余额查询确认网络与鉴权。网关响应可能不是 JSON,保留 HTTP 状态、响应头与 X-Request-ID 后反馈;创建响应不明确时不要更换操作编号。

接口参考

接口字段与响应结构见下方参考。导入 HTTP 客户端使用 https://__ORIGIN__/openapi.json;模型可用组合与价格以 /v1/catalog 和带 Key 的 /v1/pricing 实时返回为准。