New: PDF to E-Invoice for Factur-X/ZUGFeRD, and PDF to PDF/A for archive-ready PDF/A-3b.

Edit, Watermark and Password-Protect PDFs in Node.js

7 Minutes Read

This guide demonstrates text replacement, visible watermarking, PDF passwords, and rendering pages as images. These operations have different purposes. A watermark or an image-based copy does not make visible confidential information safe to distribute.

The examples below are independent operations, not five successive levels of protection.

Prerequisites

Use a current source URL you control. Keep the source accessible throughout processing.

Set your API key in the terminal where you will run the examples.

On macOS or Linux:

export PDFCO_API_KEY="YOUR_PDFCO_API_KEY"

In PowerShell:

$env:PDFCO_API_KEY = "YOUR_PDFCO_API_KEY"

Create the Script

Save this complete script as pdf-tools.cjs. It provides five operations through one shared request-and-download routine.

const { writeFile } = require("node:fs/promises");

function requiredEnv(name) {
  const value = process.env[name];
  if (!value || !value.trim()) throw new Error(`Set ${name} first.`);
  return value;
}

async function main() {
  const [operation, sourceUrl] = process.argv.slice(2);
  const configs = {
    replace: {
      path: "/v1/pdf/edit/replace-text",
      file: "replaced.pdf",
      payload: {
        searchStrings: ["Your Company Name", "Client Name", "Item"],
        replaceStrings: ["XYZ LLC", "ACME", "SKU"],
        regex: false
      }
    },
    watermark: {
      path: "/v1/pdf/edit/add",
      file: "watermarked.pdf",
      payload: {
        images: [{
          url: process.env.PDFCO_WATERMARK_URL,
          x: 400, y: 20, width: 119, height: 32, pages: "0-"
        }]
      }
    },
    protect: {
      path: "/v1/pdf/security/add",
      file: "protected.pdf",
      payload: {
        encryptionAlgorithm: "AES_256bit",
        allowAccessibilitySupport: true,
        allowPrintDocument: true,
        allowAssemblyDocument: false,
        allowFillForms: false,
        allowModifyDocument: false,
        allowContentExtraction: false,
        allowModifyAnnotations: false
      }
    },
    rasterize: {
      path: "/v1/pdf/makeunsearchable",
      file: "image-only.pdf",
      payload: { pages: "0-" }
    },
    jpg: {
      path: "/v1/pdf/convert/to/jpg",
      images: true,
      payload: {
        pages: "0-",
        inline: false,
        profiles: JSON.stringify({ RenderingResolution: 150 })
      }
    }
  };

  if (!Object.hasOwn(configs, operation) || !sourceUrl) {
    throw new Error(
      "Usage: node pdf-tools.cjs replace|watermark|protect|rasterize|jpg SOURCE_URL"
    );
  }

  const config = configs[operation];
  const apiKey = requiredEnv("PDFCO_API_KEY");

  if (operation === "watermark") requiredEnv("PDFCO_WATERMARK_URL");

  if (operation === "protect") {
    config.payload.ownerPassword = requiredEnv("PDFCO_OWNER_PASSWORD");
    config.payload.userPassword = requiredEnv("PDFCO_USER_PASSWORD");

    if (config.payload.ownerPassword === config.payload.userPassword) {
      throw new Error("Use different owner and user passwords.");
    }
  }

  const response = await fetch("https://api.pdf.co" + config.path, {
    method: "POST",
    headers: {
      "x-api-key": apiKey,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      url: sourceUrl,
      async: false,
      ...(config.file ? { name: config.file } : {}),
      ...config.payload
    }),
    signal: AbortSignal.timeout(60000)
  });

  const result = await response.json();

  if (!response.ok || result.error || result.jobId) {
    throw new Error(
      result.message || "Expected a completed synchronous result."
    );
  }

  const urls = config.images
    ? result.urls
    : (result.url ? [result.url] : []);

  if (
    !Array.isArray(urls) ||
    !urls.length ||
    urls.some(url => typeof url !== "string" || !url)
  ) {
    throw new Error("No valid output URL list was returned.");
  }

  for (const [index, url] of urls.entries()) {
    const downloaded = await fetch(url, {
      signal: AbortSignal.timeout(60000)
    });

    if (!downloaded.ok) {
      throw new Error(`Download failed: ${downloaded.status}`);
    }

    const destination = config.images
      ? `page-${index + 1}.jpg`
      : config.file;

    await writeFile(
      destination,
      Buffer.from(await downloaded.arrayBuffer()),
      { flag: "wx" }
    );

    console.log(
      `Saved ${destination}. Inspect the output before sharing it.`
    );
  }
}

main().catch(error => {
  console.error(error.message);
  process.exitCode = 1;
});

The script reads the complete JSON response before parsing it and checks each download before saving it. Output requests do not include your PDF.co API key.

Run the examples in a working folder where their output filenames do not already exist. The script will not overwrite existing files.

Step 1: Replace Selected Text

Edit the script’s searchStrings and replaceStrings arrays for your document. Each search string corresponds to the replacement at the same position.

Run:

node pdf-tools.cjs replace "YOUR_CURRENT_SOURCE_PDF_URL"

The result is saved as replaced.pdf.

Inspect every match and the rest of the document. Replacing visible text does not establish that the same information is absent from images, metadata, attachments, or other content. This example is not a complete redaction review.

See the Search and Replace Text documentation.

Step 2: Add a Visible Watermark

A logo or other image can identify a document copy. A watermark changes the visible document; it does not encrypt it, verify authenticity, or prevent copying and editing.

Set a direct URL to your watermark image.

On macOS or Linux:

export PDFCO_WATERMARK_URL="YOUR_IMAGE_URL"

In PowerShell:

$env:PDFCO_WATERMARK_URL = "YOUR_IMAGE_URL"

Run:

node pdf-tools.cjs watermark "YOUR_CURRENT_SOURCE_PDF_URL"

The result is saved as watermarked.pdf.

The example places the image at X = 400, Y = 20, with a width of 119 and height of 32, on every page. Adjust these values for your document. Use the PDF Inspector to determine suitable coordinates.

See the PDF Add documentation for image and text-placement options.

Step 3: Add Passwords and Permission Settings

Set two strong, different passwords:

  • Owner password: Used to manage document permissions. Keep it private.
  • User password: Required for recipients to open the PDF.

On macOS or Linux:

export PDFCO_OWNER_PASSWORD="YOUR_STRONG_OWNER_PASSWORD"
export PDFCO_USER_PASSWORD="YOUR_STRONG_USER_PASSWORD"

In PowerShell:

$env:PDFCO_OWNER_PASSWORD = "YOUR_STRONG_OWNER_PASSWORD"
$env:PDFCO_USER_PASSWORD = "YOUR_STRONG_USER_PASSWORD"

Run:

node pdf-tools.cjs protect "YOUR_CURRENT_SOURCE_PDF_URL"

The result is saved as protected.pdf using AES-256 encryption.

This example allows printing and accessibility support while disabling the listed editing, assembly, form-filling, annotation, and content-extraction permissions. Permission restrictions depend on the PDF software honoring them.

Test the saved PDF with the user password, not the owner password. Share the user password only with authorized recipients through an appropriate separate channel.

See the PDF Password and Security documentation.

Step 4: Create an Image-Based PDF

Rasterizing the PDF creates an image-based copy with no directly selectable source text. It can satisfy a rendered-copy requirement, but OCR can recover visible text. Do not treat rasterization as confidential-content removal.

Run:

node pdf-tools.cjs rasterize "YOUR_CURRENT_SOURCE_PDF_URL"

The result is saved as image-only.pdf.

Compare all pages with the source and check readability. Keep the original if you need selectable text, accessibility features, or interactive form fields.

See the Make Text Unsearchable documentation.

Step 5: Export PDF Pages as Images

Use page-image conversion when a destination requires previews or thumbnails. Exporting images is not an access-control or redaction method.

Run:

node pdf-tools.cjs jpg "YOUR_CURRENT_SOURCE_PDF_URL"

The script downloads every returned image as page-1.jpg, page-2.jpg, and so on.

The example processes every page at 150 DPI. Adjust RenderingResolution for your destination and check legibility and file size. Page selection is zero-based: 0 selects the first page, while 0- includes every page.

See the PDF to JPG documentation.

Processing Larger Files

These examples use synchronous requests. For longer tasks, implement the documented asynchronous job-check workflow. A client-side timeout does not prove that server processing stopped.

Download completed outputs before their temporary URLs expire, and inspect the saved files before sharing them.

Related Tutorials

See Related Tutorials