AI 画境 API Image Workspace 开放接口
返回工作台

API 文档

画境图像生成开放接口。用一个 API Key 即可在脚本、后端服务或第三方工具里调用文生图与图生图,额度与网页端共用同一个账户。

概览

所有开放接口都在 /v1 路径下,请求与响应均为 JSON(图生图额外支持 multipart 上传)。接口风格接近 OpenAI Images API,但不是它的完全兼容层,字段以本文档为准。

Base URL

https://your-domain.example/v1

本页所有示例里的地址已经自动替换成你当前访问的域名,复制下来就能直接跑,不用再手动改。

接口一览

5 分钟上手

  1. 创建密钥。登录网页端,进入「API 密钥」标签页,填个备注后点「创建密钥」。密钥形如 mk- 加 48 位十六进制字符,只显示这一次,请立刻保存。备注旁边的下拉可以把这把密钥绑定到一个模型,想两个模型分开算账就各建一把。
  2. 确认有额度。GET /v1/balance 看看余额;没有的话用兑换码或在网页端充值。
  3. 发第一个请求。下面这条命令会生成一张图并返回可直接下载的 URL。
curl -X POST https://your-domain.example/v1/images/generations \
  -H "Authorization: Bearer mk-你的密钥" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "一只在草地上奔跑的柴犬,逆光,浅景深,35mm 胶片质感",
    "ratio": "16:9",
    "resolution": "2k"
  }'

返回:

{
  "ok": true,
  "images": ["https://.../a1b2c3.png"],
  "resolution": "2k",
  "model": "gpt-image",
  "cost": 1,
  "image_count": 1,
  "balance": 999,
  "session_id": "a8f3d2e1"
}

生成很慢,这是正常的。单次生成通常 1-5 分钟,接口是同步阻塞的,会一直挂着直到出图。客户端超时请设到 320 秒以上,否则会在服务端还在正常出图时被自己的超时掐断。

鉴权

每个 /v1 请求都要带上 API Key,两种写法任选其一,效果完全相同:

Authorization: Bearer mk-xxxxxxxxxxxxxxxx
x-api-key: mk-xxxxxxxxxxxxxxxx

密钥的性质

  • 格式为 mk- + 48 位十六进制字符,共 51 个字符。
  • 服务端只保存 SHA-256 摘要,不存明文。密钥丢了没有任何找回途径,只能删掉重建。
  • 一个账户可以创建多个密钥,便于按用途区分和单独吊销。列表里只显示前 10 位加掩码,例如 mk-a1b2c3d********
  • 删除(吊销)后立即失效,正在使用它的程序会开始收到 401 api_key_revoked
  • 密钥代表账户全部权限,可以花掉账户余额。不要写进前端代码、不要提交进 Git 仓库。

按模型分开的密钥

创建密钥时可以绑定一个模型,这把密钥之后就只能调那一个模型。用途是把不同模型的流量、花费和调用记录拆开:一把给 GPT Image 的服务用,一把给 Nano Banana 的服务用,两边互不影响,其中一把泄露也只影响一条线。

密钥类型请求不带 model请求带别的 model
不限模型(默认)用站点默认模型照请求的模型执行
绑定 gpt-imagegpt-image403 model_not_allowed
绑定 nano-banananano-banana403 model_not_allowed

绑定关系在 GET /v1/mekey.model 里能查到;GET /v1/models 对绑定过的密钥只会列出它能用的那一个模型,照着这个列表选模型就不会踩到 model_not_allowed。绑定只能在创建时指定,事后改不了——需要换模型就再建一把。

老密钥(这个功能上线之前创建的)一律算「不限模型」,行为和以前完全一致,不用改代码。

密钥不能用于管理后台。/api/admin/* 只接受管理员的浏览器会话,API Key 无法访问,也无法查看或修改其他用户的数据。

鉴权失败

HTTP/1.1 401 Unauthorized

{ "error": { "code": "invalid_api_key", "message": "无效的 API Key" } }

密钥格式对但已被删除时返回的是 api_key_revoked,用来和「打错字 / 用了别站的密钥」区分开。两种都不要重试。

额度与计费

账户余额和所有价格对外统一以「分」为单位(响应里的 balanceUnit: "fen")。1 元 = 100 分。允许出现 0.5 这样的半分,因为内部按半分存储以避免四舍五入误差。

扣费流程

为了防止并发请求把余额刷成负数,计费分两步走:

  1. 预扣:请求进来先按 单价 × max_output_images 扣掉一笔(最坏情况的钱)。
  2. 结算:拿到结果后按实际出图张数计费,多扣的部分立刻退回。

举例:文生图单价 1 分,max_output_images 为 3,那么请求瞬间先扣 3 分;如果最终只出了 1 张图,就退回 2 分,本次实际花费 1 分。响应里的 cost实际花费balance结算后的余额

失败一定会全额退款。不管是提示词被拒、没有可用节点、上游超时还是服务端崩溃重启,预扣的额度都会退回。服务启动时会扫描所有未结算的预扣并退还,所以进程被杀掉也不会吞钱。

哪些接口花钱

接口计费
POST /v1/images/generations按文生图单价 × 实际出图张数
POST /v1/images/edits按图生图单价 × 实际出图张数(通常比文生图贵)
其余所有接口免费,只受速率限制

实时单价通过 GET /v1/pricingGET /v1/balance 获取,不要在代码里写死——管理员随时可以调价,站点搞活动时价格还会临时下调。

通用约定

请求

  • 除图生图的 multipart 形式外,请求体一律为 JSON,需要带 Content-Type: application/json
  • 普通接口请求体上限 128 KB/v1/images/edits 放宽到 30 MB(要塞 base64 图片)。
  • 未知字段会被忽略,不会报错。

成功响应

一律带 ok: true,HTTP 状态码 200

错误响应

/v1 下的错误结构统一error 恒为对象,永远有 codemessage 两个字段:

{
  "error": {
    "code": "insufficient_credits",
    "message": "余额不足:最多需要 3 分,当前 1 分",
    "hint": "(可选)仅部分错误会带,给出可操作的修复建议"
  },
  "balance": 1
}

两个生成接口在出错时会额外返回 balance,方便你直接判断是不是余额问题,不用再多发一次查询。

请按 code 判断错误类型,不要匹配 messagemessage 是给人看的中文提示,措辞会随版本变化;code 才是稳定契约。

时间与金额字段

字段形态含义
created_at / last_used_atUnix 毫秒时间戳(整数)。JavaScript 里 new Date(v) 直接可用。
cost / balance / delta以「分」为单位,可能带一位小数。
cost_units / delta_units内部半分单位的整数值,一般不用关心。

图片 URL 的有效期

返回的图片是上游 CDN 的直链,不会永久有效。拿到后请尽快下载转存到自己的存储,不要把这些 URL 直接存进数据库当长期资源用。

速率限制

限流按 API Key 计算(没带密钥时退化为按 IP)。触发后返回 429

{ "error": { "code": "rate_limited", "message": "API 调用过于频繁,请降低速率" } }
范围默认额度环境变量
全部 /v1 接口60 次 / 分钟RL_API
POST /v1/redeem(叠加)30 次 / 10 分钟RL_REDEEM

响应头带标准的 RateLimit-* 字段(draft-7),可以据此做退避。这些额度由部署方在环境变量里配置,自建实例可以自行调高。

被限流挡掉的请求也会记进调用历史,所以到底是哪个时段、哪把密钥撞上了限流,翻 GET /v1/usage 就能看出来,不用自己在客户端埋点。

并发上限

限流之外还有同时进行的生成任务上限,这是另一套机制:

  • 单用户默认 3 个并发生成,超出返回 429 too_many_requests。实时值见 GET /v1/pricinglimits.max_concurrent_per_user
  • 全站默认 20 个并发生成,超出返回 503 busy,等一会儿重试即可。

批量任务建议用固定大小的工作池(并发数取 max_concurrent_per_user),而不是一次性把请求全丢出去。

文生图

POST /v1/images/generations 需要密钥 消耗额度

根据文字提示词生成图片。同步接口,等到出图才返回,通常耗时 1-5 分钟。

请求参数(JSON)

参数类型说明
prompt 必填 string 图片描述。描述越具体结果越稳定,建议写清主体、场景、光线、镜头和风格。服务端会自动加上「生成图片:」前缀以确保走生图模式,你不需要自己加。最长 4000 字,超了返回 prompt_too_long
ratio 可选 string 画面比例,可选 1:116:99:164:33:4。留空表示不指定,由模型自行决定。无法识别的值等同留空,不会报错。
resolution 可选 string 输出分辨率,可选 1k2k4k,默认 2k。非法值静默回落到 2k
model 可选 string 生图模型,可选 gpt-imagenano-banana。留空使用站点默认模型;填了不支持的值会直接报错而不是回落,避免你以为用上了实际没用上。密钥绑定了模型时留空即用绑定的那个,填别的会返回 403 model_not_allowed

关于 ratio 的实现细节:上游协议没有独立的比例字段,服务端是把比例要求作为一句中文提示追加到提示词末尾来实现的。因此比例是倾向性引导而非硬约束,出图尺寸可能与要求略有出入。

响应字段

字段类型说明
okboolean恒为 true
imagesstring[]图片直链数组,顺序即生成顺序。至少一张,否则本次会按失败处理并退款。
resolutionstring实际使用的分辨率
modelstring实际使用的模型 id
costnumber本次实际扣费(分),已按出图张数结算完毕
image_countnumber出图张数,等于 images.length
balancenumber结算后的账户余额(分)
session_idstring本次生成的会话标识,排查问题时可提供给管理员

示例

curl -X POST https://your-domain.example/v1/images/generations \
  -H "Authorization: Bearer mk-你的密钥" \
  -H "Content-Type: application/json" \
  --max-time 330 \
  -d '{
    "prompt": "极简主义产品摄影:一只哑光黑陶瓷咖啡杯放在浅灰水泥台面上,柔和顶光,阴影干净,高级商业质感",
    "ratio": "1:1",
    "resolution": "4k",
    "model": "gpt-image"
  }'

可能的错误

状态码code含义与处理
400missing_promptprompt 为空或只有空白字符
400prompt_too_longprompt 超过 4000 字,不扣费
400unsupported_modelmodel 不在支持列表里,先查 /v1/models
400prompt_rejected提示词未通过内容审核,不扣费。响应带 hint 说明如何修改
401invalid_api_key密钥不存在
401api_key_revoked密钥已被删除,重新创建一把
402insufficient_credits余额不足以覆盖预扣金额,先充值
403model_not_allowed这把密钥绑定的是别的模型,不扣费。响应带 hint 指出该换哪把密钥
429rate_limited超出速率限制,退避后重试
429too_many_requests你的并发生成数已达上限,等前面的任务完成
503no_channel暂无空闲生成节点,已全额退款,稍后重试
503busy全站并发已满,已全额退款,稍后重试
500generation_failed上游生成失败或超时,已全额退款

内容审核

提示词在扣费和调用上游之前会先过一遍本地检查,命中直接拒绝,不产生任何费用。规则刻意收得很窄,需要两个独立信号同时出现才会命中,正常创作类提示词不会被误伤。拦截的是:涉及未成年人的性化内容、真实人物的裸露内容、露骨色情、武器爆炸物制作方法、毒品制作方法,以及站点管理员自定义的禁用词。

{
  "error": {
    "code": "prompt_rejected",
    "message": "提示词未通过内容审核(露骨色情内容)",
    "hint": "提示词可能触发了内容审核。建议去掉真人姓名、品牌标识、暴力或成人相关描述后重试。"
  },
  "balance": 1000
}

本地检查放行不代表一定能出图,上游还有自己的审核。上游拒绝时同样返回 prompt_rejected,同样不扣费。

图生图

POST /v1/images/edits 需要密钥 消耗额度

带参考图生成新图片。同步接口,耗时与文生图相当。支持 multipart 文件上传和 JSON 两种传图方式。

方式一:multipart/form-data(推荐)

直接上传本地文件,不用做 base64 编码,请求体也小得多。

字段类型说明
image 必填file参考图文件。字段名也可以用 imagesimage[],效果相同。
prompt 必填string希望如何改动或参考这张图
ratio / resolution / model 可选string文生图完全一致
curl -X POST https://your-domain.example/v1/images/edits \
  -H "Authorization: Bearer mk-你的密钥" \
  --max-time 330 \
  -F "image=@./reference.png" \
  -F "prompt=保持人物姿势不变,把背景换成黄昏海滩,增加暖色边缘光" \
  -F "resolution=2k"

方式二:JSON

适合图片来自远程 URL 或已经是 base64 的场景。以下字段任选一个

字段类型说明
imagestringdata URL,形如 data:image/png;base64,iVBORw0...
image_urlstring公网可访问的图片 URL,服务端会代为下载
imagesstring[]多张 data URL(受参考图数量上限约束)
image_urlsstring[]多张图片 URL(同上)
filename / filenames 可选string / string[]指定上传文件名,不填会自动生成
mime 可选string指定 MIME 类型,一般不需要,服务端会从图片内容嗅探
curl -X POST https://your-domain.example/v1/images/edits \
  -H "Authorization: Bearer mk-你的密钥" \
  -H "Content-Type: application/json" \
  --max-time 330 \
  -d '{
    "image_url": "https://example.com/reference.jpg",
    "prompt": "转换成水彩插画风格,保留构图",
    "ratio": "4:3"
  }'

参考图数量默认上限是 1 张。只有站点开启活动期间才会放宽。调用前请读 GET /v1/pricinglimits.max_reference_images,超出会返回 too_many_reference_images

图片要求

  • 支持 PNG、JPEG、WebP、GIF。服务端按文件头嗅探真实类型,改扩展名骗不过去。
  • 单张不超过 20 MB;JSON 方式整个请求体不超过 30 MB
  • image_url 时目标必须是公网地址。指向 127.0.0.1、内网段、云元数据地址(169.254.169.254)等一律被拒绝,这是防 SSRF 的硬性限制,DNS 解析后的真实 IP 也会检查。

响应字段

比文生图多三个字段,其余相同:

字段类型说明
modestring恒为 "image_to_image"
reference_countnumber实际使用的参考图数量
finalTextstring模型返回的文字说明,可能为空字符串
{
  "ok": true,
  "mode": "image_to_image",
  "images": ["https://.../edited.png"],
  "finalText": "已将背景替换为黄昏海滩。",
  "resolution": "2k",
  "model": "gpt-image",
  "reference_count": 1,
  "cost": 2.5,
  "image_count": 1,
  "balance": 996.5,
  "session_id": "b7c1e9f4"
}

额外的错误

文生图那张表里的全部错误外,还可能出现:

状态码code含义
400missing_image没有提供任何参考图
400too_many_reference_images参考图数量超过 max_reference_images
400invalid_image不是可识别的图片格式、下载失败,或 URL 指向内网地址
400LIMIT_FILE_SIZE单张参考图超过 20 MB
413payload_too_largeJSON 请求体超过 30 MB

查询余额

GET /v1/balance 需要密钥

查询当前余额和实时价格。生成前先调这个可以避免因余额不足白等一次请求。

curl https://your-domain.example/v1/balance \
  -H "Authorization: Bearer mk-你的密钥"
{
  "ok": true,
  "credits": 998.5,
  "pricing": {
    "text": { "halfFen": 2, "fen": 1, "yuan": 0.01 },
    "edit": { "halfFen": 5, "fen": 2.5, "yuan": 0.025 },
    "maxOutputImages": 3,
    "balanceUnit": "fen",
    "promo": false
  }
}
字段说明
credits当前余额(分)
pricing.text文生图单价。fen 是对外单位,yuan 是元,halfFen 是内部半分单位
pricing.edit图生图单价,结构同上
pricing.maxOutputImages单次最多出图张数,也是预扣的倍数
pricing.promo站点是否处于活动期。为 true 时价格和各项上限可能临时放宽

账户信息

GET /v1/me 需要密钥

查询密钥所属账户的基本信息。可用来验证密钥是否有效、属于哪个账户。

curl https://your-domain.example/v1/me \
  -H "Authorization: Bearer mk-你的密钥"
{
  "ok": true,
  "username": "alice@example.com",
  "email": "alice@example.com",
  "credits": 998.5,
  "credit_unit": "fen",
  "created_at": 1754870400000,
  "key": { "label": "生产环境-banana", "model": "nano-banana" }
}

email 在纯用户名注册的账户上为 nullkey 描述的是当前这把密钥modelnull 表示不限模型,否则就是它绑定的模型

使用兑换码

POST /v1/redeem 需要密钥

用兑换码给当前账户充值。兑换码由管理员发放,一码只能用一次。

请求参数(JSON)

参数类型说明
code 必填string兑换码。大小写不敏感,首尾空格会自动去掉。
curl -X POST https://your-domain.example/v1/redeem \
  -H "Authorization: Bearer mk-你的密钥" \
  -H "Content-Type: application/json" \
  -d '{"code": "FL53HIJDSTQF"}'
{ "ok": true, "added": 100, "balance": 1098.5 }
状态码code含义
400missing_codecode 为空
400invalid_code兑换码不存在
400code_already_used该兑换码已被使用过
429rate_limited兑换过于频繁(防爆破,默认 10 分钟 30 次)

并发提交同一个兑换码只会有一次成功入账,其余返回 code_already_used,不会重复加钱。

模型列表

GET /v1/models 需要密钥

列出这把密钥能用的生图模型和默认模型。

{
  "ok": true,
  "models": [
    { "id": "gpt-image",   "label": "GPT Image" },
    { "id": "nano-banana", "label": "Nano Banana" }
  ],
  "default_model": "gpt-image",
  "key_model": null
}

生成接口的 model 参数只接受这里列出的 idlabel 是给界面显示用的名字。

key_model 是当前密钥绑定的模型,不限模型时为 null。绑定过的密钥这里只会返回那一个模型,default_model 也跟着变成它:

{
  "ok": true,
  "models": [ { "id": "nano-banana", "label": "Nano Banana" } ],
  "default_model": "nano-banana",
  "key_model": "nano-banana"
}

价格与限制

GET /v1/pricing 需要密钥

一次性拿到所有价格和参数上限。写客户端时建议启动时调一次并缓存几分钟,而不是把这些值硬编码。

{
  "ok": true,
  "pricing": {
    "text": { "halfFen": 2, "fen": 1, "yuan": 0.01 },
    "edit": { "halfFen": 5, "fen": 2.5, "yuan": 0.025 },
    "maxOutputImages": 3,
    "balanceUnit": "fen",
    "promo": false
  },
  "limits": {
    "max_output_images": 3,
    "max_reference_images": 1,
    "max_concurrent_per_user": 3,
    "resolutions": ["1k", "2k", "4k"],
    "ratios": ["1:1", "16:9", "9:16", "4:3", "3:4"]
  }
}
字段说明
limits.max_output_images单次最多出图张数,预扣金额 = 单价 × 该值
limits.max_reference_images图生图单次最多几张参考图,默认 1
limits.max_concurrent_per_user你能同时跑几个生成任务,批量任务的工作池大小照这个设
limits.resolutionsresolution 参数的合法取值
limits.ratiosratio 参数的合法取值,另外允许留空

健康检查

GET /healthz 免鉴权

服务存活探针。注意路径不在 /v1,也不需要密钥,适合给负载均衡或监控用。

{ "ok": true, "uptime": 86400.5, "inflight": 2 }
字段说明
uptime进程已运行秒数
inflight当前全站正在进行的生成任务数。接近全站上限(默认 20)时新请求会收到 503 busy

生成记录

GET /v1/generations 需要密钥

按时间倒序返回本账户的历史生成记录,包含失败的任务。

查询参数

参数说明
limit 可选返回条数,默认 30,最大 100。非法或超范围的值会回落到默认/上限,不报错。
curl "https://your-domain.example/v1/generations?limit=5" \
  -H "Authorization: Bearer mk-你的密钥"
{
  "ok": true,
  "generations": [
    {
      "id": 42,
      "prompt": "一只在草地上奔跑的柴犬",
      "ratio": "16:9",
      "resolution": "2k",
      "model": "gpt-image",
      "cost": 1,
      "cost_units": 2,
      "status": "success",
      "session_id": "a8f3d2e1",
      "created_at": 1754870400000,
      "images": ["https://.../a1b2c3.png"]
    }
  ]
}

status"success""failed"。失败记录的 cost 恒为 0(已退款),images 为空数组。model 是这次实际使用的模型,这个字段上线之前的老记录为 null

历史记录里的图片直链大概率已经失效,因为上游 CDN 链接有有效期。这个接口用于对账和审计,不要拿来当图床。

调用历史

GET /v1/usage 需要密钥

返回 /v1 下的逐次调用记录,成功和失败都记。默认只看当前这把密钥,用来核对某一路服务、某一个模型到底调了多少、错在哪。

查询参数

参数说明
limit 可选返回条数,默认 50,最大 200
scope 可选默认 key,只返回当前密钥的记录;传 account 返回本账户所有密钥的记录
curl "https://your-domain.example/v1/usage?limit=20" \
  -H "Authorization: Bearer mk-你的密钥"
{
  "ok": true,
  "scope": "key",
  "summary": {
    "days": 7,
    "total": 128,
    "succeeded": 124,
    "failed": 4,
    "images": 131,
    "cost": 141.5,
    "cost_units": 283
  },
  "calls": [
    {
      "id": 9012,
      "api_key_id": 7,
      "api_key": "mk-a1b2c3d********",
      "key_label": "生产环境-banana",
      "method": "POST",
      "path": "/v1/images/generations",
      "model": "nano-banana",
      "status": 200,
      "ok": true,
      "error_code": null,
      "cost": 1,
      "cost_units": 2,
      "image_count": 1,
      "duration_ms": 42318,
      "created_at": 1754870400000
    },
    {
      "method": "POST",
      "path": "/v1/images/generations",
      "model": "gpt-image",
      "status": 403,
      "ok": false,
      "error_code": "model_not_allowed",
      "cost": 0,
      "image_count": 0,
      "duration_ms": 3,
      "created_at": 1754870300000
    }
  ]
}
字段说明
summary近 7 天的汇总:调用次数、成功/失败数、出图张数和总花费(分)
okHTTP 状态码是否 2xx,等价于 status >= 200 && status < 300
error_code失败时的 error.code,成功为 null
model本次用的模型;被 model_not_allowed 拒掉时记的是它想调用的那个,方便定位是哪段代码传错了。不涉及模型的接口为 null
cost这次调用的实际花费(分)。不计费的接口恒为 0
duration_ms服务端处理耗时,含等待上游出图的时间

被限流挡掉的 429 也会记进来,所以速率调优可以直接看这里。记录保留 30 天,更早的会被自动清理——需要长期留存请自行定期拉走。

用已删除的密钥发请求同样会记在它名下(401 api_key_revoked),方便定位「哪台机器上还有个旧脚本在跑」。完全不认识的密钥无从归属,不会记录。

额度流水

GET /v1/ledger 需要密钥

按时间倒序返回额度变动明细,包括注册赠送、充值、兑换、扣费和退款。

查询参数

参数说明
limit 可选返回条数,默认 50,最大 200
{
  "ok": true,
  "ledger": [
    { "delta": -3,  "delta_units": -6, "reason": "文生图 预扣最多 3 张", "balance_after": 997,   "created_at": 1754870400000 },
    { "delta": 2,   "delta_units": 4,  "reason": "按实际输出张数退回差额", "balance_after": 999, "created_at": 1754870460000 },
    { "delta": 100, "delta_units": 200,"reason": "兑换码充值 FL53HIJDSTQF", "balance_after": 1099, "created_at": 1754870500000 }
  ]
}

delta 为正表示入账,为负表示扣费;balance_after 是该笔变动之后的余额。一次生成通常对应两条记录——一条预扣、一条退差额,这是计费流程的正常表现,不是重复记账。

错误码总表

error.code 处理,不要匹配 message

状态码code含义建议处理
400missing_prompt缺少提示词修正请求
400prompt_too_long提示词超过 4000 字截断后重试
400missing_code缺少兑换码修正请求
400missing_image图生图未提供参考图修正请求
400unsupported_model模型不在支持列表/v1/models
400prompt_rejected提示词未通过审核不要重试,改提示词
400invalid_image图片无法识别、下载失败或指向内网换一张图或换公网 URL
400too_many_reference_images参考图超量减少张数
400invalid_code兑换码不存在核对兑换码
400code_already_used兑换码已使用换一个码
400invalid_json请求体不是合法 JSON检查序列化
400LIMIT_FILE_SIZE单张图片超过 20 MB压缩后重传
401invalid_api_key密钥不存在不要重试,检查密钥
401api_key_revoked密钥已被删除不要重试,重新创建一把
402insufficient_credits余额不足不要重试,先充值
403model_not_allowed密钥绑定了别的模型不要重试,换对应模型的密钥
413payload_too_large请求体超限改用 multipart 上传
429rate_limited超出速率限制指数退避后重试
429too_many_requests你的并发生成数已满等待在跑的任务完成
500generation_failed生成失败,已退款可重试
500internal_error服务端内部错误可重试,持续出现请联系管理员
503no_channel暂无空闲生成节点,已退款稍后重试
503busy全站并发已满,已退款稍后重试

重试建议

  • 可以重试:generation_failedno_channelbusyinternal_errorrate_limitedtoo_many_requests。建议指数退避,首次等 5 秒起步。
  • 不要重试:所有 400 类错误、invalid_api_keyapi_key_revokedmodel_not_allowedinsufficient_credits。重试只会白白消耗速率配额。

完整示例

依赖 requestspip install requests)。包含超时设置、错误分类和自动重试。

import time
import requests

BASE = "https://your-domain.example"
API_KEY = "mk-你的密钥"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

# 生成最长 5 分钟,超时必须给够,否则会掐断服务端还在正常处理的请求
TIMEOUT = (10, 330)  # (连接超时, 读取超时)

RETRYABLE = {"generation_failed", "no_channel", "busy", "internal_error", "rate_limited"}


class ApiError(Exception):
    def __init__(self, code, message):
        super().__init__(f"[{code}] {message}")
        self.code = code


def generate(prompt, ratio="", resolution="2k", model=None, max_retries=3):
    payload = {"prompt": prompt, "ratio": ratio, "resolution": resolution}
    if model:
        payload["model"] = model

    for attempt in range(max_retries):
        response = requests.post(
            f"{BASE}/v1/images/generations",
            headers={**HEADERS, "Content-Type": "application/json"},
            json=payload,
            timeout=TIMEOUT,
        )
        data = response.json()
        if response.ok:
            return data

        error = data.get("error", {})
        code = error.get("code", "unknown")
        if code not in RETRYABLE or attempt == max_retries - 1:
            raise ApiError(code, error.get("message", "未知错误"))
        time.sleep(5 * (2 ** attempt))  # 5s, 10s, 20s


def edit(image_path, prompt, **kwargs):
    with open(image_path, "rb") as handle:
        response = requests.post(
            f"{BASE}/v1/images/edits",
            headers=HEADERS,
            files={"image": handle},
            data={"prompt": prompt, **kwargs},
            timeout=TIMEOUT,
        )
    data = response.json()
    if not response.ok:
        error = data.get("error", {})
        raise ApiError(error.get("code", "unknown"), error.get("message", ""))
    return data


if __name__ == "__main__":
    balance = requests.get(f"{BASE}/v1/balance", headers=HEADERS, timeout=30).json()
    print(f"当前余额 {balance['credits']} 分,文生图单价 {balance['pricing']['text']['fen']} 分")

    result = generate("雪后的京都清水寺清晨,薄雾,暖色灯笼", ratio="16:9")
    print(f"花费 {result['cost']} 分,余额 {result['balance']} 分")
    for index, url in enumerate(result["images"]):
        image = requests.get(url, timeout=60)
        with open(f"output_{index}.png", "wb") as handle:
            handle.write(image.content)
        print(f"已保存 output_{index}.png")

Node.js 18+ 自带 fetch,无需额外依赖。注意必须显式放宽超时,否则默认设置会提前中断。

const fs = require("node:fs");

const BASE = "https://your-domain.example";
const API_KEY = "mk-你的密钥";

const RETRYABLE = new Set(["generation_failed", "no_channel", "busy", "internal_error", "rate_limited"]);
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

async function call(path, options = {}) {
  // 生成最长 5 分钟,给到 330 秒留出余量
  const response = await fetch(BASE + path, {
    ...options,
    headers: { Authorization: `Bearer ${API_KEY}`, ...options.headers },
    signal: AbortSignal.timeout(330000),
  });
  const data = await response.json();
  if (!response.ok) {
    const error = new Error(data.error?.message || "请求失败");
    error.code = data.error?.code || "unknown";
    throw error;
  }
  return data;
}

async function generate(prompt, options = {}, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await call("/v1/images/generations", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ prompt, ...options }),
      });
    } catch (error) {
      if (!RETRYABLE.has(error.code) || attempt === maxRetries - 1) throw error;
      await sleep(5000 * 2 ** attempt);
    }
  }
}

async function edit(imagePath, prompt, options = {}) {
  const form = new FormData();
  form.append("image", new Blob([fs.readFileSync(imagePath)]), imagePath.split(/[\\/]/).pop());
  form.append("prompt", prompt);
  for (const [key, value] of Object.entries(options)) form.append(key, value);
  return call("/v1/images/edits", { method: "POST", body: form });
}

(async () => {
  const { credits, pricing } = await call("/v1/balance");
  console.log(`当前余额 ${credits} 分,文生图单价 ${pricing.text.fen} 分`);

  const result = await generate("赛博朋克风格的东京街头,雨夜,霓虹倒影", { ratio: "16:9", resolution: "2k" });
  console.log(`花费 ${result.cost} 分,余额 ${result.balance} 分`);

  for (const [index, url] of result.images.entries()) {
    const image = await fetch(url);
    fs.writeFileSync(`output_${index}.png`, Buffer.from(await image.arrayBuffer()));
    console.log(`已保存 output_${index}.png`);
  }
})().catch(error => {
  console.error(`失败 [${error.code}]: ${error.message}`);
  process.exit(1);
});

需要 curljq。演示生成后直接下载所有图片。

#!/usr/bin/env bash
set -euo pipefail

BASE="https://your-domain.example"
API_KEY="mk-你的密钥"

# 先看余额
curl -sS "$BASE/v1/balance" -H "Authorization: Bearer $API_KEY" | jq

# 生成图片。--max-time 必须大于服务端 5 分钟的生成上限
response=$(curl -sS -X POST "$BASE/v1/images/generations" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  --max-time 330 \
  -d '{"prompt": "北欧极简客厅,自然光,原木家具", "ratio": "4:3"}')

if [ "$(echo "$response" | jq -r '.ok // false')" != "true" ]; then
  echo "失败: $(echo "$response" | jq -r '.error.code'): $(echo "$response" | jq -r '.error.message')" >&2
  exit 1
fi

echo "花费 $(echo "$response" | jq -r '.cost') 分,余额 $(echo "$response" | jq -r '.balance') 分"

# 下载全部结果
echo "$response" | jq -r '.images[]' | while IFS= read -r url; do
  filename="output_$(date +%s)_$RANDOM.png"
  curl -sS -o "$filename" "$url"
  echo "已保存 $filename"
done

常见问题

请求一直卡着不返回,是挂了吗?

大概率没挂。生成是同步的,正常就要 1-5 分钟。请确认客户端读取超时设到了 320 秒以上。如果你在 nginx 之类的反向代理后面,代理的 proxy_read_timeout 也要一起调大,否则会被代理提前掐断。

能异步提交、轮询取结果吗?

目前不行,只有同步接口。网页端用的是 SSE 流式推进度,但那条路径依赖浏览器会话,API Key 用不了。需要并行时请用多线程/多协程配合工作池,池大小取 limits.max_concurrent_per_user

为什么请求刚发出去余额就掉了一大截?

那是预扣。系统按最坏情况(单价 × max_output_images)先扣,出图后按实际张数把多余的退回来。以响应里的 costbalance 为准。

失败了会退款吗?

会,而且是全额。所有失败路径都退款,包括进程被杀这种极端情况——服务下次启动时会扫描未结算的预扣并退还。

图片链接过一段时间就 403 / 404 了?

返回的是上游 CDN 直链,有有效期。请在拿到结果后立即下载转存到你自己的对象存储,不要长期引用这些 URL。

ratio 设了但出图比例不太对?

上游协议没有独立的比例参数,服务端是把比例要求作为提示词的一部分传过去的,所以只是引导而非硬约束。对尺寸有严格要求的话,建议拿到图后自己裁剪。

图生图能一次传多张参考图吗?

接口本身支持数组,但默认上限是 1 张,只有站点开启活动时才会放宽。先读 /v1/pricinglimits.max_reference_images,别写死。

密钥能创建几个?弄丢了怎么办?

数量不限,建议按用途分开建,方便单独吊销。明文只在创建时显示一次,服务端只存摘要,丢了无法找回,删掉重建即可。

怎么把两个模型的用量分开统计?

给每个模型各建一把绑定该模型的密钥,然后用 GET /v1/usage 分别看各自的调用次数和花费。这么做还顺带上了一道保险:绑定 nano-banana 的密钥不可能因为哪里传错参数就跑去调 gpt-image

已经在用的密钥能改绑定的模型吗?

不能,绑定只在创建时确定。这是有意的——中途改绑定会让历史记录变得没法解释:同一把密钥前后调的是两个模型,按密钥做的用量统计就对不上了。换模型请新建一把,旧的确认没人用了再删。

调用历史能留多久?能导出吗?

默认保留 30 天,之后自动清理。要长期留存就定期拉 GET /v1/usage?scope=account 存到自己那边;单次最多返回 200 条,按 created_at 自行去重即可。

API 能管理其他用户或改站点配置吗?

不能。API Key 只能操作它所属的那个账户。管理类接口 /api/admin/* 只认管理员的浏览器会话。

为什么 /v1/ledger 里一次生成有两条记录?

一条是预扣,一条是按实际张数退回差额,属于正常的两阶段记账。把两条加起来才是这次生成的净花费。