image2ppt 0.5.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,6 +45,40 @@ const done = await client.wait(job.jobId, { pollIntervalMs: 5000, timeoutMs: 1_8
45
45
  await client.download(done.jobId, "out.pptx");
46
46
  ```
47
47
 
48
+ Show progress to your own users — `onProgress` is called after every status poll
49
+ (the last one included) with the fresh `Job` and the milliseconds elapsed since
50
+ `wait` started. `convert` and `convertAll` take it too.
51
+
52
+ ```ts
53
+ const done = await client.wait(job.jobId, {
54
+ onProgress: (job) => {
55
+ if (job.queueAhead != null) {
56
+ console.log(`queued, ${job.queueAhead} job(s) ahead`);
57
+ } else if (job.estimatedRemainingSeconds != null) {
58
+ console.log(`${job.progress}% — about ${job.estimatedRemainingSeconds}s left`);
59
+ } else {
60
+ console.log(`${job.progress ?? 0}% — estimating…`);
61
+ }
62
+ },
63
+ });
64
+ ```
65
+
66
+ While a job is running, each snapshot carries:
67
+
68
+ - `progress` — the whole job, 0–100. It moves within a page, so a one-page job
69
+ climbs steadily instead of jumping from 0 to 100.
70
+ - `pageProgress` — one `{ pageNumber, progress, stepLabel }` per page.
71
+ `stepLabel` is a short line about what that page is doing right now, in the
72
+ language of `acceptLanguage` (English by default), or `null`. Show it as-is and
73
+ never branch on it: its wording changes as the service changes.
74
+ - `estimatedRemainingSeconds` — the service's estimate of the time left. It can be
75
+ `null` while the job runs, when no estimate is available yet.
76
+ - `queueAhead` — jobs ahead of this one while it waits to start.
77
+
78
+ `pageProgress`, `estimatedRemainingSeconds` and `queueAhead` are `null` once
79
+ the job has ended. A returned promise is
80
+ awaited before the next poll, and anything the callback throws ends the wait.
81
+
48
82
  Cancel a job you no longer need:
49
83
 
50
84
  ```ts
@@ -72,10 +106,195 @@ subclass of `JobFailedError`). `JobAlreadyFinishedError` covers both a job that
72
106
  finished on its own and one already past the point where cancelling could change the
73
107
  outcome.
74
108
 
109
+ ## Task names — `name`
110
+
111
+ ```typescript
112
+ const uploaded = await client.submit(["report.pdf"], { name: "Quarterly report" });
113
+ const linked = await client.submitUrls(["https://example.com/report.pdf"], { name: "Quarterly report" });
114
+ const converted = await client.convert(["report.pdf"], "local.pptx", { name: "Quarterly report" });
115
+ console.log(converted.name); // resolved name, possibly "Quarterly report (2)"
116
+ ```
117
+
118
+ `name` is optional on submissions and conversion calls, including `submitAll()` / `convertAll()`.
119
+ Batch calls reuse it for each job; the service resolves each collision. It is the
120
+ task name and the service's download filename, without `.pptx`. After invisible
121
+ characters are removed and whitespace collapsed, it must be non-empty, at most
122
+ 60 Unicode characters and 180 UTF-8 bytes. Control characters and `/ \ : * ? " < > |`
123
+ are forbidden, as are leading/trailing dots and Windows reserved names. An invalid
124
+ name raises `InvalidNameError` with code `INVALID_NAME`. The service validates
125
+ and normalizes name contents; the SDK sends the supplied string unchanged.
126
+
127
+ Names already used by undeleted jobs in the same account get ` (2)`, ` (3)`, etc.
128
+ The resolved name is on `Job.name` in create responses, `getJob()` and job lists;
129
+ completion webhook payloads also include it. A rename on the website appears in
130
+ later snapshots. Older servers that omit it give `null`. Without `name`, one
131
+ input uses its cleaned filename without extension; multiple inputs or an unusable
132
+ filename use `Slides YYYY-MM-DD HHMMSS UTC` at creation time.
133
+
134
+ `name` participates in the `Idempotency-Key` fingerprint, compared after the
135
+ service's cleanup. Reusing a key with a name that cleans up differently raises
136
+ `IdempotencyKeyMismatchError`, just like changing other submission options; a name
137
+ that differs only in what cleanup removes (extra spaces, invisible characters)
138
+ replays the original job.
139
+
140
+ `download()` writes to the caller's path and never uses the server name, so a task
141
+ name cannot cause local filename collisions or overwrite other files. Existing
142
+ files at your explicitly chosen path can still be replaced. `convertAll()` keeps
143
+ `part-01.pptx`, `part-02.pptx`, ... naming regardless of task names.
144
+
145
+ ## Convert only some pages — `pages`
146
+
147
+ ```ts
148
+ const job = await client.submit(["report.pdf"], { pages: "1-3, 7" });
149
+ ```
150
+
151
+ Single pages and `start-end` ranges, separated by commas, counting from 1. Page numbers
152
+ run across the **whole submission in order** — an image is one page, a PDF is its page
153
+ count — so with a single PDF they are simply the PDF's own page numbers. Overlaps are
154
+ merged, and the deck keeps the pages in ascending order.
155
+
156
+ Only the selected pages are charged, and only they count towards the 50-page limit: a
157
+ 200-page PDF with `pages: "1-50"` is fine. The spelling is checked locally before
158
+ anything is uploaded (`InvalidPagesError`); a page past the end comes back
159
+ `PagesOutOfRangeError`.
160
+
161
+ With a selection, `pageResults[i].pageNumber` is the page's place **in the deck**, not
162
+ the number you selected: `pages: "3,7"` gives entries 1 and 2.
163
+
164
+ ## Submit by URL — `submitUrls`
165
+
166
+ Instead of uploading, hand the service `https` links and it downloads them itself:
167
+
168
+ ```ts
169
+ const job = await client.submitUrls(
170
+ ["https://example.com/slides/1.png", "https://example.com/report.pdf"],
171
+ { pages: "1-4" },
172
+ );
173
+ ```
174
+
175
+ Up to 50 links, converted in order into one deck, held to the same limits as uploads
176
+ (35MB per file, 90MB together, 50 pages). File types are judged by content. Links must
177
+ be `https` and resolve to public addresses; a link that is refused throws
178
+ `InvalidUrlError`, a download that fails throws `UrlFetchFailedError` (worth retrying
179
+ later — the other side may have been down), and for both `e.index` says which link,
180
+ counting from 0.
181
+
182
+ Downloading takes time, so `submitUrls` waits up to 180 seconds for an answer whatever
183
+ the client's `timeoutMs`. One submission by URL per account can be in flight at a time;
184
+ another gets `RateLimitedError`.
185
+
186
+ ## Get told when a job ends — `callbackUrl` and `verifyWebhook`
187
+
188
+ Pass `callbackUrl` to any submit call and the service `POST`s to it when the job ends,
189
+ instead of you polling:
190
+
191
+ ```ts
192
+ const job = await client.submit(["slide1.png"], {
193
+ callbackUrl: "https://example.com/hooks/image2ppt",
194
+ });
195
+ ```
196
+
197
+ Every delivery is signed ([Standard Webhooks](https://www.standardwebhooks.com)). Check
198
+ it with the **raw** request body and the signing secret from the Developer / API page:
199
+
200
+ ```ts
201
+ import express from "express";
202
+ import { verifyWebhook, WebhookVerificationError } from "image2ppt";
203
+
204
+ // express.raw(), not express.json(): the signature is over the exact bytes received.
205
+ app.post("/hooks/image2ppt", express.raw({ type: "application/json" }), (req, res) => {
206
+ let event;
207
+ try {
208
+ event = verifyWebhook(req.body, req.headers, process.env.IMAGE2PPT_WEBHOOK_SECRET!);
209
+ } catch (e) {
210
+ if (e instanceof WebhookVerificationError) return res.sendStatus(400);
211
+ throw e;
212
+ }
213
+ if (event.type === "job.completed") {
214
+ const job = event.job; // same shape as getJob()
215
+ // ...
216
+ } else if (event.type === "job.failed") {
217
+ // ...
218
+ }
219
+ // Ignore types you don't recognise — more may be added. Answer 2xx either way.
220
+ res.sendStatus(204);
221
+ });
222
+ ```
223
+
224
+ - **Pass the body exactly as received** — a `Buffer`, `Uint8Array` or string. Parsing
225
+ the JSON and serialising it again changes the bytes, and the signature will not match.
226
+ `headers` can be Node's `req.headers` or a fetch `Headers`.
227
+ - **Answer 2xx within 10 seconds.** Anything else counts as a failure, and the service
228
+ retries: 6 attempts over about 8.5 hours (right away, then +1 min, +5 min, +30 min,
229
+ +2 h, +6 h). A retry of one delivery keeps its `event.id`, so use that to skip
230
+ duplicates.
231
+ - **Timestamps more than 5 minutes off are refused**, either way, as protection against
232
+ replays (`toleranceSeconds` to change it).
233
+ - A malformed secret, or a body your framework already parsed into an object, throws a
234
+ `TypeError` — those are mistakes in your receiver, so they are kept apart from a bad
235
+ delivery.
236
+ - Rotating the secret is safe: for 24 hours the service signs with both the old and the
237
+ new one, and either verifies.
238
+
239
+ `getJob()` reports how delivery is going in `job.callback` — `status` is `pending`,
240
+ `delivered` or `failed` (all 6 attempts failed), with `attempts`, `lastResponseStatus`
241
+ and the times of the last and next attempt.
242
+
243
+ ## Resubmitting safely — `idempotencyKey`
244
+
245
+ Every submission carries an `Idempotency-Key`. If the same key comes back with the same
246
+ request within 24 hours, the service returns the job it already created —
247
+ `job.replayed` is `true` — instead of creating and charging for a second one.
248
+
249
+ That is what lets this client **resend a submission whose outcome is unknown** — a
250
+ dropped connection, a timeout, a 5xx — without risking a double charge. It does so by
251
+ itself, always under the same key (see *How it works*). By default the key is a random
252
+ UUID per call. Pass your own to extend the protection across calls, processes or
253
+ restarts:
254
+
255
+ ```ts
256
+ const job = await client.submit(paths, { idempotencyKey: `order-${order.id}` });
257
+ ```
258
+
259
+ If a submission still fails, the key it used is on the error:
260
+
261
+ ```ts
262
+ import { APIConnectionError, ServerError } from "image2ppt";
263
+
264
+ try {
265
+ job = await client.submit(paths);
266
+ } catch (e) {
267
+ if (!(e instanceof APIConnectionError || e instanceof ServerError)) throw e;
268
+ // Later, or from another process: safe for 24 hours, because the same files,
269
+ // options and key can never create a second job.
270
+ job = await client.submit(paths, { idempotencyKey: e.idempotencyKey });
271
+ }
272
+ ```
273
+
274
+ "The same request" means the same file bytes in the same order (or the same `urls`), and
275
+ the same `locale`, `aspectRatio`, `pages` and `callbackUrl`. Reusing a key for a
276
+ different request throws `IdempotencyKeyMismatchError`. Keys are 1–255 printable ASCII
277
+ characters, shared by all API keys of the account.
278
+
279
+ ## List your jobs — `listJobs` / `iterJobs`
280
+
281
+ ```ts
282
+ for await (const job of client.iterJobs({ createdFrom: "2026-10-01", createdTo: "2026-10-31" })) {
283
+ console.log(job.jobId, job.status, job.creditsUsed, job.creditsRefunded);
284
+ }
285
+ ```
286
+
287
+ Jobs submitted through the API, newest first; jobs deleted on the website are not
288
+ listed. Dates are `YYYY-MM-DD` in UTC and both inclusive. `iterJobs` follows the pages
289
+ for you; `listJobs` returns one page (`page.data`, `page.nextCursor`) if you want to page
290
+ yourself — pass `nextCursor` back as `cursor`, `limit` is 1–100 (default 20). Each job
291
+ has `getJob()`'s fields except `pageResults`. For reconciliation, add up `creditsUsed`
292
+ (charged) and `creditsRefunded`.
293
+
75
294
  ## Which pages made it
76
295
 
77
296
  Once a job is terminal, `job.pageResults` says what happened to each page, in page
78
- order, one entry per page:
297
+ order, one entry per page of the deck (with `pages`, per *selected* page):
79
298
 
80
299
  ```ts
81
300
  for (const page of done.pageResults ?? []) {
@@ -139,12 +358,12 @@ console.log(email, "credits:", credits);
139
358
  - **Async.** `submit` resolves with a job id immediately; conversion runs in the background. A single page typically takes ~2 minutes; 90% of jobs finish within 3.
140
359
  - **One job = one PPTX.** All files in a submission are merged into a single deck, in upload order.
141
360
  - **Billed per page.** 1 page = 1 credit, reserved at submit and settled on completion. If some pages fail but others succeed, the job still completes with the good pages and the failed pages' credits are refunded (`creditsRefunded`).
142
- - **Limits.** Each file ≤ 35MB; **the files in one request ≤ 45MB in total**; ≤ 50 pages per job (images count as 1, PDFs as their page count). All three are checked locally before upload — note the per-file limit is the *stricter* one, so a 40MB PDF is refused even though it fits a request. **The sizes counted are the ones that actually go on the wire**: image preparation happens first, then the final payload sizes drive pre-flight and batching. PDFs keep their on-disk size.
143
- - **The check is never stricter than the documented limit.** 45MB of file content is meant to be usable, so a submission sitting exactly on it goes through. Auto-batching is the one place that is deliberately conservative — it fills a batch only to 40MB, because starting one more batch costs nothing while refusing something the server would have accepted does not.
361
+ - **Limits.** Each file ≤ 35MB; **the files in one request ≤ 90MB in total**; ≤ 50 pages per job (images count as 1, PDFs as their page count — or, with `pages`, the pages selected). All three are checked locally before upload — note the per-file limit is the *stricter* one, so a 40MB PDF is refused even though it fits a request. **The sizes counted are the ones that actually go on the wire**: image preparation happens first, then the final payload sizes drive pre-flight and batching. PDFs keep their on-disk size.
362
+ - **The check is never stricter than the documented limit.** 90MB of file content is meant to be usable, so a submission sitting exactly on it goes through. Auto-batching is the one place that is deliberately conservative — it fills a batch only to 40MB, because starting one more batch costs nothing while refusing something the server would have accepted does not.
144
363
  - **Only the formats the API accepts.** `png`, `jpg`/`jpeg`, `webp`, `gif`, `pdf`. Anything else throws `InvalidFileError` locally — the batch calls check every file before submitting the first one, so an unsupported file at the end of the pile cannot leave you paying for the batches ahead of it.
145
- - **The local page check is a lower bound.** The client does not parse PDFs, so it counts each one as *at least* 1 page. That is enough to refuse combinations that can never work (50 images plus any PDF is already 51 pages), but a submission that passes locally can still come back `TOO_MANY_SLIDES` — a 30-page PDF counts as 1 here and 30 on the server.
364
+ - **The local page check is a lower bound.** The client does not parse PDFs, so it counts each one as *at least* 1 page. That is enough to refuse combinations that can never work (50 images plus any PDF is already 51 pages), but a submission that passes locally can still come back `TOO_MANY_SLIDES` — a 30-page PDF counts as 1 here and 30 on the server. With `pages`, the check counts the pages selected instead, and a PDF longer than 50 pages is fine.
146
365
  - **Going over the request limit is not a polite error.** Past that the connection is cut before the API can answer, so the caller sees a write timeout or a broken pipe instead of a status code. The client therefore checks locally *before* uploading and throws `InvalidFileError` (`code: "PAYLOAD_TOO_LARGE"`) without sending a byte.
147
- - **A failed submission is never retried automatically.** A network error only tells you the exchange broke — not whether the request body arrived. The job may not exist, or it may exist with credits already reserved and only the response lost. Retrying the second case charges you twice, and there is no idempotency key to tell them apart, so the error is thrown as-is. Check `account()` or your job list before resending. (Rate limits *are* retried by `submitAll()` / `convertAll()`: a 429 is the server saying it did not take the submission.)
366
+ - **A submission whose outcome is unknown is resent, under the same `Idempotency-Key`.** A network error only tells you the exchange broke — not whether the request body arrived; the job may exist with credits already reserved and only the response lost. Because every attempt carries the same key, a resend of that case gets the existing job back rather than a second charge. So `submit()` resends after a dropped connection, an idle timeout or a 5xx (up to 2 more attempts, 1s and 2s apart), and waits out `IdempotencyKeyInProgressError` — an earlier attempt the service is still working on — for up to 10 attempts or 3 minutes. Any other error is thrown straight away. Whatever finally escapes carries `e.idempotencyKey`; see *Resubmitting safely*.
148
367
  - **Downloads are all-or-nothing.** `download()` writes to a temporary file next to the destination and renames it into place at the end, so a dropped connection cannot leave a truncated `.pptx` behind — or destroy a good deck already sitting at that path.
149
368
  - **Every request identifies the client** with a `User-Agent` of `image2ppt-node/<version>`. The service uses this to tell SDK versions apart — it is not part of authentication and never changes a request's outcome.
150
369
  - **A deprecated SDK version logs one warning.** If this version is below the lowest the service still supports, the response carries a `Deprecation` header and the client warns once (`console.warn`). Pass `warnOnDeprecated: false` to `Image2PPTClient` to silence it.
@@ -152,7 +371,7 @@ console.log(email, "credits:", credits);
152
371
  - **The 60-second request timeout is idle time, not total time.** `timeoutMs` (default 60000) is how long one request may go with **no data moving in either direction** — it is not a cap on how long a request may take. A 40MB upload or a large PPTX download that keeps making progress runs as long as it needs to; only a transfer that actually stalls is given up on, as `APITimeoutError`. A request that never gets a response at all is covered by the same clock. This matches the Python client's read timeout, so the two SDKs behave the same way on a slow link.
153
372
  - **Every failure of the request is an `Image2PPTError`.** A refused connection, a reset mid-download, a 2xx that comes back as a proxy's HTML login page, a job body missing its own id — all of them arrive as an SDK error with the original kept on `.cause`, never as a raw `TypeError: fetch failed` or `SyntaxError`. **Your own filesystem is the exception, deliberately:** if `download` cannot write where you asked it to, you get the operating system's error — `ENOSPC`, `EACCES`, `ENOENT` — because that names the thing you have to go and fix, and no error of ours would say it better. The Python client draws the same line.
154
373
 
155
- > Both the Node and Python SDKs pre-compress images that need processing before upload. PNG/JPEG files already at most 1MiB with a longest edge at most 2000px upload byte-for-byte unchanged. Other PNG/JPEG files, and all WebP/GIF files, may be resized, flattened onto white, and sent as JPEG; PDFs are never compressed or decoded and are streamed unchanged.
374
+ > Both the Node and Python SDKs pre-compress images that need processing before upload. PNG/JPEG files already at most 2MiB with a longest edge at most 2000px upload byte-for-byte unchanged. Other PNG/JPEG files, and all WebP/GIF files, may be resized, flattened onto white, and sent as full-colour (4:4:4) JPEG; PDFs are never compressed or decoded and are streamed unchanged.
156
375
 
157
376
  ## More files than one request can hold
158
377
 
@@ -163,7 +382,9 @@ const files = await client.convertAll(imagePaths, "decks/");
163
382
  console.log(files); // ['decks/part-01.pptx', 'decks/part-02.pptx']
164
383
  ```
165
384
 
166
- Batches hold at most 40MB of file content and at most 50 images; every PDF goes in a batch of its own, because the client does not parse PDFs and only the server knows their page count. `submitAll()` does the same splitting and hands back the jobs if you want to drive polling yourself. To see the plan without uploading anything, use `planBatches()`.
385
+ Batches hold at most 80MB of file content and at most 50 images; every PDF goes in a batch of its own, because the client does not parse PDFs and only the server knows their page count. `submitAll()` does the same splitting and hands back the jobs if you want to drive polling yourself. To see the plan without uploading anything, use `planBatches()`.
386
+
387
+ Both take `callbackUrl` — each batch's job calls it. They do not take `pages` or `idempotencyKey` (passing either throws a `TypeError`): page numbers run across one submission and a key names one job, so neither can span several batches. Each batch gets its own key, kept across that batch's retries.
167
388
 
168
389
  **Rate limits are waited out, not thrown.** A pile big enough to need batching will hit the account's per-minute page quota (and its cap on concurrently active jobs). Both arrive as a `429` with a `Retry-After`; both are handled the same way — sleep that long, retry the same batch. Retrying a 429 is free: the server is saying it did *not* take the submission, so nothing was created and nothing was charged. Total waiting is capped by `rateLimitMaxWaitMs` (default 30 min) — and **only waiting counts against it**, not the time the uploads themselves take, so a slow link cannot quietly turn the cap into "do not wait at all". A single batch is also retried at most 10 times, whatever the budget says: every retry re-uploads the whole batch, and a service still refusing after ten tries will not be talked round by more of them.
169
390
 
@@ -187,14 +408,15 @@ try {
187
408
 
188
409
  Per account (all keys share the budget): ≤ 10 concurrent jobs, ≤ 60 pages/minute submitted. Over the limit returns `429` with a `Retry-After` hint. **Only submissions are rate limited — polling job status is not.**
189
410
 
190
- `submitAll()` / `convertAll()` wait these out for you: a pile big enough to need batching is a pile big enough to hit the quota, so a 429 mid-pile is the normal path, not an error. `submit()` and `convert()` do not — they submit exactly once, so catch `RateLimitedError` and honor `retryAfter` (seconds) yourself:
411
+ `submitAll()` / `convertAll()` wait these out for you: a pile big enough to need batching is a pile big enough to hit the quota, so a 429 mid-pile is the normal path, not an error. `submit()`, `submitUrls()` and `convert()` do not, so catch `RateLimitedError` and honor `retryAfter` (seconds) yourself — resending with the same key keeps it safe:
191
412
 
192
413
  ```ts
193
414
  import { RateLimitedError } from "image2ppt";
194
415
 
416
+ const idempotencyKey = "order-42";
195
417
  for (;;) {
196
418
  try {
197
- job = await client.submit(paths);
419
+ job = await client.submit(paths, { idempotencyKey });
198
420
  break;
199
421
  } catch (e) {
200
422
  if (e instanceof RateLimitedError) {
@@ -206,11 +428,11 @@ for (;;) {
206
428
 
207
429
  ## Errors
208
430
 
209
- Every error this client raises about a *request* subclasses `Image2PPTError` and carries `statusCode`, `code`, and `message`. Branch on `code`, not `message`. The one thing that reaches you unwrapped is a failure of your own disk while `download` is writing — see the note above.
431
+ Every error this client raises about a *request* subclasses `Image2PPTError` and carries `statusCode`, `code`, and `message` — plus `index` (which of `urls` it is about, else `undefined`) and, out of a submit call, `idempotencyKey`. Branch on `code`, not `message`. The one thing that reaches you unwrapped is a failure of your own disk while `download` is writing — see the note above.
210
432
 
211
- Every `Image2PPTError` also carries **`isTransient`** — whether **repeating this exact read** later could plausibly succeed. (A raw filesystem error from `download` has no such field; that is the same exception as above, and it is never transient — free the space or fix the permission first.) It is what `wait()` uses to decide whether a failed status poll should be backed off and retried or should end the wait: `true` for a 5xx, a dropped connection and a stalled request, `false` for a bad key, a job that does not exist, or a response this client cannot parse.
433
+ Every `Image2PPTError` also carries **`isTransient`** — whether **repeating this exact read** later could plausibly succeed. (A raw filesystem error from `download` has no such field; that is the same exception as above, and it is never transient — free the space or fix the permission first.) It is what `wait()` uses to decide whether a failed status poll should be backed off and retried or should end the wait: `true` for a 5xx, a rate limit, `IdempotencyKeyInProgressError`, `UrlFetchFailedError`, a dropped connection and a stalled request, `false` for a bad key, a job that does not exist, or a response this client cannot parse.
212
434
 
213
- **It says nothing about submitting.** `submit()` is never retried on this signal, and neither should your code be: a lost response cannot be told apart from a submission the server accepted, and there is no idempotency key, so resending the same files can create the same job twice and **charge you twice**. Only a `RateLimitedError` is retried on the submit path — a 429 is the service explicitly saying it took nothing.
435
+ **Resubmitting is only safe with the same key.** A lost response cannot be told apart from a submission the server accepted, so resending under a *new* key can create the same job twice and **charge you twice**. Resend with `e.idempotencyKey`, as `submit()` itself does.
214
436
 
215
437
  | Class | HTTP | code |
216
438
  |---|---|---|
@@ -220,7 +442,17 @@ Every `Image2PPTError` also carries **`isTransient`** — whether **repeating th
220
442
  | `MalformedUploadError` | 400 | `MALFORMED_UPLOAD` — the body was not valid `multipart/form-data`; **resending identical bytes will not help** |
221
443
  | `NoFilesError` | 400 | `NO_FILES` — no files reached the server |
222
444
  | `InvalidAspectRatioError` | 400 | `INVALID_ASPECT_RATIO` — use `auto`, `16:9`, or `4:3` |
223
- | `TooManySlidesError` | 400 | `TOO_MANY_SLIDES` |
445
+ | `TooManySlidesError` | 400 | `TOO_MANY_SLIDES` — also for more than 50 pages selected |
446
+ | `InvalidPagesError` | 400 | `INVALID_PAGES` — `pages` is misspelled (also checked locally) |
447
+ | `PagesOutOfRangeError` | 400 | `PAGES_OUT_OF_RANGE` — page 0, or past the end; subclasses `InvalidPagesError` |
448
+ | `InvalidCallbackUrlError` | 400 | `INVALID_CALLBACK_URL` — not `https`, has credentials, over 2048 characters, or not a public address |
449
+ | `InvalidIdempotencyKeyError` | 400 | `INVALID_IDEMPOTENCY_KEY` — not 1–255 printable ASCII characters (also checked locally) |
450
+ | `IdempotencyKeyMismatchError` | 422 | `IDEMPOTENCY_KEY_MISMATCH` — the key was used for a different request |
451
+ | `IdempotencyKeyInProgressError` | 409 | `IDEMPOTENCY_KEY_IN_PROGRESS` (has `retryAfter`) — an earlier request with this key is still being processed |
452
+ | `InvalidUrlError` | 400 | `INVALID_URL` — a link was refused before downloading (`e.index` says which) |
453
+ | `UrlFetchFailedError` | 400 | `URL_FETCH_FAILED` — downloading a link failed; may be temporary (`e.index` says which) |
454
+ | `InvalidNameError` | 400 | `INVALID_NAME` — invalid task name |
455
+ | `InvalidParameterError` | 400 | `INVALID_PARAMETER`, `INVALID_JSON` — e.g. more than 50 `urls`, or a bad `listJobs` argument |
224
456
  | `PageRateExceededError` | 400 | `PAGE_RATE_EXCEEDED` — this one submission has more pages than a minute's quota, so waiting will not help; split it |
225
457
  | `InsufficientCreditsError` | 402 | `INSUFFICIENT_CREDITS` |
226
458
  | `RateLimitedError` | 429 | `RATE_LIMITED` (has `retryAfter`) |
@@ -235,6 +467,7 @@ Every `Image2PPTError` also carries **`isTransient`** — whether **repeating th
235
467
  | `APITimeoutError` | — | `REQUEST_TIMEOUT` — one request went `timeoutMs` with **no data moving**; subclasses `APIConnectionError` |
236
468
  | `MalformedResponseError` | — | — (the server answered with something this client cannot read: a 2xx that is not JSON, or a body missing a field the contract guarantees) |
237
469
  | `Image2PPTTimeoutError` | — | — (`wait()` exceeded its own `timeoutMs`; job may still be running — **not** a transport failure, and not the same as `APITimeoutError`) |
470
+ | `WebhookVerificationError` | — | — (`verifyWebhook` refused a delivery: missing header, stale timestamp, or no matching signature) |
238
471
 
239
472
  > **5xx now lands on `ServerError` rather than the base class.** It still subclasses `Image2PPTError`, so `catch (e) { if (e instanceof Image2PPTError) }` is unaffected — only code matching on the exact class or on `e.name` needs updating.
240
473
 
package/dist/client.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** The image2ppt API client. */
2
- import type { Account, CancellationResult, ClientOptions, ConvertOptions, SubmitOptions, WaitOptions } from "./types.js";
3
- import { Job } from "./types.js";
2
+ import type { Account, CancellationResult, ClientOptions, ConvertAllOptions, ConvertOptions, ListJobsOptions, SubmitAllOptions, SubmitOptions, WaitOptions } from "./types.js";
3
+ import { Job, JobList } from "./types.js";
4
4
  export declare const DEFAULT_BASE_URL = "https://image2ppt.com";
5
5
  /**
6
6
  * Client for the image2ppt API. Server-side only — keep your key off the browser.
@@ -28,30 +28,67 @@ export declare class Image2PPTClient {
28
28
  * Submit a batch of files and create a conversion job.
29
29
  *
30
30
  * Checked locally before anything is uploaded: the files must add up to at most
31
- * 45MB and at most 50 pages. Over either limit this throws without opening a
31
+ * 90MB and at most 50 pages (the pages *selected*, when `pages` is given), and
32
+ * `pages` must be spelled correctly. Over a limit this throws without opening a
32
33
  * connection — going over the size cap on the wire does not come back as a clean
33
34
  * error, it comes back as a dead connection.
34
35
  *
35
- * **A failed submission is never retried automatically.** A network error does
36
- * not tell you whether the request body made it: the job may not exist, or it
37
- * may exist with credits already reserved and only the response lost. Retrying
38
- * the second case charges twice, and without an idempotency key there is no way
39
- * to tell them apart — so the error is thrown as-is. Check `account()` or your
40
- * job list before resending.
36
+ * **Every submission carries an `Idempotency-Key`**, and that is what makes a
37
+ * failed one safe to resend: if an earlier attempt did create the job, the service
38
+ * hands that job back (`job.replayed` is true) instead of creating and charging for
39
+ * a second one. So this call resends by itself, always with the same key, when the
40
+ * outcome is unknown — a dropped connection, an idle timeout, or a 5xx (up to 2
41
+ * more attempts, 1s and 2s apart) — and waits out `IDEMPOTENCY_KEY_IN_PROGRESS`, an
42
+ * earlier attempt the service is still working on (up to 10 attempts / 3 minutes).
43
+ * A 429 is not retried here; `submitAll` waits those out.
44
+ *
45
+ * If it still fails, the key is on the error as `err.idempotencyKey`. Calling
46
+ * `submit` again with that key and the same files and options is safe for 24
47
+ * hours; calling it without the key may create a second job.
41
48
  *
42
49
  * @param paths Local file paths (one or more). png/jpeg/webp/gif/pdf, each ≤ 35MB,
43
- * and ≤ 45MB of file content per request. An image is 1 page, a PDF is its page
50
+ * and ≤ 90MB of file content per request. An image is 1 page, a PDF is its page
44
51
  * count; the total must be ≤ 50 pages. For more files than one request can hold,
45
52
  * use `submitAll` / `convertAll`.
46
- * @returns A `Job` with status `pending`, plus `slideCount` and `creditsReserved`.
53
+ * @returns A `Job` with status `pending` (for a replay: the job's current status),
54
+ * plus `slideCount` and `creditsReserved`.
47
55
  * @throws InvalidFileError Including the local `PAYLOAD_TOO_LARGE` pre-flight failure.
56
+ * @throws InvalidPagesError / PagesOutOfRangeError / TooManySlidesError A selection
57
+ * that is certain to be refused, before anything is sent.
58
+ * @throws IdempotencyKeyMismatchError The key was used for a different request.
59
+ * @throws APIConnectionError / ServerError / IdempotencyKeyInProgressError Once the
60
+ * retries above run out.
48
61
  */
49
62
  submit(paths: string[], options?: SubmitOptions): Promise<Job>;
63
+ /**
64
+ * Create a conversion job from files the service downloads itself.
65
+ *
66
+ * Instead of uploading, hand over `https` links; the service fetches them in order
67
+ * and converts them as one deck. The downloaded files are held to the same limits
68
+ * as uploads (35MB each, 90MB together, 50 pages). File types are judged by
69
+ * content, not by the link or its `Content-Type`.
70
+ *
71
+ * Downloading takes time — up to 60 seconds a link and 120 for the request — so
72
+ * this call waits at least 180 seconds for an answer, whatever the client's
73
+ * `timeoutMs`. Retries and `idempotencyKey` work exactly as in `submit`. Only one
74
+ * submission by URL per account can be in flight at a time; another gets a
75
+ * `RateLimitedError`.
76
+ *
77
+ * @param urls 1–50 `https` links. Each must resolve to a public address.
78
+ * @param options As `submit`. With links the page total is not known here, so
79
+ * `pages` is only checked for spelling and for selecting more than 50 pages.
80
+ * @throws InvalidUrlError A link was refused before downloading.
81
+ * @throws UrlFetchFailedError A download failed; may be temporary. For these two —
82
+ * and `InvalidFileError` about a downloaded file — `err.index` says which link,
83
+ * counting from 0.
84
+ * @throws InvalidParameterError More than 50 links, thrown locally.
85
+ */
86
+ submitUrls(urls: string[], options?: SubmitOptions): Promise<Job>;
50
87
  /**
51
88
  * Split files into submittable batches and create **one job per batch**.
52
89
  *
53
90
  * For a pile of files too big or too numerous for a single request. Batching
54
- * rules live in `planBatches`: at most 40MB of file content and at most 50
91
+ * rules live in `planBatches`: at most 80MB of file content and at most 50
55
92
  * images per batch, and every PDF in a batch of its own (the SDK does not parse
56
93
  * PDFs, so only the server knows their page count). Input order is preserved.
57
94
  *
@@ -66,8 +103,11 @@ export declare class Image2PPTClient {
66
103
  * the same batch again. Waiting is the normal path here. Total waiting is capped
67
104
  * by the client's `rateLimitMaxWaitMs`.
68
105
  *
69
- * **Network errors are not retried** — see `submit`. Only a 429 is, because only
70
- * a 429 proves the server did not take the submission.
106
+ * **Each batch is its own submission with its own `Idempotency-Key`**, and is
107
+ * retried exactly as `submit` retries — plus the 429s above, with the same key.
108
+ * There is no `pages` or `idempotencyKey` option here: page numbers run across one
109
+ * submission and a key names one job, so neither can span several batches. Submit
110
+ * batches yourself with `submit` if you need them.
71
111
  *
72
112
  * **If it does give up, the jobs already created are handed back on the error**,
73
113
  * in `err.submittedJobs`. Those jobs are running on the server with credits
@@ -79,7 +119,7 @@ export declare class Image2PPTClient {
79
119
  * batching can carry it.
80
120
  * @throws RateLimitedError Still rate limited after `rateLimitMaxWaitMs`.
81
121
  */
82
- submitAll(paths: string[], options?: SubmitOptions): Promise<Job[]>;
122
+ submitAll(paths: string[], options?: SubmitAllOptions): Promise<Job[]>;
83
123
  /** Fetch the current job state as a `Job` snapshot. Throws JobNotFoundError. */
84
124
  getJob(jobId: string): Promise<Job>;
85
125
  /**
@@ -118,7 +158,7 @@ export declare class Image2PPTClient {
118
158
  /**
119
159
  * One-shot: submit → wait for completion → download to `destPath`.
120
160
  *
121
- * One job, one PPTX — the files must fit in a single submission (45MB of file
161
+ * One job, one PPTX — the files must fit in a single submission (90MB of file
122
162
  * content, 50 pages). For more than that, `convertAll` splits the pile and writes
123
163
  * one PPTX per batch.
124
164
  */
@@ -147,7 +187,30 @@ export declare class Image2PPTClient {
147
187
  * before anything is submitted**, so an unusable destination costs nothing.
148
188
  * @returns The written file paths, in batch order.
149
189
  */
150
- convertAll(paths: string[], destDir: string, options?: ConvertOptions): Promise<string[]>;
190
+ convertAll(paths: string[], destDir: string, options?: ConvertAllOptions): Promise<string[]>;
191
+ /**
192
+ * One page of the jobs this account submitted through the API, newest first.
193
+ *
194
+ * Jobs deleted on the website are not listed. Each job has `getJob`'s shape minus
195
+ * `pageResults`; for reconciliation, add up `creditsUsed` and `creditsRefunded`.
196
+ * To walk every page, use `iterJobs`.
197
+ *
198
+ * @throws InvalidParameterError A parameter is invalid; `message` says which.
199
+ */
200
+ listJobs(options?: ListJobsOptions): Promise<JobList>;
201
+ /**
202
+ * Every job `listJobs` would list, following `nextCursor` to the end.
203
+ *
204
+ * `limit` is the page size, not a cap on how many jobs come back. A 429 is waited
205
+ * out (`Retry-After`, else 5s), and a page that fails in a way worth repeating
206
+ * (`isTransient`) is fetched again after a short backoff — at most 10 attempts per
207
+ * page before the error is thrown.
208
+ *
209
+ * ```ts
210
+ * for await (const job of client.iterJobs({ createdFrom: "2026-10-01" })) { ... }
211
+ * ```
212
+ */
213
+ iterJobs(options?: Omit<ListJobsOptions, "cursor">): AsyncGenerator<Job>;
151
214
  /** Return account info: `{ email, credits }` (available credits). */
152
215
  account(): Promise<Account>;
153
216
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,gCAAgC;AA8BhC,OAAO,KAAK,EACV,OAAO,EACP,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,aAAa,EACb,WAAW,EACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,GAAG,EAA2B,MAAM,YAAY,CAAC;AAG1D,eAAO,MAAM,gBAAgB,0BAA0B,CAAC;AAgbxD;;;;;;;GAOG;AACH,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;gBAKrB,OAAO,EAAE,aAAa;IAiBlC;;;;;;;;;;;;;;;;;;;;;OAqBG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,GAAG,CAAC;IAOxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAmC7E,gFAAgF;IAC1E,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAQzC;;;;;;;;;;;OAWG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAQxD;;;;;;OAMG;IACG,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,GAAG,CAAC;IAmDlE;;;;;;;;;;OAUG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoEhE;;;;;;OAMG;IACG,OAAO,CACX,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,GAAG,CAAC;IAOf;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,UAAU,CACd,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,MAAM,EAAE,CAAC;IAyBpB,qEAAqE;IAC/D,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;CAuXlC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAoChC,OAAO,KAAK,EACV,OAAO,EACP,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,WAAW,EACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,GAAG,EAAE,OAAO,EAAwC,MAAM,YAAY,CAAC;AAGhF,eAAO,MAAM,gBAAgB,0BAA0B,CAAC;AAmhBxD;;;;;;;GAOG;AACH,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;gBAKrB,OAAO,EAAE,aAAa;IAiBlC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkCG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,GAAG,CAAC;IAgBxE;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,GAAG,CAAC;IA+B3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkCG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAqChF,gFAAgF;IAC1E,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAQzC;;;;;;;;;;;OAWG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAQxD;;;;;;OAMG;IACG,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,GAAG,CAAC;IAqDlE;;;;;;;;;;OAUG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoEhE;;;;;;OAMG;IACG,OAAO,CACX,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,GAAG,CAAC;IAsBf;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,UAAU,CACd,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,MAAM,EAAE,CAAC;IA4BpB;;;;;;;;OAQG;IACG,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,OAAO,CAAC;IAoB/D;;;;;;;;;;;OAWG;IACI,QAAQ,CAAC,OAAO,GAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAM,GAAG,cAAc,CAAC,GAAG,CAAC;IAUnF,qEAAqE;IAC/D,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;CA2elC"}