How-to2026-09-28約 4 分鐘閱讀

PDF API 的 TypeScript SDK:五分鐘完成第一次呼叫,以及它留給你的事

在 TypeScript 中使用 @pdf123/sdk 合併檔案、加入浮水印、讀取檔案資訊,並把多個工具串成一次請求。它會在上傳任何東西之前攔下拼錯的工具名稱或參數;伺服器拒絕時錯誤會附上原因與提示,而重試、取消和批次中的部分失敗則由你自己處理。

PDF123 · Updated 2026-09-28

從 Node 呼叫 PDF API 時,軟體開發套件(SDK)@pdf123/sdk 會在任何檔案上傳之前,攔下拼錯的工具名稱或參數。它不會重試、不會替你取消,也不會以串流回傳結果。new Pdf123Client() 預設以匿名身分指向 https://pdf123.xyz;你的檔案會上傳到那裡處理,因為沒有本機引擎。

示意圖:一次呼叫依序通過三道關卡,編譯時的 tsc 檢查、送出請求前的執行期驗證,以及上傳後伺服器回傳的 400;重試與取消留給呼叫端,輸入總計超過 95 MiB 時會自動分塊上傳

資料夾裡有兩份 PDF 就能合併

你需要 Node 20.3 以上版本,或 Bun。這個套件是 ES module:把程式碼放進 .mjs 檔,或先執行 npm pkg set type=module。在目前目錄放好 a.pdf 和 b.pdf。

npm install @pdf123/sdk
// merge.mjs
import { Pdf123Client } from "@pdf123/sdk";
import { readFileInput, saveResult } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const merged = await client.run("merge", {
  input: [await readFileInput("a.pdf"), await readFileInput("b.pdf")],
});
console.log(await saveResult(merged, { output: "merged.pdf" }));

執行 node merge.mjs 後,終端機會印出你傳入的輸出路徑 merged.pdf,檔案就在目前目錄。readFileInput 和 saveResult 位於 @pdf123/sdk/node。根入口只依賴 fetch,輸入就是單純的 { name, data },所以沒有檔案系統的環境也能用同一個 client。結果的形狀是 { data, contentType, filename, json };data 是完整的 Uint8Array,用 Buffer.from(result.data) 就能得到 Buffer。

合併 PDF只是其中一個工具。

檔案在 data 裡,報告在 json 裡

每個工具都是 client.run(toolName, { input, params })。下面的程式碼接續上一節的 merge.mjs,client 和 saveResult 已經在作用域內。結果要從哪裡讀,取決於 getTool 回傳的 returns 欄位,它不是 "file" 就是 "json":前者用 data,後者用 json。

import { getTool } from "@pdf123/sdk";
import { readFileInput, saveResult } from "@pdf123/sdk/node";

// client comes from merge.mjs in the previous section
const marked = await client.run("watermark", {
  input: await readFileInput("a.pdf"),
  params: { watermarkText: "DRAFT", fontSize: 40 },
});
console.log(await saveResult(marked, { output: "marked.pdf" }));

const info = await client.run("get-info", { input: await readFileInput("a.pdf") });
console.log(info.json.FileSize, info.json.Encrypted);
console.log(getTool("get-info")?.returns); // "json"

第一段把加了浮水印的檔案寫到 marked.pdf;第二段的 info.json 就是報告。欄位名稱、預設值和允許的值都不必背:getTool("watermark")?.fields 就是那份目錄,命令列上的 pdfx describe watermark 讀的也是同一份。TOOLS 收錄全部 95 個工具。完整參考見開發者頁面上的 SDK 指南。

要先合併、再加浮水印、再壓縮,可以用 pipeline,也可以連續呼叫三次 run;選哪一種,看你要不要中間檔案。這裡同樣接續上面的 client。pipeline 把三個步驟收進一次請求,中間結果留在伺服器上,呼叫端只拿到最後一步,下面的程式碼把它寫到 out.pdf。如果你想要每一步的檔案,就分開呼叫。

const result = await client.pipeline(
  [{ tool: "merge" }, { tool: "watermark", params: { watermarkText: "DRAFT" } }, { tool: "compress" }],
  [await readFileInput("a.pdf"), await readFileInput("b.pdf")],
);
console.log(await saveResult(result, { output: "out.pdf" }));

一條管線最多 8 個步驟。第 9 個步驟會得到 HTTP 400: at most 8 pipeline steps allowed。

匿名、帶金鑰,或指向你自己的伺服器

如果你設了 PDFX_API_BASE,請求卻還是打到 https://pdf123.xyz,原因是 new Pdf123Client() 不讀環境變數,它只看建構子的選項。

匿名呼叫就照原樣使用建構子。要帶金鑰,寫 new Pdf123Client({ apiKey: process.env.PDFX_API_KEY });請求標頭是 X-API-KEY。

要指向自己的伺服器,使用 @pdf123/sdk/node 的 clientFromEnv(),它會讀取 PDFX_API_BASE 和 PDFX_API_KEY。你也可以直接寫 new Pdf123Client({ baseUrl: "http://localhost:8080" })。位址連不上時呼叫會失敗,沒有離線模式。檔案留在自己的網路裡能得到什麼、要付出什麼,見自架實際買到什麼(以及要付出什麼)。

錯誤的參數在上傳前就被攔下

沒有這一層的話,檔案會先上傳,拼錯的欄位名稱只會以伺服器回傳的 400 浮現。每個工具的參數型別都由目錄產生,所以三種常見的失誤在 tsc 階段就會失敗:

你寫的內容 編譯器錯誤(節錄)
client.run("rotat", { input }) Argument of type '"rotat"' is not assignable to parameter of type 'ToolId'
params: { watermarkTxt: "DRAFT" } Object literal may only specify known properties, but 'watermarkTxt' does not exist,結尾是 Did you mean to write 'watermarkText'?
params: { angle: 45 }(rotate 只接受 90、180、270) Type '45' is not assignable to type …,後面接著允許的值

{ angle: 90 } 或 { watermarkText: "DRAFT", fontSize: 30 } 這類正確的呼叫可以通過編譯。數值欄位同時接受數字和字串,所以 fontSize: 30 與 fontSize: "30" 等價。不使用 TypeScript 的專案會在執行期得到同樣的錯誤,而且請求不會送出:第三種情況得到 Field "angle" of "rotate" must be one of: 90, 180, 270,工具名稱拼錯則得到 unknown_tool,訊息裡會列出相近的名稱。

伺服器拒絕時,依 reason 分支

伺服器拒絕請求時,SDK 會丟出 Pdf123Error,帶有 status、code、reason 和 problem.hint。同一個 code 可能對應多個 reason,所以先檢查 reason,再退回看 code。三種常見失敗,在本機用相同輸入執行,結果如下:

輸入 status code reason hint(原文為英文)
已加密的 PDF,沒有提供密碼 400 bad_request password_required Provide the document password, or unlock the PDF first
密碼錯誤 400 bad_request wrong_password Check the password and try again
不是 PDF,或檔案已損毀 400 invalid_document invalid_pdf Upload a valid, undamaged PDF; you can try repairing it first
import { Pdf123Client, Pdf123Error } from "@pdf123/sdk";
import { readFileInput } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const input = await readFileInput("locked.pdf");
try {
  await client.run("compress", { input });
} catch (error) {
  if (!(error instanceof Pdf123Error)) throw error;
  console.error(error.status, error.code, error.reason, error.problem?.hint);
  if (error.reason === "password_required") {
    await client.run("compress", { input, password: "secret" });
  }
}

知道密碼時,單一檔案可以傳 password;解鎖與目標工具在同一次請求內完成。檔案損毀的話,先試試修復 PDF。

「沒有結果」需要分辨清楚。PDF 轉 CSV 在 PDF 裡找不到任何表格時,伺服器回傳 204,SDK 丟出 code: "no_content";原因見空白表格匯出(204):你的 PDF 大概沒有欄。條件式篩選工具把條件不成立視為正常結果:它們不會丟出錯誤,而是回傳 matched: false 與空的 data。

重試、記憶體與取消由呼叫端負責

網路錯誤和 5xx 回應會原樣丟出;SDK 從不自行重試。對會改變狀態的請求,傳入你自己的 idempotencyKey:用同一個金鑰重送,會回傳第一次的結果而不是重新處理,機制與限制見 Idempotency-Key:PDF 工作的安全重試。結果會整份讀進記憶體,沒有串流。輸入與輸出都很大時,要把兩者同時佔用的記憶體算進去。

取消與逾時是兩個不同的 code,而且一次呼叫只會回報先發生的那一個。下面的範例分成兩次呼叫分別測試,讓兩個分支都能真正執行:

import { Pdf123Client, Pdf123Error } from "@pdf123/sdk";
import { readFileInput } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const input = await readFileInput("a.pdf");

const controller = new AbortController();
setTimeout(() => controller.abort(), 10_000);
try {
  await client.run("compress", { input, signal: controller.signal });
} catch (error) {
  if (error instanceof Pdf123Error && error.code === "cancelled") { /* 是你取消的 */ }
}

try {
  await client.run("compress", { input, timeoutMs: 60_000 });
} catch (error) {
  if (error instanceof Pdf123Error && error.code === "timeout") { /* 超過 timeoutMs;這次沒有 signal */ }
}

中止訊號(AbortSignal)一觸發,請求就會以 code: "cancelled" 被拒絕;超過逾時則是 code: "timeout"。每個請求的預設逾時為 5 分鐘,可以在建立 client 時修改,也可以在單次呼叫用 timeoutMs 覆寫。分塊上傳時,每個請求各自帶有這個逾時。取消只會關閉這條連線,並不保證伺服器會停止處理。

一個檔案失敗,其餘繼續,回呼按完成順序到達

對單檔工具處理一批輸入時,使用 runBatch。如果其中一個檔案失敗,其他檔案照常進行。onResult 在每個檔案完成的當下被呼叫,所以它依完成順序執行,index 是該檔案在輸入陣列中的位置;回傳的陣列則永遠依輸入順序排列。請在這個回呼裡把位元組寫到磁碟:retainData: false 會在回呼返回後清空 data,所以沒有在這裡 saveResult 的結果就沒了。如果執行中途被打斷,已經寫出的檔案會保留。

假設資料夾裡有三份正常的 PDF,a.pdf、b.pdf 和已加密的 locked.pdf(密碼為 secret),另外還有一個只含幾個手打字元的損毀檔案 broken.pdf:

import { Pdf123Client } from "@pdf123/sdk";
import { fileSource, saveResult } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const results = await client.runBatch("compress", [
  fileSource("a.pdf"), fileSource("broken.pdf"), fileSource("b.pdf"), fileSource("locked.pdf"),
], {
  concurrency: 2,
  passwordFor: (file) => (file.name === "locked.pdf" ? "secret" : undefined),
  onResult: async (entry, index) => {
    console.log(index, entry.input.name, entry.ok ? "ok" : entry.error.reason);
    if (entry.ok) await saveResult(entry.result, { output: "out/" });
  },
  retainData: false,
});

預設同時處理兩個檔案;用 concurrency 調整。fileSource(path) 只在輪到該檔案時才從磁碟讀取,所以一大批大檔案不會同時全部待在記憶體裡。passwordFor 讓上鎖與未上鎖檔案混在一起的批次可以一次跑完。在本機執行上面這四個檔案,回呼收到的是:

1 broken.pdf invalid_pdf
0 a.pdf ok
2 b.pdf ok
3 locked.pdf ok

完成順序每次執行都可能不同;這只是某一次執行的樣子。retainData: false 會讓回傳陣列中的 data 變成空的;成功的檔案已經由上面的 saveResult 寫進 out/。對 runBatch 傳入 idempotencyKey 時,實際送給每個檔案的金鑰是 <key>:<index>。

總計超過 95 MiB 才會啟用分塊上傳

直接請求的本體上限是 100 MiB,超過就會被拒絕;細節見大型 PDF 上傳被拒:100 MiB 請求本體上限,與那個指向錯誤方向的訊息。同一次請求內所有檔案加起來超過 95 MiB 時,SDK 會改用分塊上傳,而你的 client.run 呼叫不用改。伺服器對單次上傳的預設上限是 500 MiB;自架時用 PDFX_UPLOAD_MAX_BYTES 調整。

同一個操作用 curl、MCP 與命令列怎麼寫,見同一操作,四種用戶端:瀏覽器、curl、MCP、pdfx:那篇把四種呼叫並排放在一起,這篇只展開 SDK。這個套件在 npm 上的頁面是 @pdf123/sdk。

Open tool
Process in the browser — no watermark, files removed after the job.
Open tool