Image2PPTAPI

Image2PPT API

Batch-convert images and PDFs into editable PowerPoint (.pptx). You upload a batch of files; Image2PPT reconstructs the layout with AI into editable text and shapes, and hands you back one .pptx.

This doc is for developers integrating the API — read it top to bottom and you're ready to ship.


One-minute tour

  1. Sign in and open the Developer / API page to create an API key.
  2. Call POST /api/v1/jobs to upload files (or hand us file URLs) and get back a job id.
  3. Poll GET /api/v1/jobs/{jobId} every few seconds until status is completed — or pass a callbackUrl when you submit and we'll notify you when the job ends (see Completion webhooks).
  4. Call GET /api/v1/jobs/{jobId}/download to fetch the finished PPTX.

If you no longer need the result, call POST /api/v1/jobs/{jobId}/cancel to stop pages that have not started yet. To reconcile usage or find an earlier job, call GET /api/v1/jobs to list your jobs by date.

Conversion is asynchronous: submitting returns a job id immediately and the real work runs in the background. Don't block on the submit call waiting for the result.

Everything added in 1.3.0 is optional. Without callbackUrl, pages, the Idempotency-Key header, or URL submission, the API behaves exactly as it did in 1.2.0 — existing integrations need no changes.


Authentication

Get a key

Sign in to Image2PPT, open the Developer / API page from the account menu, and create a key under API Keys. You'll get a string like:

i2p_live_xxxxxxxxxxxxxxxxxxxxxxxx

The key is shown in full only once, at creation — save it right then. Afterward the page shows only the first few characters for identification. If a key leaks or you need to rotate, revoke the old one and create a new one on the same page.

Send it

Pass the key in the HTTP header on every request:

Authorization: Bearer i2p_live_xxxxxxxxxxxxxxxxxxxxxxxx

A missing or wrong key returns 401 (code INVALID_API_KEY).

Base URL

https://image2ppt.com

All paths below are appended to this base URL.


Conventions


Endpoints

1. Submit a job — POST /api/v1/jobs

Submit a batch of files and create a conversion job. The request body comes in two forms — pick one, never mix them:

Multipart fields

Field Required Description
files Yes One or more files. png / jpeg / webp / gif / pdf, each ≤ 35MB and ≤ 90MB total file content per request. Repeat the files field name to send multiple files.
locale No Output locale: zh-CN (default) or en.
aspectRatio No Slide ratio: auto (default, follows the source) / 16:9 / 4:3.
pages No Convert only some pages, e.g. 1-3,7 — see Page selection. Omit to convert everything.
callbackUrl No An https URL we notify when the job ends — see Completion webhooks.

Request headers

Header Required Description
Idempotency-Key No Makes retries safe after a network timeout — see Idempotent submission.

A submission has two independent limits, and it must satisfy both:

Limit Value Notes
Total pages ≤ 50 pages An image is 1 page; a PDF counts as its actual page count. With pages, the selected pages are what count — the PDF itself may be longer than 50 pages.
Total size ≤ 90MB The combined file content of one request. A single file is separately capped at 35MB.

They are independent: 40 high-resolution images are only 40 pages, yet easily exceed 90MB. Being under the page limit does not mean the request will be accepted.

Exceeding the size limit returns 413 PAYLOAD_TOO_LARGE. When you see it, send fewer files per request — retrying the same payload will not succeed.

The official SDKs offer two modes, and they behave differently. submit() / convert() send exactly the batch you hand them; they check size and page count locally before uploading, so an over-limit batch fails immediately instead of after a full upload — but they do not split for you. For automatic splitting use submit_all() / convert_all() (submitAll() / convertAll() in TypeScript), which divide the files into as many submissions as the size and page limits require, one job per submission.

Success — 201 Created

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

Replaying the same request with the same Idempotency-Key also returns 201 with the same body shape, plus the response header Idempotent-Replayed: true — see Idempotent submission.

curl example

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=en" \
  -F "aspectRatio=16:9" \
  -F "pages=1-3,7" \
  -F "callbackUrl=https://example.com/hooks/image2ppt"

Submit by URL

If your files already sit in object storage or on a CDN, skip the download-and-reupload round trip and hand us the URLs. Send a JSON body instead:

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": "en",
    "aspectRatio": "auto",
    "pages": "1-3,7",
    "callbackUrl": "https://example.com/hooks/image2ppt"
  }'
Field Required Description
urls Yes File URLs as an array of strings, 1–50 entries, merged into one PPTX in order.
locale / aspectRatio / pages / callbackUrl No Same meaning as the multipart fields.

Rules:

Whenever an error is caused by one particular URL — INVALID_URL, URL_FETCH_FAILED, and INVALID_FILE / PAYLOAD_TOO_LARGE raised by a downloaded file — error.index says which one (counting from 0). Other errors do not carry the field.

Page selection — pages

To convert just a few pages of a PDF, list them in pages instead of splitting the PDF yourself.

HTTP code When
400 INVALID_PAGES Syntax error.
400 PAGES_OUT_OF_RANGE A page number is 0 or beyond the total page count (message states the total).
400 TOO_MANY_SLIDES More than 50 pages selected.

Idempotent submission — Idempotency-Key

When a submit call times out, you can't tell whether it went through. Send an Idempotency-Key header and you can safely retry with the same key: the same request creates one job and is charged once.

Situation Result
Same key, same request, original job still exists 201 with the same body shape as the first time (status is the job's current status), response header Idempotent-Replayed: true, no further credits held.
Same key, different request 422 IDEMPOTENCY_KEY_MISMATCH.
Same key, previous request still being processed 409 IDEMPOTENCY_KEY_IN_PROGRESS with Retry-After: 2; wait and retry.
Previous request failed (any 4xx / 5xx) A failed request doesn't claim the key; retry with the same key.
Original job was deleted on the website The key is released; the request is treated as a new submission.

Possible errors

HTTP code Meaning
401 INVALID_API_KEY Key missing or invalid.
400 NO_FILES No files attached, or urls is empty.
400 INVALID_FILE Unsupported format, or a single file over 35MB.
400 INVALID_PDF The PDF can't be read or parsed.
400 TOO_MANY_SLIDES Total pages (or selected pages) over 50.
400 INVALID_PAGES pages has a syntax error.
400 PAGES_OUT_OF_RANGE A page number in pages is 0 or beyond the total page count.
400 INVALID_CALLBACK_URL callbackUrl isn't https, is too long, carries a username or password, or isn't a public address.
400 INVALID_IDEMPOTENCY_KEY Idempotency-Key isn't 1–255 characters, or contains anything outside 0x21–0x7E (spaces and non-ASCII included).
400 INVALID_JSON The JSON body isn't valid JSON, or isn't a JSON object.
400 INVALID_PARAMETER A JSON field has the wrong type, or there are more than 50 URLs.
400 INVALID_URL A URL is malformed, isn't https, carries a username or password, or points at a private network (including one only revealed by DNS resolution). Carries index.
400 URL_FETCH_FAILED A URL could not be downloaded: DNS failure, a non-2xx response, a timeout, more than 4 redirects, or a downgrade to http. Often transient — retrying is reasonable. Carries index.
400 UPLOAD_ABORTED The upload was cut off before the body finished arriving. Retrying is fine; if it keeps happening the submission is probably too big — split it by the size limit above.
400 MALFORMED_UPLOAD The body is not valid multipart/form-data. A client-side framing problem — retrying will not help; check the boundary and per-part headers.
402 INSUFFICIENT_CREDITS Not enough credits to cover this submission.
409 IDEMPOTENCY_KEY_IN_PROGRESS The previous request with this key is still being processed; wait for Retry-After and retry.
413 PAYLOAD_TOO_LARGE Total file content in one request exceeds 90MB.
422 IDEMPOTENCY_KEY_MISMATCH This key was already used for a request with different content.
429 RATE_LIMITED Rate limit hit — see Rate limits.

2. Get job status — GET /api/v1/jobs/{jobId}

Poll this endpoint for progress.

Success — 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
}

Fields

Field Description
status pending (queued) / processing / completed / failed.
progress Percent complete, 0–100.
slideCount Total pages.
creditsUsed Credits actually charged after settlement.
creditsRefunded Credits refunded for failed pages on partial success — see Billing & refunds.
cancellationRequested Whether the service accepted a cancellation request. The four-state status enum is unchanged for backward compatibility.
createdAt / completedAt UTC creation / completion time in YYYY-MM-DD HH:MM:SS format (completedAt is null until complete).
downloadUrl Given only when completed and the output is still retained — a relative path to the download endpoint; omitted otherwise.
error Given only when failed — {"code": "...", "message": "..."}.
pageResults Given only when completed or failed — the per-page outcome, see below.
callback Present only when the job was submitted with a callbackUrl — the webhook delivery state, see Completion webhooks.

A failed job looks like

{
  "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": "Conversion failed, please retry later" },
  "pageResults": [
    {
      "pageNumber": 1,
      "status": "failed",
      "error": {
        "code": "CONVERSION_FAILED",
        "message": "Conversion failed, please retry later",
        "retryable": true
      }
    },
    {
      "pageNumber": 2,
      "status": "failed",
      "error": {
        "code": "PAGE_NOT_ATTEMPTED",
        "message": "The job ended without a result for this page",
        "retryable": true
      }
    }
  ]
}

Per-page results — pageResults

Once a job is terminal (completed or failed), this array reports every page in page order; its length equals slideCount. It is omitted before that — while a job is still running, "this page didn't convert" and "this page hasn't had its turn yet" are not distinguishable.

(It is also omitted for early jobs that have no per-page record. Check whether the field is present rather than assuming every terminal job carries it.)

creditsRefunded only tells you how many pages did not convert. pageResults tells you which ones.

Field Description
pageNumber 1-based page number, matching the order you submitted (PDFs follow their split page order). With pages, it is the position in the deck — the k-th selected page.
status converted: the page became editable content. failed: it did not.
error Present only when status is failed. Carries code, message and retryable.

A failed page ends up one of two ways, told apart by error.code:

retryable says whether submitting the same image again could succeed. Every failed page this endpoint can return today is true — the three codes below are either a transient fault or a page left without a result because the job ended first, so resubmitting is worth doing in all of them. Still branch on the field rather than hardcoding true: a code added later may carry false.

Error codes

The job-level error.code (given when the whole job failed) has the same two values it has had since this endpoint shipped:

code Meaning
JOB_CANCELLED You cancelled or abandoned the job and nothing was deliverable.
CONVERSION_FAILED Every other failure reason.

Per-page pageResults[].error.code uses a finer set:

code Meaning retryable
CONVERSION_FAILED The page was attempted and did not succeed; the cause is not broken out. true
CONVERSION_TIMEOUT The page was cut off after exceeding its time budget. true
PAGE_NOT_ATTEMPTED The job ended first and the page has no result (it never got its turn, or was cut off). true

Why the two levels differ: the job-level field has carried only those two values since this endpoint shipped, and deployed client code branches on them — widening it would silently stop matching those branches. The finer reasons live in pageResults, which is new field surface with no such history.

message is a human-readable sentence that follows your Accept-Language header — do not branch on it, branch on code. It never carries diagnostic detail.

Either level may gain new codes later. Treat a code you do not recognise as CONVERSION_FAILED.

Possible errors

HTTP code Meaning
404 JOB_NOT_FOUND Job id doesn't exist, isn't owned by this key's account, or was deleted by the account owner on the website.

Note: job ids are visible only within your own account — nobody else can fetch or see your jobs.


3. Cancel a job — POST /api/v1/jobs/{jobId}/cancel

Ask the service to stop future work on this job. Cancellation is a graceful drain, not a hard kill:

Still winding down — 202 Accepted

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

finalizing: true means running pages or PPTX assembly are still winding down. Keep polling job status until it becomes completed or failed. If cancellation settles within this request, the response is 200 OK with finalizing: false.

Cancellation can end in two ways:

Possible errors

HTTP code Meaning
404 JOB_NOT_FOUND Job id does not exist, is not an API job, or belongs to another account.
409 JOB_ALREADY_FINISHED The job already finished, or is past the point where cancellation can change the outcome.
500 JOB_CANCEL_FAILED The service could not accept cancellation; retrying is safe.

4. Download the result — GET /api/v1/jobs/{jobId}/download

Once the job is complete, download the PPTX here.

Success — 200 OK, with the PPTX binary as the response body (Content-Type: application/vnd.openxmlformats-officedocument.presentationml.presentation).

Possible errors

HTTP code Meaning
409 NOT_READY Job isn't complete yet; the result isn't downloadable. Wait for completed.
410 OUTPUT_EXPIRED The result was cleaned up after its retention window — see Retention.
416 RANGE_NOT_SATISFIABLE The requested Range starts beyond the file size; discard the stale resume offset and retry.
404 JOB_NOT_FOUND Job id doesn't exist, isn't owned by this account, or was deleted on the website.

Retention: the finished PPTX is kept for 7 days after completion, then auto-deleted; downloads afterward return 410 OUTPUT_EXPIRED. Fetch it within the window. (The job record stays; only the output file is removed.)


5. Get account — GET /api/v1/account

Success — 200 OK

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

credits is your currently available balance (excluding credits held by in-flight jobs). API conversions and the web app share the same credit pool. Pro pass pages (quarterly / annual) can only be used on the web and in the plugin; they are not included here, and the API spends regular credits only. Accounts that can call this endpoint have API access, so their credits never expire with the free signup credits (those expire after 7 days only on accounts with no payment and no API access).


6. List jobs — GET /api/v1/jobs

Lists the jobs this account submitted through the API and has not deleted on the website, newest first. Use it to reconcile usage at month end, or to find a job whose id you lost.

Query parameters (all optional)

Parameter Description
createdFrom Start date, YYYY-MM-DD (UTC), inclusive.
createdTo End date, YYYY-MM-DD (UTC), inclusive.
limit Items per page, default 20, range 1–100.
cursor For paging: pass the nextCursor from the previous page. It is an opaque string — send it back unchanged, never build one yourself.

Success — 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"

This endpoint is not rate limited today, but handle 429 anyway: if you get one, wait for Retry-After and retry.

Possible errors

HTTP code Meaning
401 INVALID_API_KEY Key missing or invalid.
400 INVALID_PARAMETER limit, cursor, createdFrom, or createdTo is invalid; message names which one.

Completion webhooks

Pass a callbackUrl when you submit and we POST to it when the job ends — no polling needed. Webhooks follow the Standard Webhooks specification, so any library that verifies Standard Webhooks works as-is.

When it is sent

When the job reaches completed or failed. Cancelled jobs end in one of those two states as well; there is no separate cancelled status. If we re-run a job and it ends again, a new webhook is sent with a new webhook-id.

URL requirements

Checked at submission; a URL that fails returns 400 INVALID_CALLBACK_URL:

Request format

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=
Header Description
webhook-id Id of this delivery, shaped msg_…. It stays the same across retries of the same delivery — use it to deduplicate.
webhook-timestamp When this attempt was sent, in Unix seconds.
webhook-signature Signature, shaped v1,<base64>. During a secret rotation there are two, separated by a space.

Body:

{
  "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" }]
  }
}

Verifying the signature

Verify every request before trusting its content:

  1. Reject it if webhook-timestamp is more than 300 seconds away from now.
  2. Take the part of the secret after the whsec_ prefix and base64-decode it (standard alphabet) — that is the HMAC key.
  3. The signed content is <webhook-id>.<webhook-timestamp>.<raw body>; compute HMAC-SHA256 over it and base64-encode the result (standard alphabet).
  4. Split webhook-signature on spaces; the request is valid if any v1, value equals yours (compare in constant time).

Always compute over the raw bytes you received — never parse the JSON and re-serialise it. Using only the Python standard library:

import base64, hashlib, hmac, json, time

def verify_webhook(body: bytes, headers, secret: str) -> dict:
    # headers must be looked up case-insensitively (Flask, Django and FastAPI headers are)
    msg_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]
    if abs(time.time() - int(timestamp)) > 300:
        raise ValueError("webhook-timestamp is more than 5 minutes off")

    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("signature mismatch")

Managing the secret

Each account has one webhook secret, shaped whsec_<base64> (32 random bytes in standard base64 after the prefix). View, copy, and rotate it on the Developer / API page; it is created automatically the first time you open that page or first submit a job with a callbackUrl.

For 24 hours after a rotation the signature header carries both the new and the old signature, which gives you a day to switch to the new secret without missing a webhook.

Retries

Delivery status

For jobs submitted with a callbackUrl, GET /api/v1/jobs/{jobId} and each item of the job list carry an extra callback field:

"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"
}
Field Description
url The callback URL given at submission.
status pending: the job hasn't ended yet, or the next attempt is scheduled. delivered: delivered. failed: all 6 attempts failed; no more retries.
attempts Delivery attempts made so far.
lastAttemptAt Time of the most recent attempt.
lastResponseStatus HTTP status you returned on the most recent attempt; null for a network error or timeout.
nextAttemptAt Time of the next attempt.

Times are UTC in YYYY-MM-DD HH:MM:SS format, or null when there is no value. A webhook that ultimately fails doesn't affect the job itself — you can still fetch and download it as usual.


Rate limits

Limits are per account (all keys under one account share the quota):

Over the limit returns 429 (RATE_LIMITED) with a Retry-After response header giving the suggested wait in seconds.

The right way to handle it: read Retry-After, wait that many seconds, then retry — don't hammer immediately. The official Python client's wait() has this backoff built in. If you submit directly yourself, mirror this pseudocode:

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")))

If you retry after a rate limit, reuse the same Idempotency-Key: a refused request doesn't claim the key, and if an earlier attempt did go through you won't end up with a second job.

Polling job status is not rate limited. The job list isn't rate limited today either, but that may change — handle 429 the same way everywhere.


Versions & upgrade notices

The official SDKs (0.2.0 and later) identify themselves on every request:

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

The header is only used to understand which clients and versions are in use and to reach you if one of them has a problem — it plays no part in authentication or rate limiting and never changes the outcome of a request.

If you wrote your own client, you do not need to do anything about it. Every non-SDK caller is recorded simply as a custom client — no language, no version, and the string you send is not stored, so a custom name will not make your program easier for us to pick out. We would rather say that than promise visibility that does not exist.

One request only: please do not send image2ppt-python/... or image2ppt-node/.... Those identify the official SDKs, so borrowing them distorts the official-SDK share and makes us send you upgrade notices about a version you are not running.

If the official SDK you are running is older than the lowest version we still support, responses carry three standard headers (RFC 8594 / RFC 9745) — on successful responses too, not only on errors:

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

These headers are advisory only. The status code is unchanged, the request is processed as usual, and nothing is ever refused because of them. The official SDKs log a single warning when they see them (and let you switch it off). Actually retiring a version is a separate decision announced well in advance — never through this header alone.


Semantics

Async & latency expectations

Jobs run in the background after submission. A single page typically takes ~2 minutes; 90% of jobs finish within 3 minutes. Larger jobs take longer. Poll starting at 5s and back off toward ~15s — don't poll every second.

One job = one PPTX

All files in a single submission (multiple images / multi-page PDFs) are merged into one deck, paginated in upload order (only the selected pages, when you pass pages). For separate PPTX files, split into separate submissions.

Billing & refunds

In short: you only pay for pages that were successfully produced.


Official SDKs

We provide official Python and Node.js/TypeScript clients that wrap submission, polling, cancellation, download, 429 backoff, and error mapping. Source, examples, and the changelog live at https://github.com/image2ppt/image2ppt-sdk; install from the package pages: Python at https://pypi.org/project/image2ppt/, Node.js at https://www.npmjs.com/package/image2ppt.

Use the SDK server-side only. Never put an API key in a browser or anywhere a user can read it — anyone can extract it.

Python

pip install image2ppt
from image2ppt import Image2PPTClient, Image2PPTError, JobFailedError

client = Image2PPTClient(api_key="i2p_live_your_key")

try:
    # One shot: submit → poll → download
    job = client.convert(
        ["slide1.png", "slide2.png", "report.pdf"],
        dest_path="out.pptx",
        locale="en",
        aspect_ratio="16:9",
    )
    print("done — credits used:", job.credits_used, "refunded:", job.credits_refunded)
except JobFailedError as e:
    print("conversion failed:", e.code, e.message)
except Image2PPTError as e:
    print("request error:", e.status_code, e.code, e.message)

Node.js / TypeScript

Zero dependencies, needs Node 18+ (uses the built-in fetch).

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

const client = new Image2PPTClient({ apiKey: "i2p_live_your_key" });

try {
  const job = await client.convert(
    ["slide1.png", "slide2.png", "report.pdf"],
    "out.pptx",
    { locale: "en", aspectRatio: "16:9" },
  );
  console.log("done — credits used:", job.creditsUsed, "refunded:", job.creditsRefunded);
} catch (e) {
  if (e instanceof JobFailedError) console.error("conversion failed:", e.code, e.message);
  else if (e instanceof Image2PPTError) console.error("request error:", e.statusCode, e.code, e.message);
  else throw e;
}

Step-by-step control (submit / wait / download), account lookup (account), and full details on each exception are in the GitHub repo's README and examples.


Error code reference

message is written for people to read, and its language follows the request's Accept-Language and nothing else: ask for Chinese and you get Chinese, send no such header or ask for anything else and you get English. Browser cookies and UI-language headers do not affect it, so calling this API from a browser behaves the same as calling it from a script. Branch on code in your own code — it never changes with the language.

HTTP code When it happens
401 INVALID_API_KEY Key missing or invalid (all endpoints).
400 NO_FILES No files attached, or urls is empty (submit).
400 INVALID_FILE Unsupported format or a single file over 35MB (submit).
400 INVALID_PDF PDF can't be read or parsed (submit).
400 INVALID_ASPECT_RATIO Unrecognized aspect ratio; use auto, 16:9, or 4:3 (submit).
400 TOO_MANY_SLIDES Total pages, or selected pages, over 50 (submit).
400 INVALID_PAGES pages has a syntax error (submit).
400 PAGES_OUT_OF_RANGE A page number in pages is 0 or beyond the total page count; message states the total (submit).
400 INVALID_CALLBACK_URL callbackUrl isn't https, exceeds 2048 characters, carries a username or password, or isn't a public address (submit).
400 INVALID_IDEMPOTENCY_KEY Idempotency-Key isn't 1–255 printable ASCII characters (submit).
400 INVALID_JSON The body isn't valid JSON, or isn't a JSON object (submit by URL).
400 INVALID_PARAMETER A JSON field has the wrong type or there are more than 50 URLs (submit by URL); limit, cursor, createdFrom, or createdTo is invalid (list jobs).
400 INVALID_URL A URL is malformed, isn't https, carries a username or password, or points at a private network; carries index (submit by URL).
400 URL_FETCH_FAILED A URL could not be downloaded: DNS failure, non-2xx, timeout, more than 4 redirects, or a downgrade to http; carries index. Often transient — retrying is reasonable (submit by URL).
400 PAGE_RATE_EXCEEDED A single submission's page count exceeds the per-minute submission limit, so it can never fit the window (submit).
402 INSUFFICIENT_CREDITS Not enough available credits, or a zero balance (submit).
403 API_KEY_REQUIRED No valid API key present (submit).
403 ACCOUNT_DELETED Account has been deleted (submit).
400 UPLOAD_ABORTED The upload was cut off before the body finished arriving (submit).
400 MALFORMED_UPLOAD The body is not valid multipart/form-data (submit).
413 PAYLOAD_TOO_LARGE Total file content in one request exceeds 90MB (submit).
429 RATE_LIMITED Rate limit hit, with a Retry-After header (submit). Status polling is not rate limited; the job list isn't today either, but handle 429 there too.
404 JOB_NOT_FOUND Job id doesn't exist, isn't owned by this account, or was deleted on the website (status, cancel, download).
409 IDEMPOTENCY_KEY_IN_PROGRESS The previous request with the same Idempotency-Key is still being processed, with a Retry-After header (submit).
409 JOB_ALREADY_FINISHED The job already finished, or is past the point where cancellation can change the outcome (cancel).
409 NOT_READY Download requested before the job completed (download).
422 IDEMPOTENCY_KEY_MISMATCH The same Idempotency-Key was already used for a request with different content (submit).
— JOB_CANCELLED Cancellation settled with no deliverable pages (error.code in job status).
410 OUTPUT_EXPIRED Result cleaned up after its retention window (download).
416 RANGE_NOT_SATISFIABLE Resume range starts beyond the result file size (download).
5xx JOB_CANCEL_FAILED, STORAGE_FAILED, etc. Server-side error; retry later. If it persists, contact us.