Image2PPTAPI

Image2PPT 开发者 API

把图片和 PDF 批量转换成可编辑的 PPTX。上传一批文件,我们在后台用 AI 拆解版面、还原成可编辑的文字与形状,合成一个 PPTX 给你下载。

本文面向对接方的开发同学,读完就能接入。


一分钟了解怎么用

  1. 登录后进入「开发者 / API」页面创建一个 API 密钥。
  2. 调 POST /api/v1/jobs 上传文件(或者给出文件链接),拿到一个任务号。
  3. 每隔几秒调 GET /api/v1/jobs/{任务号} 查进度,直到状态变成 completed;也可以在提交时带上 callbackUrl,任务结束时我们主动通知你(见「完成回调」)。
  4. 调 GET /api/v1/jobs/{任务号}/download 下载成品 PPTX。

如果不再需要结果,可随时调 POST /api/v1/jobs/{任务号}/cancel 请求停止后续页面。要对账或找回之前的任务,调 GET /api/v1/jobs 按时间列出。

转换是异步的:提交后立刻返回任务号,真正的转换在后台跑。别在提交那一步干等结果。

1.3.0 新增的参数全部可选。callbackUrl、pages、Idempotency-Key 请求头和按链接提交都不传时,接口行为与 1.2.0 完全一致,已有的接入代码不用改。


认证

拿到密钥

登录 Image2PPT 后,从账号菜单进入「开发者 / API」页面,在「API Keys」处自助创建,得到一串形如下面的密钥:

i2p_live_xxxxxxxxxxxxxxxxxxxxxxxx

密钥只在创建时完整显示一次,请当场保存好。 之后页面只会显示前几位用于辨认。密钥泄露或需要轮换时,在同一页面吊销旧的、重建新的。

怎么带

每个请求都在 HTTP 头里带上密钥:

Authorization: Bearer i2p_live_xxxxxxxxxxxxxxxxxxxxxxxx

没带或带错,会返回 401(错误码 INVALID_API_KEY)。

基础地址

https://image2ppt.com

下文所有路径都拼在这个地址后面。


统一约定


端点

1. 提交任务 POST /api/v1/jobs

提交一批文件,创建一个转换任务。请求体有两种写法,二选一,不能混用:

multipart 字段

字段 必填 说明
files 是 一个或多个文件。支持 png / jpeg / webp / gif / pdf,单文件不超过 35MB,同一请求的文件内容合计不超过 90MB。同一个字段名 files 重复出现来传多个文件。
locale 否 成品语言环境,zh-CN(默认)或 en。
aspectRatio 否 幻灯片比例,auto(默认,随原图)/ 16:9 / 4:3。
pages 否 只转换其中几页,如 1-3,7,见下方「选页」。不传则全部转换。
callbackUrl 否 任务结束时通知的 https 地址,见「完成回调」。

请求头

请求头 必填 说明
Idempotency-Key 否 防重复提交的标识,网络超时后可以放心重试,见下方「防重复提交」。

一次提交有两条上限,都要满足:

上限 数值 说明
总页数 ≤ 50 页 一张图片算 1 页,一个 PDF 按它的实际页数算。传了 pages 时按选中的页数算,PDF 本身可以超过 50 页。
总体积 ≤ 90MB 这一次请求里所有文件内容加起来。单个文件另有 35MB 的上限。

两条是独立的:40 张高清图片只有 40 页,却很容易超过 90MB。页数没满不代表能提交。

超过总体积会返回 413 PAYLOAD_TOO_LARGE。遇到它请减少单次提交的文件数量分批提交,重试同样的内容不会成功。

官方 SDK 分两种用法,行为不同:submit() / convert()(TypeScript 同名)只提交你给的这一批,会在上传前先本地核对体积和页数,超了直接报错,不会白传一遍,但不会替你拆批;要自动拆批请用 submit_all() / convert_all()(TypeScript 为 submitAll() / convertAll()),它们按体积和页数把文件切成若干次提交,每次一个任务。

成功响应 201 Created

{
  "jobId": "3f2b8c1e-9a4d-4c7b-8e2f-1a6d5c0b9e37",
  "status": "pending",
  "slideCount": 12,
  "creditsReserved": 12
}

带 Idempotency-Key 重放同一个请求时,同样返回 201、结构相同,另带响应头 Idempotent-Replayed: true,见下方「防重复提交」。

curl 示例

curl -X POST https://image2ppt.com/api/v1/jobs \
  -H "Authorization: Bearer i2p_live_xxxx" \
  -H "Idempotency-Key: 7d9e2c4a-order-20261001-001" \
  -F "files=@slide1.png" \
  -F "files=@slide2.png" \
  -F "files=@report.pdf" \
  -F "locale=zh-CN" \
  -F "aspectRatio=16:9" \
  -F "pages=1-3,7" \
  -F "callbackUrl=https://example.com/hooks/image2ppt"

按链接提交

文件已经在对象存储或 CDN 上时,不用先下载再上传,直接把链接交给我们。请求体改成 JSON:

curl -X POST https://image2ppt.com/api/v1/jobs \
  -H "Authorization: Bearer i2p_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": ["https://cdn.example.com/deck.pdf", "https://cdn.example.com/cover.png"],
    "locale": "zh-CN",
    "aspectRatio": "auto",
    "pages": "1-3,7",
    "callbackUrl": "https://example.com/hooks/image2ppt"
  }'
字段 必填 说明
urls 是 文件链接,字符串数组,1–50 条,按顺序拼成一个 PPTX。
locale / aspectRatio / pages / callbackUrl 否 含义同 multipart 字段。

规则:

只要错误是某一条链接引起的——INVALID_URL、URL_FETCH_FAILED,以及下载下来的文件触发的 INVALID_FILE、PAYLOAD_TOO_LARGE——error.index 都会指出是第几条(从 0 开始);其余错误不带这个字段。

选页 pages

只想转换 PDF 里的几页时,用 pages 指定,不必先自己拆 PDF。

HTTP code 场景
400 INVALID_PAGES 写法错误。
400 PAGES_OUT_OF_RANGE 页码为 0,或超过总页数(message 会写明总页数)。
400 TOO_MANY_SLIDES 选中超过 50 页。

防重复提交 Idempotency-Key

网络超时的时候,你不知道上一次提交到底成功没有。带上 Idempotency-Key 请求头,就可以放心用同一个 key 重试:同一个请求只会建一个任务、只扣一次积分。

情况 结果
同一个 key、同一个请求,原任务还在 201,响应结构与首次相同(status 是任务当前的状态),响应头 Idempotent-Replayed: true,不再扣积分。
同一个 key、请求内容不同 422 IDEMPOTENCY_KEY_MISMATCH。
同一个 key 的上一个请求还在处理 409 IDEMPOTENCY_KEY_IN_PROGRESS,带 Retry-After: 2,稍等再重试。
上一个请求失败了(任何 4xx / 5xx) 失败的请求不占用 key,可以用同一个 key 重试。
原任务已经在网站上删除 key 作废,按一次新提交处理。

可能的错误

HTTP code 含义
401 INVALID_API_KEY 密钥无效或缺失。
400 NO_FILES 没有带任何文件,或 urls 为空。
400 INVALID_FILE 文件格式不支持,或单文件超过 35MB。
400 INVALID_PDF PDF 无法读取或解析。
400 TOO_MANY_SLIDES 总页数(或选中的页数)超过 50。
400 INVALID_PAGES pages 写法错误。
400 PAGES_OUT_OF_RANGE pages 里的页码为 0 或超过总页数。
400 INVALID_CALLBACK_URL callbackUrl 不合格:不是 https、太长、带用户名密码,或不是公网地址。
400 INVALID_IDEMPOTENCY_KEY Idempotency-Key 长度不在 1–255 之间,或含 0x21–0x7E 以外的字符(空格、中文都不行)。
400 INVALID_JSON JSON 请求体不是合法 JSON,或不是 JSON 对象。
400 INVALID_PARAMETER JSON 字段类型不对,或超过 50 条链接。
400 INVALID_URL 链接格式不对、不是 https、带用户名密码,或指向内网(包括 DNS 解析后才落到内网的)。带 index。
400 URL_FETCH_FAILED 链接下载失败:无法解析、对方返回非 2xx、超时、重定向超过 4 次或降级到 http。可能是临时故障,可以重试。带 index。
400 UPLOAD_ABORTED 上传中途断开,请求体没收完。重试即可;反复出现多半是这次提交太大,先按上面的体积上限分批。
400 MALFORMED_UPLOAD 请求体不是合法的 multipart/form-data。这是客户端拼装问题,重试不会好——检查边界串和各分段的头。
402 INSUFFICIENT_CREDITS 可用积分不够覆盖这次提交。
409 IDEMPOTENCY_KEY_IN_PROGRESS 同一个 key 的上一个请求还在处理,按 Retry-After 等待后重试。
413 PAYLOAD_TOO_LARGE 同一请求的文件内容合计超过 90MB。
422 IDEMPOTENCY_KEY_MISMATCH 这个 key 已经用在另一个内容不同的请求上。
429 RATE_LIMITED 触发限流,见下方「限流」。

2. 查询任务状态 GET /api/v1/jobs/{jobId}

轮询这个端点看进度。

成功响应 200 OK

{
  "jobId": "3f2b8c1e-9a4d-4c7b-8e2f-1a6d5c0b9e37",
  "status": "processing",
  "progress": 45,
  "slideCount": 12,
  "creditsUsed": 0,
  "creditsRefunded": 0,
  "cancellationRequested": false,
  "createdAt": "2026-07-07 08:00:00",
  "completedAt": null
}

字段

字段 说明
status pending(排队中)/ processing(转换中)/ completed(已完成)/ failed(已失败)。
progress 进度百分比,0–100。
slideCount 总页数。
creditsUsed 结算后实际扣除的积分。
creditsRefunded 部分成功时退回的失败页积分,见「计费与退款」。
cancellationRequested 服务器是否已接受取消请求。为兼容旧客户端,任务状态仍使用原有四种取值。
createdAt / completedAt UTC 创建时间 / 完成时间,格式为 YYYY-MM-DD HH:MM:SS(未完成时 completedAt 为 null)。
downloadUrl 仅当 completed 且成品仍在保留期内时给出,是下载端点的相对路径;其余状态不返回这个字段。
error 仅当 failed 时给出,形如 {"code": "...", "message": "..."}。
pageResults 仅当 completed 或 failed 时给出,逐页交账,见下。
callback 仅当提交时带了 callbackUrl 才有,是完成回调的投递情况,见「完成回调」。

失败时的样子

{
  "jobId": "3f2b8c1e-9a4d-4c7b-8e2f-1a6d5c0b9e37",
  "status": "failed",
  "progress": 0,
  "slideCount": 2,
  "creditsUsed": 0,
  "creditsRefunded": 2,
  "createdAt": "2026-07-07 08:00:00",
  "completedAt": "2026-07-07 08:01:00",
  "error": { "code": "CONVERSION_FAILED", "message": "转换失败,请稍后重试" },
  "pageResults": [
    {
      "pageNumber": 1,
      "status": "failed",
      "error": { "code": "CONVERSION_FAILED", "message": "转换失败,请稍后重试", "retryable": true }
    },
    {
      "pageNumber": 2,
      "status": "failed",
      "error": { "code": "PAGE_NOT_ATTEMPTED", "message": "任务已结束,这一页没有转换结果", "retryable": true }
    }
  ]
}

逐页结果 pageResults

任务到终局(completed 或 failed)后,这个数组按页码顺序列出每一页的结果,长度等于 slideCount。在此之前不返回——任务还在跑的时候,「这页没转出来」和「这页还没轮到」区分不了。

(早期任务没有逐页记录,这个字段也不返回。判断时请检查字段是否存在,不要假定终局任务一定带它。)

creditsRefunded 只回答「有几页没转出来」,pageResults 回答「是哪几页」。

字段 说明
pageNumber 页码,从 1 开始,与你提交的顺序一致(PDF 按拆出来的页序)。用了 pages 选页时,指成品中的第几页(第几个被选中的页)。
status converted:这一页转成了可编辑内容。failed:没转成。
error 仅当 status 为 failed。含 code、message 和 retryable。

失败页有两种去向,靠 error.code 区分:

retryable 表示同样的图再交一次有没有可能成功。目前逐页结果里的每个失败页都是 true——下面那三个码要么是一次性的故障,要么是任务先一步结束、这页没留下结果,重交都有意义。仍然请判断这个字段而不要写死 true:以后新增的码可能带 false。

错误码

任务级 error.code(整单失败时给出)只有两个值,跟这个接口发布时一样:

code 含义
JOB_CANCELLED 你自己取消或放弃了这个任务,且没有可交付页面。
CONVERSION_FAILED 其余所有失败原因。

逐页 pageResults[].error.code 用的是更细的一套:

code 含义 retryable
CONVERSION_FAILED 这页转过但没成,原因不细分。 true
CONVERSION_TIMEOUT 这页超过时间预算被中断。 true
PAGE_NOT_ATTEMPTED 任务先一步结束,这页没有转换结果(没轮到或被打断)。 true

为什么两层粒度不同:任务级那个字段从接口发布起就只有两个值,客户已经部署的代码是照着写的,扩大取值会让那些分支静默失效。细分放在 pageResults 里——那是新字段,没有历史包袱。

message 是给人看的一句话,会跟着你的 Accept-Language 走,不要拿它做分支判断——请判断 code。它不会夹带出错的诊断细节。

两层都可能新增错误码。请把认不出的 code 当作 CONVERSION_FAILED 处理。

可能的错误

HTTP code 含义
404 JOB_NOT_FOUND 任务号不存在、不属于当前密钥所在账户,或已被账户所有者在网站上删除。

提示:任务号只在你自己的账户内可见,别人拿不到、也查不到你的任务。


3. 取消任务 POST /api/v1/jobs/{jobId}/cancel

请求服务器停止这个任务的后续工作。这个动作是收尾式取消,不是硬切:

任务仍在收尾 202 Accepted

{
  "jobId": "3f2b8c1e-9a4d-4c7b-8e2f-1a6d5c0b9e37",
  "cancellationRequested": true,
  "finalizing": true
}

finalizing: true 表示仍有页面或 PPTX 装包正在收尾。继续轮询任务状态,直到变成 completed 或 failed。如果取消在本次请求内已经结算完,则返回 200 OK 且 finalizing: false。

取消后可能有两种终局:

可能的错误

HTTP code 含义
404 JOB_NOT_FOUND 任务号不存在、不是 API 任务,或不属于本账户。
409 JOB_ALREADY_FINISHED 任务已经结束,或已进入收尾、取消不再能改变结果。
500 JOB_CANCEL_FAILED 服务端暂时无法接受取消请求,可安全重试。

4. 下载成品 GET /api/v1/jobs/{jobId}/download

任务完成后,从这里下载 PPTX。

成功响应 200 OK,响应体就是 PPTX 二进制流(Content-Type: application/vnd.openxmlformats-officedocument.presentationml.presentation)。

可能的错误

HTTP code 含义
409 NOT_READY 任务还没完成,成品暂不可下载。等状态变成 completed 再来。
410 OUTPUT_EXPIRED 成品已过保留期被清理,无法下载(见下方「保留期」)。
416 RANGE_NOT_SATISFIABLE Range 请求的起点超出文件大小,请丢弃旧的续传位置后重新下载。
404 JOB_NOT_FOUND 任务号不存在、不属于本账户,或已在网站上被删除。

保留期:成品 PPTX 在完成后保留 7 天,过期自动清理,之后下载会返回 410 OUTPUT_EXPIRED。请在保留期内取走。(历史记录仍在,只是成品文件不再保存。)


5. 查询账户 GET /api/v1/account

成功响应 200 OK

{
  "email": "you@example.com",
  "credits": 328
}

credits 是当前可用积分(不含已被进行中任务锁定的部分)。API 转换与网页端共用同一份积分。Pro 季卡 / 年卡的页数只能在网页和插件里用,不计入这里,API 只扣普通积分。能调用本接口的账号都已开通 API,积分不会因注册赠额到期而作废(只有从没付过费、也没开通 API 的账号,注册赠送的积分才会在 7 天后作废)。


6. 任务列表 GET /api/v1/jobs

按创建时间从新到旧列出本账户通过 API 提交、且没有在网站上删除的任务。适合月底对账,或者找回丢了任务号的任务。

查询参数(都可省略)

参数 说明
createdFrom 起始日期,YYYY-MM-DD(UTC),包含当天。
createdTo 截止日期,YYYY-MM-DD(UTC),包含当天。
limit 每页条数,默认 20,范围 1–100。
cursor 翻页用,填上一页返回的 nextCursor。它是不透明字符串,原样传回即可,不要自己拼。

成功响应 200 OK

{
  "data": [
    {
      "jobId": "3f2b8c1e-9a4d-4c7b-8e2f-1a6d5c0b9e37",
      "status": "completed",
      "progress": 100,
      "slideCount": 12,
      "creditsUsed": 11,
      "creditsRefunded": 1,
      "cancellationRequested": false,
      "createdAt": "2026-10-01 08:00:00",
      "completedAt": "2026-10-01 08:03:10",
      "downloadUrl": "/api/v1/jobs/3f2b8c1e-9a4d-4c7b-8e2f-1a6d5c0b9e37/download"
    }
  ],
  "nextCursor": "eyJjIjoiMjAyNi0xMC0wMSJ9"
}
curl "https://image2ppt.com/api/v1/jobs?createdFrom=2026-09-01&createdTo=2026-09-30&limit=100" \
  -H "Authorization: Bearer i2p_live_xxxx"

这个端点目前不限流,但请照样处理 429:收到时按 Retry-After 等待再重试。

可能的错误

HTTP code 含义
401 INVALID_API_KEY 密钥无效或缺失。
400 INVALID_PARAMETER limit、cursor、createdFrom 或 createdTo 不合法,message 会写明是哪一个。

完成回调(Webhook)

提交时带上 callbackUrl,任务结束时我们会向这个地址发一个 POST,不用再轮询。回调遵循 Standard Webhooks 规范,任何支持这个规范的校验库都能直接用。

什么时候发

任务进入 completed 或 failed 时发送。取消的任务也会落到这两种状态之一,没有单独的「已取消」状态。如果任务被我们重跑后再次结束,会以新的 webhook-id 再发一次。

地址要求

提交时就会检查,不合格返回 400 INVALID_CALLBACK_URL:

请求格式

POST <callbackUrl>
Content-Type: application/json
User-Agent: Image2PPT-Webhook/1.0
webhook-id: msg_2Lx8Qe7cV1nR4tYk
webhook-timestamp: 1790841600
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
请求头 说明
webhook-id 这次投递的编号,形如 msg_…。同一次投递的各次重试都相同,用它去重。
webhook-timestamp 本次发出的时间,Unix 秒。
webhook-signature 签名,形如 v1,<base64>。密钥轮换期间会有两个,用空格分隔。

请求体:

{
  "type": "job.completed",
  "data": {
    "jobId": "3f2b8c1e-9a4d-4c7b-8e2f-1a6d5c0b9e37",
    "status": "completed",
    "progress": 100,
    "slideCount": 12,
    "creditsUsed": 12,
    "creditsRefunded": 0,
    "cancellationRequested": false,
    "createdAt": "2026-10-01 08:00:00",
    "completedAt": "2026-10-01 08:03:10",
    "downloadUrl": "/api/v1/jobs/3f2b8c1e-9a4d-4c7b-8e2f-1a6d5c0b9e37/download",
    "pageResults": [{ "pageNumber": 1, "status": "converted" }]
  }
}

校验签名

每个请求都要先校验签名,再相信里面的内容:

  1. webhook-timestamp 与当前时间相差超过 300 秒,拒收。
  2. 取密钥 whsec_ 前缀之后的那段,按标准 base64 解码,得到 HMAC 密钥。
  3. 签名内容是 <webhook-id>.<webhook-timestamp>.<原始请求体>,用 HMAC-SHA256 计算,结果做标准 base64 编码。
  4. 把 webhook-signature 按空格拆开,任意一个 v1, 后面的值与自己算出的相等(用恒定时间比较)即通过。

一定要用收到的原始字节计算,不要先解析 JSON 再重新序列化。只用 Python 标准库的写法:

import base64, hashlib, hmac, json, time

def verify_webhook(body: bytes, headers, secret: str) -> dict:
    # headers 需按不区分大小写的方式取值(Flask、Django、FastAPI 的 headers 都是)
    msg_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]
    if abs(time.time() - int(timestamp)) > 300:
        raise ValueError("webhook-timestamp 超出 5 分钟")

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()

    for item in headers["webhook-signature"].split(" "):
        version, _, signature = item.partition(",")
        if version == "v1" and hmac.compare_digest(signature, expected):
            return json.loads(body)
    raise ValueError("签名不匹配")

密钥管理

每个账户一份回调密钥,形如 whsec_<base64>(前缀后是 32 个随机字节的标准 base64)。在「开发者 / API」页面查看、复制和轮换;第一次打开这个页面,或第一次提交带 callbackUrl 的任务时自动生成。

轮换后的 24 小时内,签名头会同时带新旧两个签名,你有一天时间把新密钥换上,期间不会漏收。

重试

查看投递情况

带了 callbackUrl 的任务,GET /api/v1/jobs/{jobId} 和任务列表的每一项会多一个 callback 字段:

"callback": {
  "url": "https://example.com/hook",
  "status": "pending",
  "attempts": 1,
  "lastAttemptAt": "2026-10-01 08:00:00",
  "lastResponseStatus": 503,
  "nextAttemptAt": "2026-10-01 08:01:00"
}
字段 说明
url 提交时给的回调地址。
status pending:任务还没结束,或在等下一次投递。delivered:已送达。failed:6 次都失败,不再重试。
attempts 已经投递的次数。
lastAttemptAt 最近一次投递的时间。
lastResponseStatus 最近一次你返回的 HTTP 状态码;网络错误或超时为 null。
nextAttemptAt 下一次投递的时间。

时间都是 UTC,格式 YYYY-MM-DD HH:MM:SS;没有值时为 null。回调最终失败时,任务本身不受影响,照常查询和下载即可。


限流

按账户限流(同一账户下所有密钥共享额度):

超出时返回 429(RATE_LIMITED),并在 Retry-After 响应头给出建议等待的秒数。

查询任务状态不限流。任务列表目前也不限流,但以后可能会加,请统一处理 429。

正确的应对:读 Retry-After,等这么多秒再重试,别无脑立刻重试。官方 Python 客户端的 wait() 已经内建了这个退避;若你自己直接提交,参考下面的伪代码:

import time, requests

while True:
    resp = requests.post(url, headers=headers, files=files)
    if resp.status_code != 429:
        break
    time.sleep(int(resp.headers.get("Retry-After", "5")))

撞到限流后重试时,带上同一个 Idempotency-Key 更稳妥:被拒的请求不占用 key,而万一上一次其实已经成功,也不会建出第二个任务。


版本与升级提示

官方 SDK(0.2.0 起)每个请求都会自报家门:

User-Agent: image2ppt-python/0.2.0
User-Agent: image2ppt-node/0.2.0

这个头只用于统计和联系你,不参与鉴权、不参与限流、不影响任何请求的结果。

自己写客户端的话,不用管这个头——带不带都一样,我们把所有非官方 SDK 的调用统一记成「自建客户端」,不区分语言、不记版本,也不保存你报的那串字符(所以带一个自定义的名字并不会让你的程序在我们这边更容易被认出来,这一点我们不做假承诺)。

只有一个请求:不要冒用 image2ppt-python/... 或 image2ppt-node/...。这两个字符串是官方 SDK 的身份,冒用会让「官方 SDK 占比」这个数失真,而且我们会照着它给你发升级提示——提示里指的版本你根本没在用。

如果你用的官方 SDK 版本已经低于我们仍在维护的最低版本,响应里会多带三个标准头(RFC 8594 / RFC 9745),成功的响应上也带:

Deprecation: @1793491200
Sunset: Sun, 01 Nov 2026 00:00:00 GMT
Link: <https://github.com/image2ppt/image2ppt-sdk/blob/main/CHANGELOG.md>; rel="deprecation"

**这些头只是提醒。**状态码不变,请求照常处理,不会因此被拒绝。官方 SDK 收到后会在你的日志里打一条警告(可以关掉)。真要停用某个版本,我们会提前很久单独公告,不会只靠这个头。


业务语义

异步与时延预期

提交后任务在后台跑。单页典型耗时约 2 分钟,九成任务在 3 分钟内完成。页数多的任务更久。建议轮询间隔从 5 秒起、逐步退避到 15 秒左右,不要每秒猛查。

一个任务 = 一个 PPTX

一次提交的所有文件(多张图 / 多页 PDF)会合成同一个 deck,按上传顺序排页;用了 pages 时只包含选中的页。想要多个独立 PPTX,就分成多次提交。

计费与退款

一句话:你只为成功产出的页付费。


官方 SDK

我们提供 Python 和 Node.js/TypeScript 两个官方客户端,都封装了提交、轮询、取消、下载、429 退避和错误映射。源码、示例和变更记录在 https://github.com/image2ppt/image2ppt-sdk;安装从包页面走:Python 见 https://pypi.org/project/image2ppt/,Node.js 见 https://www.npmjs.com/package/image2ppt。

SDK 只在服务端使用。别把 API 密钥放进浏览器或任何用户能看到的地方——谁都能读出来。

Python

pip install image2ppt
from image2ppt import Image2PPTClient, Image2PPTError, JobFailedError

client = Image2PPTClient(api_key="i2p_live_你的密钥")

try:
    # 一步到位:提交 → 轮询等待 → 下载
    job = client.convert(
        ["slide1.png", "slide2.png", "report.pdf"],
        dest_path="out.pptx",
        locale="zh-CN",
        aspect_ratio="16:9",
    )
    print("完成,用掉积分:", job.credits_used, "退回:", job.credits_refunded)
except JobFailedError as e:
    print("转换失败:", e.code, e.message)
except Image2PPTError as e:
    print("请求出错:", e.status_code, e.code, e.message)

Node.js / TypeScript

零依赖,需要 Node 18+(用内置 fetch)。

npm install image2ppt
import { Image2PPTClient, Image2PPTError, JobFailedError } from "image2ppt";

const client = new Image2PPTClient({ apiKey: "i2p_live_你的密钥" });

try {
  const job = await client.convert(
    ["slide1.png", "slide2.png", "report.pdf"],
    "out.pptx",
    { locale: "zh-CN", aspectRatio: "16:9" },
  );
  console.log("完成,用掉积分:", job.creditsUsed, "退回:", job.creditsRefunded);
} catch (e) {
  if (e instanceof JobFailedError) console.error("转换失败:", e.code, e.message);
  else if (e instanceof Image2PPTError) console.error("请求出错:", e.statusCode, e.code, e.message);
  else throw e;
}

分步控制(submit / wait / download)、账户查询(account)和各异常的完整说明见 GitHub 仓库的 README 与示例。


错误码总表

message 是给人读的,语言只跟着请求的 Accept-Language 走:说中文就给中文,不带这个头或者说别的语言一律给英文。浏览器 cookie、界面语言头这些都不影响它——从浏览器里调这个接口也一样,你声明什么就是什么。要在代码里分支请用 code,它不随语言变。

HTTP code 出现场景
401 INVALID_API_KEY 密钥无效或缺失(所有端点)。
400 NO_FILES 没有带任何文件,或 urls 为空(提交)。
400 INVALID_FILE 文件格式不支持或单文件超 35MB(提交)。
400 INVALID_PDF PDF 无法读取或解析(提交)。
400 INVALID_ASPECT_RATIO 画幅比例不认识,用 auto 或 16:9、4:3(提交)。
400 TOO_MANY_SLIDES 总页数或选中的页数超过 50(提交)。
400 INVALID_PAGES pages 写法错误(提交)。
400 PAGES_OUT_OF_RANGE pages 里的页码为 0 或超过总页数,message 写明总页数(提交)。
400 INVALID_CALLBACK_URL callbackUrl 不是 https、超过 2048 字符、带用户名密码,或不是公网地址(提交)。
400 INVALID_IDEMPOTENCY_KEY Idempotency-Key 不是 1–255 个可打印 ASCII 字符(提交)。
400 INVALID_JSON 请求体不是合法 JSON,或不是 JSON 对象(按链接提交)。
400 INVALID_PARAMETER JSON 字段类型不对或超过 50 条链接(按链接提交);limit、cursor、createdFrom、createdTo 不合法(任务列表)。
400 INVALID_URL 链接格式不对、不是 https、带用户名密码,或指向内网,带 index(按链接提交)。
400 URL_FETCH_FAILED 链接下载失败:无法解析、对方非 2xx、超时、重定向超过 4 次或降级到 http,带 index。可能是临时故障,可重试(按链接提交)。
400 PAGE_RATE_EXCEEDED 单次提交页数就超过每分钟提交上限,永远排不进窗口(提交)。
400 UPLOAD_ABORTED 上传中途断开,请求体没收完(提交)。重试即可;反复出现多半是这次提交太大,见 PAYLOAD_TOO_LARGE。
400 MALFORMED_UPLOAD 请求体不是合法的 multipart/form-data(提交)。客户端拼装问题,重试不会好。
402 INSUFFICIENT_CREDITS 可用积分不足,或余额为 0(提交)。
403 API_KEY_REQUIRED 缺少有效的 API key(提交)。
403 ACCOUNT_DELETED 账号已删除(提交)。
413 PAYLOAD_TOO_LARGE 同一请求的文件内容合计超过 90MB(提交)。
429 RATE_LIMITED 触发限流,带 Retry-After 头(提交)。轮询状态不限流;任务列表目前不限流,但也请照样处理 429。
404 JOB_NOT_FOUND 任务号不存在、不属于本账户,或已在网站上被删除(查询、取消、下载)。
409 IDEMPOTENCY_KEY_IN_PROGRESS 同一个 Idempotency-Key 的上一个请求还在处理,带 Retry-After 头(提交)。
409 JOB_ALREADY_FINISHED 任务已经结束,或已进入收尾、取消不再能改变结果(取消)。
409 NOT_READY 任务未完成就来下载(下载)。
422 IDEMPOTENCY_KEY_MISMATCH 同一个 Idempotency-Key 已用在内容不同的请求上(提交)。
— JOB_CANCELLED 取消已结算且没有可交付页面(任务状态里的 error.code)。
410 OUTPUT_EXPIRED 成品已过保留期被清理(下载)。
416 RANGE_NOT_SATISFIABLE 下载续传范围超出成品文件大小(下载)。
5xx JOB_CANCEL_FAILED、STORAGE_FAILED 等 服务端处理出错,稍后重试;反复出现请联系我们。