SDK TypeScript pour l’API PDF : cinq minutes jusqu’au premier appel, et ce qu’il vous laisse à faire
Utilisez @pdf123/sdk depuis TypeScript pour fusionner des fichiers, ajouter des filigranes, lire les informations d’un fichier et enchaîner plusieurs outils dans une seule requête. Il repère un nom d’outil ou un paramètre mal orthographié avant tout envoi ; les rejets du serveur portent une raison et une indication, alors que les reprises, l’annulation et les échecs partiels d’un lot restent à votre charge.

Quand vous appelez l’API PDF depuis Node, le kit de développement (SDK) @pdf123/sdk repère un nom d’outil ou un paramètre mal orthographié avant l’envoi de tout fichier. Il ne réessaie pas, n’annule pas à votre place et ne diffuse pas les résultats en flux. new Pdf123Client() pointe par défaut, de façon anonyme, vers https://pdf123.xyz ; vos fichiers y sont envoyés et traités, puisqu’il n’y a pas de moteur local.
Deux PDF dans un dossier suffisent pour fusionner
Il vous faut Node 20.3 ou plus récent, ou Bun. Le paquet est un module ES : mettez le code dans un fichier .mjs, ou exécutez d’abord npm pkg set type=module. Placez a.pdf et b.pdf dans le répertoire courant.
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" }));
Après node merge.mjs, le terminal affiche le chemin de sortie que vous avez donné, merged.pdf, et le fichier se trouve dans le répertoire courant. readFileInput et saveResult se trouvent dans @pdf123/sdk/node. L’entrée racine ne dépend que de fetch et prend des entrées sous la forme simple { name, data }, de sorte qu’un environnement sans système de fichiers peut utiliser le même client. Un résultat est { data, contentType, filename, json } ; data est un Uint8Array complet, et Buffer.from(result.data) vous donne un Buffer.
Fusionner des PDF n’est qu’un outil parmi d’autres.
Les fichiers reviennent dans data, les rapports dans json
Chaque outil s’appelle par client.run(toolName, { input, params }). Le code ci-dessous prolonge merge.mjs de la section précédente, avec client et saveResult déjà dans la portée. L’endroit où lire le résultat dépend du champ returns renvoyé par getTool, qui vaut "file" ou "json" : utilisez data dans le premier cas, json dans le second.
import { getTool } from "@pdf123/sdk";
import { readFileInput, saveResult } from "@pdf123/sdk/node";
// client vient de merge.mjs, dans la section précédente
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"
Le premier bloc écrit le fichier filigrané dans marked.pdf ; dans le second, info.json est le rapport. Inutile de mémoriser les noms de champs, les valeurs par défaut ou les valeurs autorisées : getTool("watermark")?.fields est ce catalogue, et pdfx describe watermark en ligne de commande lit le même. TOOLS contient les 95 outils. La référence complète se trouve dans le guide du SDK sur la page développeurs.
Pour fusionner, puis filigraner, puis compresser, vous pouvez utiliser pipeline ou trois appels run à la suite ; le choix dépend de votre besoin des fichiers intermédiaires. Là encore, cela prolonge le client ci-dessus. pipeline replie les trois étapes en une seule requête, les résultats intermédiaires restent sur le serveur et l’appelant ne reçoit que la dernière étape, que le code ci-dessous écrit dans out.pdf. Si vous voulez le fichier de chaque étape, faites les appels séparément.
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 compte au plus 8 étapes. Une 9e étape reçoit HTTP 400: at most 8 pipeline steps allowed.
En anonyme, avec une clé, ou vers votre propre serveur
Si vous avez défini PDFX_API_BASE et que les appels vont quand même vers https://pdf123.xyz, c’est parce que new Pdf123Client() ne lit pas les variables d’environnement ; il ne regarde que les options de son constructeur.
Pour des appels anonymes, utilisez ce constructeur tel quel. Avec une clé, écrivez new Pdf123Client({ apiKey: process.env.PDFX_API_KEY }) ; l’en-tête de la requête est X-API-KEY.
Pour viser votre propre serveur, utilisez clientFromEnv() de @pdf123/sdk/node. Il lit PDFX_API_BASE et PDFX_API_KEY. Vous pouvez aussi écrire directement new Pdf123Client({ baseUrl: "http://localhost:8080" }). Si l’adresse est injoignable, l’appel échoue ; il n’existe pas de mode hors ligne. Ce que vous obtenez, et ce que cela coûte, une fois les fichiers gardés sur votre propre réseau est détaillé dans Ce que l’auto-hébergement vous apporte vraiment (et ce qu’il coûte).
Un mauvais paramètre est repéré avant l’envoi
Sans cela, le fichier serait d’abord envoyé et un nom de champ mal orthographié ne ressortirait que sous la forme d’un 400 du serveur. Les types des paramètres de chaque outil sont générés à partir du catalogue, de sorte que trois fautes courantes échouent dès l’étape tsc :
| Ce que vous écrivez | Erreur du compilateur (extrait) |
|---|---|
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, se terminant par Did you mean to write 'watermarkText'? |
params: { angle: 45 } (rotate n’accepte que 90, 180, 270) |
Type '45' is not assignable to type …, suivi des valeurs autorisées |
Les appels corrects comme { angle: 90 } ou { watermarkText: "DRAFT", fontSize: 30 } compilent. Les champs numériques acceptent aussi bien des nombres que des chaînes, donc fontSize: 30 et fontSize: "30" sont équivalents. Un projet qui n’utilise pas TypeScript obtient les mêmes erreurs à l’exécution, et la requête n’est jamais envoyée : le troisième cas donne Field "angle" of "rotate" must be one of: 90, 180, 270, et un nom d’outil mal orthographié donne unknown_tool, avec les noms proches listés dans le message.
Quand le serveur rejette, branchez sur reason
Quand le serveur rejette une requête, le SDK lève Pdf123Error avec status, code, reason et problem.hint. Un même code peut avoir plusieurs valeurs de reason ; vérifiez donc reason d’abord et repliez-vous sur code. Trois échecs courants, exécutés en local avec la même entrée, ressemblent à ceci :
| Entrée | status | code | reason | hint (l’original est en anglais) |
|---|---|---|---|---|
| PDF chiffré, aucun mot de passe fourni | 400 | bad_request |
password_required |
Fournissez le mot de passe du document, ou déverrouillez d’abord le PDF |
| Mauvais mot de passe | 400 | bad_request |
wrong_password |
Vérifiez le mot de passe et réessayez |
| Pas un PDF, ou fichier endommagé | 400 | invalid_document |
invalid_pdf |
Envoyez un PDF valide et intact ; vous pouvez d’abord essayer de le réparer |
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" });
}
}
Quand vous connaissez le mot de passe, passez password pour un seul fichier ; le déverrouillage et l’outil visé se font dans la même requête. Pour un fichier endommagé, essayez d’abord Réparer le PDF.
Il faut distinguer les cas de « pas de résultat ». Quand PDF vers CSV ne trouve aucun tableau dans le PDF, le serveur renvoie 204 et le SDK lève code: "no_content" ; la raison est expliquée dans Export de tableau vide (204) : votre PDF n’a probablement pas de colonnes. Les outils de filtre conditionnels traitent une condition fausse comme un résultat normal : ils ne lèvent rien et renvoient matched: false avec un data vide.
Reprises, mémoire et annulation : c’est l’affaire de l’appelant
Les erreurs réseau et les réponses 5xx sont levées telles quelles ; le SDK ne réessaie jamais de lui-même. Pour une requête qui modifie un état, passez votre propre idempotencyKey : renvoyer la requête avec la même clé retourne le premier résultat au lieu de traiter à nouveau, et le mécanisme comme ses limites sont dans Idempotency-Key : des reprises sûres pour les tâches PDF. Les résultats sont lus en mémoire en entier, sans flux. Quand l’entrée et la sortie sont toutes deux volumineuses, comptez la mémoire qu’elles occupent en même temps.
L’annulation et le délai d’expiration correspondent à deux valeurs de code différentes, et un même appel ne signale que celle qui survient en premier. L’exemple ci-dessous les teste dans deux appels séparés, pour que les deux branches puissent réellement s’exécuter :
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") { /* vous l’avez annulé */ }
}
try {
await client.run("compress", { input, timeoutMs: 60_000 });
} catch (error) {
if (error instanceof Pdf123Error && error.code === "timeout") { /* timeoutMs dépassé ; pas de signal cette fois */ }
}
Dès que le signal d’abandon (AbortSignal) se déclenche, la requête est rejetée avec code: "cancelled" ; dépasser le délai donne code: "timeout". Chaque requête a un délai par défaut de 5 minutes, que vous pouvez modifier à la création du client ou remplacer appel par appel avec timeoutMs. Dans un envoi par morceaux, chaque requête porte ce délai pour elle-même. Annuler ne ferme que cette connexion ; rien ne garantit que le serveur arrête son traitement.
Un fichier échoue, les autres continuent, et les rappels arrivent dans l’ordre d’achèvement
Pour un lot d’entrées destinées à un outil à fichier unique, utilisez runBatch. Si un fichier échoue, les autres continuent. onResult est appelé dès que chaque fichier est terminé, donc dans l’ordre d’achèvement, et index est la position du fichier dans le tableau d’entrée ; le tableau renvoyé suit toujours l’ordre d’entrée. Écrivez les octets sur le disque dans ce rappel : retainData: false vide data dès que le rappel retourne, donc un résultat que vous ne passez pas à saveResult ici est perdu. Si l’exécution est interrompue, les fichiers déjà écrits restent.
Supposons que le dossier contienne trois PDF valides, a.pdf, b.pdf et un locked.pdf chiffré (mot de passe secret), plus un fichier cassé broken.pdf qui ne contient que quelques caractères tapés :
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,
});
Par défaut, deux fichiers sont traités à la fois ; changez cela avec concurrency. fileSource(path) ne lit un fichier sur le disque que lorsque son tour arrive, si bien qu’un grand lot de gros fichiers n’est jamais entièrement en mémoire. passwordFor permet de traiter d’un seul coup un lot qui mélange fichiers verrouillés et non verrouillés. En exécutant les quatre fichiers ci-dessus en local, le rappel a reçu :
1 broken.pdf invalid_pdf
0 a.pdf ok
2 b.pdf ok
3 locked.pdf ok
L’ordre d’achèvement peut changer à chaque exécution ; ceci est simplement ce qu’a donné une exécution. retainData: false rend vide data dans le tableau renvoyé ; les fichiers réussis ont déjà été écrits dans out/ par le saveResult ci-dessus. Quand vous passez idempotencyKey à runBatch, la clé réellement envoyée pour chaque fichier est <key>:<index>.
L’envoi par morceaux ne commence qu’au-delà de 95 MiB au total
Le corps d’une requête directe est limité à 100 MiB, et tout ce qui dépasse est rejeté ; voir Quand un gros PDF ne monte pas : la limite de 100 MiB par requête et l'erreur qui vous oriente mal pour le détail. Quand tous les fichiers d’une requête dépassent ensemble 95 MiB, le SDK passe à l’envoi par morceaux, et votre appel client.run ne change pas. Le maximum par défaut du serveur pour un envoi unique est de 500 MiB ; si vous auto-hébergez, ajustez-le avec PDFX_UPLOAD_MAX_BYTES.
Pour la même opération écrite en curl, en MCP et en ligne de commande, voir Une même opération, quatre clients : navigateur, curl, MCP, pdfx : ce billet met les quatre appels côte à côte, et celui-ci ne développe que le SDK. La page du paquet sur npm est @pdf123/sdk.