Back to Blog
WorkflowArchitecture

How to Build an eSignature Workflow (From Scratch)

PDFSignify TeamMarch 25, 202614 min read

Building a document signing workflow doesn't require a massive platform with built-in email delivery, webhook infrastructure, and workflow state machines. If you have a synchronous signing API, you can build exactly the workflow you need — with your own storage, your own logic, and full control over the user experience.

This article shows you how to architect a complete document signing workflow using PDFSignify's synchronous API as the signing engine. PDFSignify handles the cryptographic signing — you handle the orchestration.

The Architecture: Synchronous Signing + Your Workflow Layer

Traditional eSignature platforms bundle everything together: document storage, signer management, email delivery, status tracking, and the actual signing. This is convenient but inflexible. If you want to customize the flow, send notifications through your own channels, or store documents in your own system, you're fighting the platform.

A better approach for developers is to decouple the signing from the workflow. PDFSignify provides a stateless signing API — you POST a PDF and a digital certificate, and you get a signed PDF back. There are no stored documents, no signer emails, no webhook callbacks. You build the workflow around this core operation using the tools you already know.

Core Components of Your Workflow

  • Document storage — where unsigned and signed PDFs live (your database, S3, GCS, etc.)
  • Certificate management — securely storing and accessing .pfx/.p12 certificates
  • Signing orchestration — deciding when to sign, calling the API, handling the response
  • Status tracking — recording what happened (pending, signed, failed) in your own database
  • Notification layer — emailing or notifying users through your existing channels
  • Audit trail — logging every signing event for compliance

Step 1: Secure Certificate Storage

Your digital certificate (.pfx or .p12) contains a private key. Treat it like any other secret. Never store it in your codebase, never commit it to version control, and never expose it in client-side code.

  • AWS Secrets Manager or AWS S3 with SSE — store the certificate as a binary secret
  • HashiCorp Vault — store and rotate certificates with access policies
  • Azure Key Vault — native PKCS#12 certificate support with managed access
  • Environment variables — acceptable for the certificate password, but not the certificate file itself
  • Encrypted database column — store the certificate as an encrypted blob if you need per-user certificates
javascript
// Example: loading certificate from AWS Secrets Manager
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";

const client = new SecretsManagerClient({ region: "us-east-1" });

async function getCertificate() {
  const response = await client.send(
    new GetSecretValueCommand({ SecretId: "signing-certificate" })
  );
  return Buffer.from(response.SecretBinary);
}

async function getCertPassword() {
  const response = await client.send(
    new GetSecretValueCommand({ SecretId: "signing-cert-password" })
  );
  return response.SecretString;
}

Step 2: Document Storage and Lifecycle

Since PDFSignify doesn't store documents, your application is responsible for managing the document lifecycle. A simple approach is to use cloud object storage (S3, GCS, Azure Blob) for the PDF files and a database table to track their status.

sql
CREATE TABLE documents (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  filename VARCHAR(255) NOT NULL,
  storage_key VARCHAR(500) NOT NULL,
  status VARCHAR(50) DEFAULT 'pending',
  requested_by VARCHAR(255),
  signed_at TIMESTAMP,
  signed_storage_key VARCHAR(500),
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

The status field is entirely under your control. You might use values like 'pending', 'signing', 'signed', or 'failed' — whatever makes sense for your business logic. This is your workflow, not the API's.

Step 3: The Signing Orchestration Layer

The signing service ties everything together. It fetches the document, loads the certificate, calls the PDFSignify API, stores the signed document, and updates the database record.

javascript
import axios from "axios";
import FormData from "form-data";

class SigningService {
  constructor(accessKey, secretKey, storage, db) {
    this.accessKey = accessKey;
    this.secretKey = secretKey;
    this.storage = storage; // S3, GCS, etc.
    this.db = db;
  }

  async signDocument(documentId, certBuffer, certPassword, options = {}) {
    const doc = await this.db.query("SELECT * FROM documents WHERE id = $1", [documentId]);
    if (!doc) throw new Error("Document not found");

    await this.db.query(
      "UPDATE documents SET status = 'signing', updated_at = NOW() WHERE id = $1",
      [documentId]
    );

    try {
      const pdfBuffer = await this.storage.download(doc.storage_key);

      const formData = new FormData();
      formData.append("certificate", certBuffer, {
        filename: "certificate.pfx",
        contentType: "application/x-pkcs12"
      });
      formData.append("certificatePassword", certPassword);
      formData.append("pdf", pdfBuffer, {
        filename: doc.filename,
        contentType: "application/pdf"
      });

      for (const [key, value] of Object.entries(options)) {
        formData.append(key, value);
      }

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

      const signedKey = "signed/" + doc.storage_key;
      await this.storage.upload(signedKey, Buffer.from(response.data));

      await this.db.query(
        "UPDATE documents SET status = 'signed', signed_at = NOW(), signed_storage_key = $1, updated_at = NOW() WHERE id = $2",
        [signedKey, documentId]
      );

      return { success: true, signedKey };
    } catch (error) {
      await this.db.query(
        "UPDATE documents SET status = 'failed', updated_at = NOW() WHERE id = $1",
        [documentId]
      );
      throw error;
    }
  }
}

Step 4: Batch Signing

Because PDFSignify's API is synchronous, batch signing is straightforward: loop through your documents and sign each one. You can process them sequentially or in parallel with concurrency limits.

javascript
async function signBatch(documentIds, certBuffer, certPassword, signingService) {
  const concurrency = 5;
  const results = [];

  for (let i = 0; i < documentIds.length; i += concurrency) {
    const batch = documentIds.slice(i, i + concurrency);
    const batchResults = await Promise.allSettled(
      batch.map(id => signingService.signDocument(id, certBuffer, certPassword, {
        signatureMessage: "Batch signed by ACME Corp"
      }))
    );
    results.push(...batchResults);
  }

  const succeeded = results.filter(r => r.status === "fulfilled").length;
  const failed = results.filter(r => r.status === "rejected").length;
  console.log(succeeded + " signed, " + failed + " failed out of " + documentIds.length);
  return results;
}

Step 5: Notifications

Since you own the workflow, you can send notifications through whatever channels make sense for your application — email, Slack, in-app notifications, SMS, or push notifications. You're not locked into the signing platform's notification system.

javascript
async function signAndNotify(documentId, certBuffer, certPassword, signingService, notifier) {
  const result = await signingService.signDocument(documentId, certBuffer, certPassword);

  await notifier.send({
    channel: "email",
    to: result.requestedBy,
    subject: "Your document has been signed",
    body: "The document has been digitally signed and is ready for download."
  });

  await notifier.send({
    channel: "slack",
    webhookUrl: process.env.SLACK_WEBHOOK,
    text: "Document " + documentId + " signed successfully."
  });
}

Step 6: Audit Trail

For compliance, you'll want to log every signing event. Since PDFSignify is stateless and doesn't store your documents, the audit trail lives entirely in your system — which is often a compliance advantage.

sql
CREATE TABLE signing_audit_log (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  document_id UUID REFERENCES documents(id),
  action VARCHAR(50) NOT NULL,
  status VARCHAR(50) NOT NULL,
  initiated_by VARCHAR(255),
  ip_address INET,
  certificate_fingerprint VARCHAR(128),
  timestamp TIMESTAMP DEFAULT NOW(),
  details JSONB
);

Log every operation: certificate validation, signing attempts, successes, and failures. Include the certificate fingerprint (a hash of the public certificate, not the private key) so you can trace which certificate was used for each signature.

Step 7: Exposing the Workflow via API

Finally, expose your signing workflow through your own API. This gives your frontend (or your customers' integrations) a clean interface that hides the certificate management and signing details.

javascript
// Express routes
app.post("/api/documents", upload.single("pdf"), async (req, res) => {
  const storageKey = await storage.upload("uploads/" + req.file.originalname, req.file.buffer);
  const doc = await db.query(
    "INSERT INTO documents (filename, storage_key, requested_by) VALUES ($1, $2, $3) RETURNING *",
    [req.file.originalname, storageKey, req.user.email]
  );
  res.json(doc);
});

app.post("/api/documents/:id/sign", async (req, res) => {
  const certBuffer = await getCertificate();
  const certPassword = await getCertPassword();
  const result = await signingService.signDocument(req.params.id, certBuffer, certPassword);
  res.json(result);
});

app.get("/api/documents/:id/download-signed", async (req, res) => {
  const doc = await db.query("SELECT * FROM documents WHERE id = $1", [req.params.id]);
  if (doc.status !== "signed") return res.status(400).json({ error: "Document not yet signed" });
  const buffer = await storage.download(doc.signed_storage_key);
  res.set("Content-Type", "application/pdf");
  res.send(buffer);
});

Why This Approach Works

  • Full control — you decide the workflow, storage, notifications, and user experience
  • No vendor lock-in — your documents and certificates never leave your infrastructure (except briefly for signing)
  • Simpler debugging — every component is yours, so there are no black-box webhook failures or opaque status transitions
  • Better compliance — your audit trail is in your database, queryable and exportable on your terms
  • Scalable — sign one document or ten thousand; the synchronous API handles each request independently

Summary

You don't need a full-featured eSignature platform to build a signing workflow. With a synchronous signing API like PDFSignify, you get the hard part (cryptographic certificate-based signing) handled for you, while keeping full ownership of the workflow, storage, notifications, and audit trail. The result is a system that fits your architecture instead of forcing you into someone else's.

The best workflow is the one you design for your use case — not the one a signing platform designed for everyone.