SDK de TypeScript para la API de PDF: cinco minutos hasta la primera llamada, y lo que deja en tus manos
Usa @pdf123/sdk desde TypeScript para unir archivos, añadir marcas de agua, leer la información de un archivo y encadenar varias herramientas en una sola petición. Detecta un nombre de herramienta o de parámetro mal escrito antes de subir nada; los rechazos del servidor traen un motivo y una pista, mientras que los reintentos, la cancelación y los fallos parciales de un lote son cosa tuya.

Cuando llamas a la API de PDF desde Node, el kit de desarrollo de software (SDK) @pdf123/sdk detecta un nombre de herramienta o de parámetro mal escrito antes de subir ningún archivo. No reintenta, no cancela por ti ni transmite resultados en streaming. new Pdf123Client() apunta por defecto, de forma anónima, a https://pdf123.xyz; tus archivos se suben allí y se procesan allí, porque no hay motor local.
Dos PDF en una carpeta bastan para unirlos
Necesitas Node 20.3 o superior, o Bun. El paquete es un módulo ES: pon el código en un archivo .mjs, o ejecuta antes npm pkg set type=module. Deja a.pdf y b.pdf en el directorio actual.
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" }));
Tras node merge.mjs, el terminal imprime la ruta de salida que indicaste, merged.pdf, y el archivo queda en el directorio actual. readFileInput y saveResult están en @pdf123/sdk/node. La entrada raíz depende solo de fetch y recibe las entradas como un simple { name, data }, así que un entorno sin sistema de archivos puede usar el mismo cliente. Un resultado es { data, contentType, filename, json }; data es un Uint8Array completo, y Buffer.from(result.data) te da un Buffer.
Unir PDF es solo una de las herramientas.
Los archivos vuelven en data, los informes en json
Cada herramienta se llama con client.run(toolName, { input, params }). El código siguiente continúa merge.mjs de la sección anterior, con client y saveResult ya disponibles. Dónde leer el resultado depende del campo returns de getTool, que es "file" o "json": usa data para el primero y json para el segundo.
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"
El primer bloque escribe el archivo con marca de agua en marked.pdf; en el segundo, info.json es el informe. No hace falta memorizar nombres de campos, valores por defecto ni valores permitidos: getTool("watermark")?.fields es ese catálogo, y pdfx describe watermark en la línea de comandos lee el mismo. TOOLS contiene las 95 herramientas. La referencia completa está en la guía del SDK en la página para desarrolladores.
Para unir, luego poner marca de agua y luego comprimir, puedes usar pipeline o tres llamadas a run seguidas; cuál elegir depende de si quieres los archivos intermedios. De nuevo, esto continúa con el client de arriba. pipeline reúne los tres pasos en una sola petición, los resultados intermedios se quedan en el servidor, y quien llama recibe solo el último paso, que el código siguiente escribe en out.pdf. Si quieres el archivo de cada paso, llámalos por separado.
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" }));
Un pipeline tiene como máximo 8 pasos. Un noveno paso recibe HTTP 400: at most 8 pipeline steps allowed.
De forma anónima, con clave o apuntando a tu propio servidor
Si estableces PDFX_API_BASE y sigues llegando a https://pdf123.xyz, es porque new Pdf123Client() no lee variables de entorno; solo mira las opciones de su constructor.
Para llamadas anónimas, usa ese constructor tal cual. Con clave, escribe new Pdf123Client({ apiKey: process.env.PDFX_API_KEY }); la cabecera de la petición es X-API-KEY.
Para apuntar a tu propio servidor, usa clientFromEnv() de @pdf123/sdk/node. Lee PDFX_API_BASE y PDFX_API_KEY. También puedes escribir directamente new Pdf123Client({ baseUrl: "http://localhost:8080" }). Si la dirección no es alcanzable, la llamada falla; no hay modo sin conexión. Lo que ganas y lo que cuesta cuando los archivos se quedan en tu propia red se explica en Qué te compra realmente el self-hosting (y qué te cuesta).
Un parámetro incorrecto se detecta antes de la subida
Sin esto, el archivo se subiría primero y un nombre de campo mal escrito solo aparecería como un 400 del servidor. Los tipos de parámetros de cada herramienta se generan a partir del catálogo, de modo que tres descuidos habituales fallan en la fase de tsc:
| Lo que escribes | Error del compilador (extracto) |
|---|---|
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, terminando en Did you mean to write 'watermarkText'? |
params: { angle: 45 } (rotate solo acepta 90, 180, 270) |
Type '45' is not assignable to type …, seguido de los valores permitidos |
Las llamadas correctas, como { angle: 90 } o { watermarkText: "DRAFT", fontSize: 30 }, compilan. Los campos numéricos aceptan números y cadenas, así que fontSize: 30 y fontSize: "30" son equivalentes. Un proyecto que no usa TypeScript recibe los mismos errores en tiempo de ejecución, y la petición nunca se envía: el tercer caso da Field "angle" of "rotate" must be one of: 90, 180, 270, y un nombre de herramienta mal escrito da unknown_tool, con los nombres parecidos listados en el mensaje.
Cuando el servidor rechaza, ramifica según reason
Cuando el servidor rechaza una petición, el SDK lanza Pdf123Error con status, code, reason y problem.hint. Un mismo code puede tener varios valores de reason, así que comprueba primero reason y recurre a code si hace falta. Tres fallos habituales, ejecutados en local con la misma entrada, se ven así:
| Entrada | status | code | reason | hint (el original está en inglés) |
|---|---|---|---|---|
| PDF cifrado, sin contraseña | 400 | bad_request |
password_required |
Provide the document password, or unlock the PDF first |
| Contraseña incorrecta | 400 | bad_request |
wrong_password |
Check the password and try again |
| No es un PDF, o es un archivo dañado | 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" });
}
}
Cuando conozcas la contraseña, pasa password para un solo archivo; el desbloqueo y la herramienta de destino se hacen en la misma petición. Para un archivo dañado, prueba primero Reparar PDF.
Hay que distinguir el «sin resultado». Cuando PDF a CSV no encuentra ninguna tabla en el PDF, el servidor devuelve 204 y el SDK lanza code: "no_content"; el motivo está en Exportación de tabla vacía (204): tu PDF probablemente no tiene columnas. Las herramientas de filtro condicional tratan una condición falsa como un resultado normal: no lanzan nada y devuelven matched: false con data vacío.
Reintentos, memoria y cancelación son cosa de quien llama
Los errores de red y las respuestas 5xx se lanzan tal cual; el SDK nunca reintenta por su cuenta. En una petición que cambia estado, pasa tu propio idempotencyKey: reenviar con la misma clave devuelve el primer resultado en lugar de procesar de nuevo, y el mecanismo y los límites están en Idempotency-Key: reintentos seguros para trabajos PDF. Los resultados se leen enteros en memoria, sin streaming. Cuando la entrada y la salida son grandes, cuenta la memoria que ocupan a la vez.
Cancelación y tiempo de espera son dos valores de code distintos. Una sola llamada informa únicamente del que ocurra primero. El ejemplo de abajo los prueba en dos llamadas separadas, para que las dos ramas puedan ejecutarse de verdad:
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") { /* lo cancelaste tú */ }
}
try {
await client.run("compress", { input, timeoutMs: 60_000 });
} catch (error) {
if (error instanceof Pdf123Error && error.code === "timeout") { /* superó timeoutMs; esta vez sin signal */ }
}
En cuanto se dispara la señal de aborto (AbortSignal), la petición se rechaza con code: "cancelled"; superar el tiempo de espera da code: "timeout". Cada petición tiene un tiempo de espera por defecto de 5 minutos, que puedes cambiar al crear el cliente o sobrescribir en cada llamada con timeoutMs. En una subida por partes, cada petición lleva ese tiempo de espera por su cuenta. Cancelar solo cierra esta conexión; no hay garantía de que el servidor deje de procesar.
Un archivo falla, el resto continúa, y los callbacks llegan en orden de finalización
Para un lote de entradas con una herramienta de un solo archivo, usa runBatch. Si un archivo falla, los demás siguen. onResult se llama en el momento en que termina cada archivo, así que se ejecuta en orden de finalización, e index es la posición del archivo en el array de entrada; el array devuelto siempre está en el orden de entrada. Escribe los bytes en disco dentro de este callback: retainData: false vacía data en cuanto el callback retorna, así que un resultado que no guardes aquí con saveResult se pierde. Si la ejecución se interrumpe, los archivos ya escritos se conservan.
Supón que la carpeta tiene tres PDF buenos, a.pdf, b.pdf y un locked.pdf cifrado (contraseña secret), además de un archivo roto, broken.pdf, que solo contiene unos pocos caracteres tecleados:
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,
});
Por defecto se procesan dos archivos a la vez; cámbialo con concurrency. fileSource(path) lee un archivo del disco solo cuando le toca el turno, de modo que un lote grande de archivos grandes nunca está entero en memoria. passwordFor permite que un lote que mezcla archivos bloqueados y no bloqueados se ejecute de una vez. Al ejecutar en local los cuatro archivos anteriores, el callback recibió:
1 broken.pdf invalid_pdf
0 a.pdf ok
2 b.pdf ok
3 locked.pdf ok
El orden de finalización puede ser distinto en cada ejecución; esto es solo cómo se vio una. retainData: false deja vacío data en el array devuelto; los archivos correctos ya se habían escrito en out/ con el saveResult de arriba. Cuando pasas idempotencyKey a runBatch, la clave que se envía realmente para cada archivo es <key>:<index>.
La subida por partes empieza solo por encima de 95 MiB en total
El cuerpo de una petición directa está limitado a 100 MiB, y cualquier cosa mayor se rechaza; los detalles están en Cuando un PDF grande no sube: el límite de 100 MiB y el error que apunta en la dirección equivocada. Cuando todos los archivos de una petición suman más de 95 MiB, el SDK cambia a la subida por partes, y tu llamada a client.run no cambia. El máximo por defecto del servidor para una sola subida es de 500 MiB; si alojas tú mismo el servicio, ajústalo con PDFX_UPLOAD_MAX_BYTES.
Para la misma operación escrita en curl, MCP y línea de comandos, consulta Misma operación, cuatro clientes: navegador, curl, MCP, pdfx: ese artículo pone las cuatro llamadas una al lado de la otra, y este solo desarrolla el SDK. La página del paquete en npm es @pdf123/sdk.