Back to Blog
JavaScriptNode.jsPDFDigital Signature

How to Sign a PDF in JavaScript (With Real API Examples)

PDFSignify TeamMarch 12, 202610 min read

Digitally signing a PDF in JavaScript requires a backend approach. Browsers don't have access to digital certificates or the cryptographic operations needed for real PDF signing. The actual signing must happen server-side, where your code can read certificate files and communicate securely with a signing API. In this guide, we'll walk through how to sign PDFs using JavaScript with the PDFSignify API.

Why You Can't Sign PDFs in the Browser

Certificate-based digital signatures require access to a .pfx or .p12 file and the private key inside it. Browsers intentionally restrict access to the filesystem and cryptographic key stores for security reasons. Even the Web Crypto API doesn't support PKCS#12 certificate files directly. For real digital signatures, you need a server-side environment like Node.js.

The Architecture: Frontend + Backend

  • Frontend: Collects the PDF file from the user (e.g., via a file input or drag-and-drop)
  • Backend: Receives the PDF, reads the certificate from the server, calls the PDFSignify API
  • PDFSignify API: Applies the cryptographic digital signature and returns the signed PDF
  • Backend: Sends the signed PDF back to the frontend for download

Your digital certificate and API credentials stay on your server — they should never be exposed to the client.

Prerequisites

  • A PDFSignify account with your AccessKey and SecretKey
  • A digital certificate file (.pfx or .p12) and its password
  • Node.js installed on your server
  • axios and form-data packages (npm install axios form-data)

Basic Example: Sign a PDF with axios

This is the simplest possible example. Read the certificate and PDF from disk, build a multipart form, and POST to PDFSignify. The signed PDF comes back as binary data in the response.

javascript
import axios from "axios";
import * as fs from "fs";
import FormData from "form-data";

async function signPdf() {
  const formData = new FormData();

  formData.append("certificate", fs.readFileSync("certificate.pfx"), {
    filename: "certificate.pfx",
    contentType: "application/x-pkcs12"
  });
  formData.append("certificatePassword", "your_cert_password");
  formData.append("pdf", fs.readFileSync("document.pdf"), {
    filename: "document.pdf",
    contentType: "application/pdf"
  });

  const response = await axios.post(
    "https://api.pdfsignify.com/api/v1/sign-pdf",
    formData,
    {
      headers: {
        ...formData.getHeaders(),
        "AccessKey": "YOUR_ACCESS_KEY",
        "SecretKey": "YOUR_SECRET_KEY"
      },
      responseType: "arraybuffer"
    }
  );

  fs.writeFileSync("signed-document.pdf", response.data);
  console.log("Signed PDF saved!");
}

signPdf();

Using fetch Instead of axios

If you prefer the native fetch API (available in Node.js 18+), here's the equivalent. Note that Node.js's native fetch works with the standard FormData from the undici library or the built-in global.

javascript
import { readFileSync, writeFileSync } from "fs";

async function signPdfWithFetch() {
  const formData = new FormData();

  formData.append(
    "certificate",
    new Blob([readFileSync("certificate.pfx")]),
    "certificate.pfx"
  );
  formData.append("certificatePassword", "your_cert_password");
  formData.append(
    "pdf",
    new Blob([readFileSync("document.pdf")]),
    "document.pdf"
  );

  const response = await fetch(
    "https://api.pdfsignify.com/api/v1/sign-pdf",
    {
      method: "POST",
      headers: {
        "AccessKey": "YOUR_ACCESS_KEY",
        "SecretKey": "YOUR_SECRET_KEY"
      },
      body: formData
    }
  );

  const buffer = Buffer.from(await response.arrayBuffer());
  writeFileSync("signed-document.pdf", buffer);
  console.log("Signed PDF saved!");
}

signPdfWithFetch();

Full Example: Custom Signature Appearance

PDFSignify lets you customize exactly how the signature appears on the PDF — its position, size, the message displayed, the date format, and even a background image (like your company logo). Here's a complete example with all optional fields.

javascript
import axios from "axios";
import * as fs from "fs";
import FormData from "form-data";

async function signPdfWithCustomAppearance() {
  const formData = new FormData();

  formData.append("certificate", fs.readFileSync("certificate.pfx"), {
    filename: "certificate.pfx",
    contentType: "application/x-pkcs12"
  });
  formData.append("certificatePassword", "your_cert_password");
  formData.append("pdf", fs.readFileSync("document.pdf"), {
    filename: "document.pdf",
    contentType: "application/pdf"
  });
  formData.append("signatureBackgroundImage", fs.readFileSync("logo.png"), {
    filename: "logo.png",
    contentType: "image/png"
  });

  // Signature appearance
  formData.append("signaturePageAppearance", "-1"); // all pages
  formData.append("signatureMessage", "Digitally signed by ACME Corp");
  formData.append("signatureDateLabel", "Signed on");
  formData.append("signatureDateFormat", "Y-m-d H:i:s");
  formData.append("timezone", "America/New_York");
  formData.append("signatureHeight", "100");
  formData.append("signatureWidth", "200");
  formData.append("signatureXPosition", "350");
  formData.append("signatureYPosition", "50");

  // PDF metadata
  formData.append("pdfMetadataAuthor", "ACME Corporation");
  formData.append("pdfMetadataTitle", "Employment Contract");
  formData.append("pdfMetadataSubject", "Contract");
  formData.append("pdfMetadataKeywords", "contract,employment,signed");
  formData.append("pdfMetadataCreator", "ACME HR System");
  formData.append("pdfMetadataProducer", "PDFSignify");

  const response = await axios.post(
    "https://api.pdfsignify.com/api/v1/sign-pdf",
    formData,
    {
      headers: {
        ...formData.getHeaders(),
        "AccessKey": "YOUR_ACCESS_KEY",
        "SecretKey": "YOUR_SECRET_KEY"
      },
      responseType: "arraybuffer"
    }
  );

  fs.writeFileSync("signed-contract.pdf", response.data);
  console.log("Custom-signed PDF saved!");
}

signPdfWithCustomAppearance();

Building an Express Endpoint for Frontend Integration

In a real application, your frontend will upload a PDF to your backend, which then calls PDFSignify and returns the signed PDF. Here's how to build that with Express and multer.

javascript
import express from "express";
import multer from "multer";
import axios from "axios";
import FormData from "form-data";
import * as fs from "fs";

const app = express();
const upload = multer({ dest: "uploads/" });

app.post("/api/sign", upload.single("pdf"), async (req, res) => {
  try {
    const formData = new FormData();

    formData.append("certificate", fs.readFileSync("certificate.pfx"), {
      filename: "certificate.pfx",
      contentType: "application/x-pkcs12"
    });
    formData.append("certificatePassword", process.env.CERT_PASSWORD);
    formData.append("pdf", fs.readFileSync(req.file.path), {
      filename: req.file.originalname,
      contentType: "application/pdf"
    });
    formData.append("signatureMessage", "Signed via ACME Portal");

    const response = await axios.post(
      "https://api.pdfsignify.com/api/v1/sign-pdf",
      formData,
      {
        headers: {
          ...formData.getHeaders(),
          "AccessKey": process.env.PDFSIGNIFY_ACCESS_KEY,
          "SecretKey": process.env.PDFSIGNIFY_SECRET_KEY
        },
        responseType: "arraybuffer"
      }
    );

    // Clean up temp file
    fs.unlinkSync(req.file.path);

    res.contentType("application/pdf");
    res.send(response.data);
  } catch (error) {
    console.error("Signing failed:", error.message);
    res.status(500).json({ error: "Failed to sign PDF" });
  }
});

app.listen(3000, () => console.log("Server running on port 3000"));

Frontend: Uploading and Downloading the Signed PDF

On the frontend, you simply POST the PDF to your backend endpoint and trigger a download of the signed result.

javascript
async function signDocument(file) {
  const formData = new FormData();
  formData.append("pdf", file);

  const response = await fetch("/api/sign", {
    method: "POST",
    body: formData
  });

  const blob = await response.blob();
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "signed-" + file.name;
  a.click();
  URL.revokeObjectURL(url);
}

// Usage with a file input
document.querySelector("#file-input").addEventListener("change", (e) => {
  signDocument(e.target.files[0]);
});

Error Handling Best Practices

  • Validate the certificate password before signing using the /check-certificate-password endpoint
  • Check that the uploaded file is a valid PDF before sending it to the API
  • Handle network errors and API errors separately — a 4xx response means bad input, 5xx means retry later
  • Never expose your AccessKey, SecretKey, or certificate password to the client
  • Store credentials in environment variables, not in source code

Summary

Signing PDFs in JavaScript is a backend operation. Your server holds the certificate and API credentials, calls PDFSignify's synchronous API, and returns the signed PDF to the client. Whether you use axios or fetch, the pattern is the same: build a multipart form with the PDF and certificate, POST it to the sign-pdf endpoint, and write the binary response to a file or stream it to the user.

Keep the certificate on your server, keep credentials in env vars, and let PDFSignify handle the cryptography.