Back to Blog
ReactNode.jsFrontendIntegrationAPI

React PDF Signature Integration: Build a Full-Stack Signing App with PDFSignify

PDFSignify TeamMarch 18, 202612 min read

If you want users of your React application to upload a PDF and get it digitally signed with a certificate, you need a backend that calls the PDFSignify API. PDFSignify is a server-side API — it requires your certificate and secret keys, so the signing logic lives on your backend, not in the browser.

This guide shows how to build the complete stack: a React frontend for uploading files, and a Node.js/Express backend that sends them to PDFSignify and returns the signed PDF.

Architecture Overview

The flow is straightforward because PDFSignify is a synchronous API. There are no webhooks, no polling, and no document IDs to track. The user uploads a PDF, your backend signs it, and the signed file comes right back.

  • User selects a PDF in the React UI
  • React sends the file to your Express backend
  • Express forwards the PDF + certificate to PDFSignify
  • PDFSignify returns the signed PDF binary
  • Express sends the signed file back to React
  • User downloads the signed PDF

Prerequisites

  • Node.js 18+ with Express
  • React 18+ (Create React App, Vite, or Next.js)
  • A PDFSignify account with AccessKey and SecretKey
  • A .pfx or .p12 digital certificate and its password
  • axios and form-data packages

Part 1: The Express Backend

Install Dependencies

bash
npm install express multer axios form-data cors dotenv

Environment Variables

bash
PDFSIGNIFY_ACCESS_KEY=your_access_key
PDFSIGNIFY_SECRET_KEY=your_secret_key
CERTIFICATE_PASSWORD=your_certificate_password
PORT=4000

Express Server

javascript
require("dotenv").config();
const express = require("express");
const multer = require("multer");
const axios = require("axios");
const FormData = require("form-data");
const fs = require("fs");
const path = require("path");
const cors = require("cors");

const app = express();
app.use(cors());

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

const CERT_PATH = path.join(__dirname, "certificates", "company.pfx");

app.post("/api/sign", upload.single("pdf"), async (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: "No PDF file provided" });
  }

  try {
    const formData = new FormData();

    formData.append("pdf", fs.createReadStream(req.file.path), {
      filename: req.file.originalname,
      contentType: "application/pdf",
    });

    formData.append("certificate", fs.createReadStream(CERT_PATH), {
      filename: "certificate.pfx",
      contentType: "application/x-pkcs12",
    });

    formData.append(
      "certificatePassword",
      process.env.CERTIFICATE_PASSWORD
    );
    formData.append("signaturePageAppearance", "-1");
    formData.append("signatureMessage", "Digitally signed");
    formData.append("signatureHeight", "100");
    formData.append("signatureWidth", "150");
    formData.append("signatureXPosition", "180");
    formData.append("signatureYPosition", "100");

    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",
      }
    );

    fs.unlinkSync(req.file.path);

    res.setHeader("Content-Type", "application/pdf");
    res.setHeader(
      "Content-Disposition",
      "attachment; filename=signed_document.pdf"
    );
    res.send(Buffer.from(response.data));
  } catch (error) {
    fs.unlinkSync(req.file.path);
    console.error("Signing failed:", error.message);
    res.status(500).json({ error: "Failed to sign the document" });
  }
});

app.listen(process.env.PORT, () => {
  console.log("Server running on port " + process.env.PORT);
});

This endpoint receives the user's PDF via multer, builds a multipart request with the certificate and signing options, sends it to PDFSignify, and streams the signed PDF back. The temporary upload file is cleaned up in both the success and error paths.

Part 2: The React Frontend

File Upload Component

javascript
import { useState } from "react";
import axios from "axios";

export default function SignPdf() {
  const [file, setFile] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  const handleSign = async () => {
    if (!file) return;

    setLoading(true);
    setError("");

    const formData = new FormData();
    formData.append("pdf", file);

    try {
      const response = await axios.post(
        "http://localhost:4000/api/sign",
        formData,
        { responseType: "blob" }
      );

      const url = window.URL.createObjectURL(
        new Blob([response.data], { type: "application/pdf" })
      );
      const link = document.createElement("a");
      link.href = url;
      link.download = "signed_document.pdf";
      link.click();
      window.URL.revokeObjectURL(url);
    } catch (err) {
      setError("Signing failed. Please try again.");
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <h2>Sign a PDF Document</h2>
      <input
        type="file"
        accept=".pdf"
        onChange={(e) => setFile(e.target.files[0])}
      />
      <button onClick={handleSign} disabled={!file || loading}>
        {loading ? "Signing..." : "Sign PDF"}
      </button>
      {error && <p style={{ color: "red" }}>{error}</p>}
    </div>
  );
}

The React component uploads the PDF to your Express backend, receives the signed PDF as a blob, and triggers a browser download. The user never interacts with PDFSignify directly — your backend handles all the API communication.

Adding Signature Customization

You can let users customize the signature appearance by passing additional fields from the React form to your backend, and then forwarding them to PDFSignify.

javascript
// In your React component, add form fields:
const [message, setMessage] = useState("Digitally signed");
const [page, setPage] = useState("-1");

const handleSign = async () => {
  const formData = new FormData();
  formData.append("pdf", file);
  formData.append("signatureMessage", message);
  formData.append("signaturePageAppearance", page);

  const response = await axios.post(
    "http://localhost:4000/api/sign",
    formData,
    { responseType: "blob" }
  );
  // ... download logic
};
javascript
// In your Express backend, read the extra fields from req.body:
formData.append(
  "signatureMessage",
  req.body.signatureMessage || "Digitally signed"
);
formData.append(
  "signaturePageAppearance",
  req.body.signaturePageAppearance || "-1"
);

Adding a Signature Background Image

PDFSignify supports a custom signature background image — for example, a handwritten signature scan or a company stamp. You can accept this as a second file upload in your React form and forward it to the API.

javascript
// Accept a second file in Express (using multer fields)
const uploadFields = multer({ dest: "uploads/" }).fields([
  { name: "pdf", maxCount: 1 },
  { name: "signatureImage", maxCount: 1 },
]);

app.post("/api/sign", uploadFields, async (req, res) => {
  const formData = new FormData();
  formData.append("pdf", fs.createReadStream(req.files.pdf[0].path), {
    filename: "document.pdf",
    contentType: "application/pdf",
  });
  formData.append("certificate", fs.createReadStream(CERT_PATH), {
    filename: "certificate.pfx",
    contentType: "application/x-pkcs12",
  });
  formData.append("certificatePassword", process.env.CERTIFICATE_PASSWORD);

  if (req.files.signatureImage) {
    formData.append(
      "signatureBackgroundImage",
      fs.createReadStream(req.files.signatureImage[0].path),
      { filename: "signature.png", contentType: "image/png" }
    );
  }

  // ... send to PDFSignify and return result
});

Why the Backend Is Required

PDFSignify authenticates with AccessKey and SecretKey headers, and requires your .pfx certificate file. None of these should ever be exposed to the browser. The React frontend is purely a UI layer — file selection, progress feedback, and download handling. All signing logic runs on your server.

Production Considerations

  • Add authentication to your Express /api/sign endpoint so only authorized users can sign
  • Set file size limits on multer to prevent abuse
  • Store signed PDFs in cloud storage (S3, GCS) instead of returning them directly if you need an audit trail
  • Add rate limiting to prevent certificate abuse
  • Use HTTPS in production — never send certificates over plain HTTP
  • Consider streaming large PDF responses instead of buffering the entire file in memory

Full Project Structure

bash
project/
├── backend/
│   ├── certificates/
│   │   └── company.pfx
│   ├── uploads/          # temporary, cleaned after signing
│   ├── server.js
│   ├── .env
│   └── package.json
└── frontend/
    ├── src/
    │   └── SignPdf.jsx
    └── package.json

Key Takeaways

Because PDFSignify is synchronous, the React + Express integration is as simple as any file upload feature. There are no webhooks to configure, no document status to poll, and no signing URLs to generate. The entire round trip — upload, sign, download — happens in a single request-response cycle.

The best developer experience is when the complexity lives in the API, not in your code. Upload a PDF, get a signed PDF back.